yuqi1129 commented on code in PR #13049:
URL: https://github.com/apache/gravitino/pull/13049#discussion_r4345267337


##########
.github/workflows/fork-ci-status.yml:
##########
@@ -0,0 +1,157 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.
+#
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: Fork CI Status
+
+on:
+  pull_request_target:
+    types: [opened, synchronize, reopened]
+
+permissions:
+  actions: read
+  checks: write
+  contents: read
+
+jobs:
+  notify:
+    name: Notify fork CI
+    if: github.repository == 'apache/gravitino'
+    runs-on: ubuntu-slim
+    steps:
+      - name: Create fork CI check
+        uses: actions/github-script@v7
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          retries: 3
+          script: |
+            const pr = context.payload.pull_request;
+            if (!pr.head.repo) {
+              core.warning('The pull request head repository no longer 
exists.');
+              return;
+            }
+            const owner = pr.head.repo.owner.login;
+            const repo = pr.head.repo.name;
+            const branch = pr.head.ref;
+            const headSha = pr.head.sha;
+            const workflow = 'fork-ci.yml';
+
+            // Pull requests to a branch without Fork CI, such as a release 
branch it has not
+            // been backported to, still run that branch's own pull_request 
workflows.
+            try {
+              await github.rest.repos.getContent({
+                owner: context.repo.owner, repo: context.repo.repo,
+                path: `.github/workflows/${workflow}`, ref: pr.base.ref });
+            } catch (error) {
+              if (error.status !== 404) throw error;
+              core.info(`${pr.base.ref} has no Fork CI; no check is created.`);
+              return;
+            }
+
+            let run;
+            for (let attempt = 0; attempt < 3 && !run; attempt++) {
+              if (attempt > 0) {
+                await new Promise(resolve => setTimeout(resolve, 3000));
+              }
+              try {
+                const runs = await github.paginate(
+                  'GET 
/repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs',
+                  { owner, repo, workflow_id: workflow, branch, per_page: 100 
});
+                run = runs.find(candidate => candidate.head_sha === headSha);
+              } catch (error) {
+                core.warning(`Unable to read fork Actions: ${error.message}`);
+              }
+            }
+
+            // The check mirrors the Required CI job rather than the run 
conclusion: a run whose
+            // suites were all skipped (no pull request yet, or Fork CI 
disabled in the fork)
+            // still concludes as success. Failed jobs are listed while the 
run is still going,
+            // so reviewers and contributors can open their logs without 
waiting for the run.
+            const mirror = async (owner, repo, run) => {
+              const jobs = await github.paginate(
+                'GET 
/repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}/jobs',
+                { owner, repo, run_id: run.id, attempt_number: 
run.run_attempt, per_page: 100 });
+              const failed = jobs.filter(job =>
+                job.name !== 'Required CI' && ['failure', 
'timed_out'].includes(job.conclusion));
+              const failedNote = failed.length
+                ? `Failed jobs:\n${failed.map(job => `- 
[${job.name}](${job.html_url})`).join('\n')}`
+                : '';
+              if (run.status !== 'completed') {
+                // A run stays queued while any of its jobs waits for a 
runner, even after other
+                // jobs have finished, so report it as in progress once any 
job has started.
+                const started = jobs.some(job => ['in_progress', 
'completed'].includes(job.status));
+                return {
+                  status: started ? 'in_progress' : 'queued',
+                  title: failed.length ? `Running, ${failed.length} job(s) 
failed so far` : 'Running',
+                  note: failedNote
+                };
+              }
+              if (run.conclusion !== 'success') {
+                // Check runs reject some run conclusions, such as 
startup_failure.
+                const conclusion =
+                  ['action_required', 'cancelled', 'failure', 
'timed_out'].includes(run.conclusion)
+                    ? run.conclusion
+                    : (['neutral', 'skipped'].includes(run.conclusion) ? 
'action_required' : 'failure');
+                return {
+                  status: 'completed', conclusion,
+                  title: failed.length ? `${failed.length} job(s) failed` : 
`Fork CI ${run.conclusion}`,
+                  note: [`The fork workflow run concluded with 
${run.conclusion}.`, failedNote]
+                    .filter(Boolean).join('\n\n')
+                };
+              }
+              const required = jobs.find(job => job.name === 'Required CI');

Review Comment:
   Addressed in the fixes through 
[effa17fc17](https://github.com/apache/gravitino/commit/effa17fc1779a2b2a96a8980778a74fb0c3b07f3).
 Before accepting a fork result, the publisher compares the immutable Git tree 
SHAs for `.github/workflows`, `.github/actions`, `.github/scripts`, and 
`.asf.yaml` with the exact PR base commit. It also requires successful jobs 
from all 16 callers and a successful final job encoding the exact head, base 
ref and base SHA. Changed CI definitions require an explicit 
committer-dispatched upstream run using reviewed default-branch workflows.
   
   Added regressions for fabricated results and changed definitions. The 
publisher no longer restores Actions caches: neither verification decisions nor 
refresh scheduling can be supplied by a contributor-writable cache.



##########
.github/workflows/asf-allowlist-check.yml:
##########
@@ -20,15 +20,13 @@
 name: "ASF Allowlist Check"
 
 on:
-  workflow_dispatch:
+  # Also run upstream for pull requests that change workflows or actions, so 
the

Review Comment:
   Addressed with a separate default-branch `Trusted ASF Allowlist` workflow, 
and added `ASF Allowlist Check` to the required contexts in `.asf.yaml`.
   
   I used an unprivileged PR signal followed by `workflow_run`: [ASF's current 
policy](https://infra.apache.org/github-actions-policy.html#avoid-using-the-pull-request-target-trigger)
 disables `pull_request_target` from November 2026. The trusted scanner checks 
out only the default branch, downloads the exact PR head's bounded YAML files 
as data outside that checkout, and runs the Apache allowlist action. It never 
checks out or executes PR code, and rechecks the current PR head before 
publishing the check. End-to-end activation still needs the default-branch 
rollout.



##########
.github/workflows/fork-ci.yml:
##########
@@ -0,0 +1,310 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Run the complete CI entry point from branch pushes in the contributor's 
repository.
+name: Fork CI
+
+on:
+  push:
+    branches: ['**']
+
+concurrency:
+  group: fork-ci-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  eligibility:
+    name: Resolve fork CI eligibility
+    # Downstream mirrors can set this repository variable to keep the
+    # inherited workflow completely dormant.
+    if: vars.DISABLE_FORK_CI != 'true'
+    runs-on: ubuntu-slim
+    outputs:
+      applicable: ${{ steps.resolve.outputs.applicable }}
+      valid: ${{ steps.resolve.outputs.valid == 'true' || 
steps.merge.outputs.valid == 'true' }}
+    steps:
+      - name: Resolve repository and pull request
+        id: resolve
+        env:
+          GH_TOKEN: ${{ github.token }}
+          HEAD_BRANCH: ${{ github.ref_name }}
+          HEAD_OWNER: ${{ github.repository_owner }}
+          HEAD_REPOSITORY: ${{ github.repository }}
+          HEAD_SHA: ${{ github.sha }}
+          REPOSITORY_FORK: ${{ github.event.repository.fork }}
+        run: |
+          set -euo pipefail
+
+          echo "applicable=false" >> "${GITHUB_OUTPUT}"
+          echo "valid=false" >> "${GITHUB_OUTPUT}"
+
+          if [[ "${HEAD_REPOSITORY}" == "apache/gravitino" && \
+            ("${GITHUB_REF}" == "refs/heads/main" || "${GITHUB_REF}" == 
refs/heads/branch-*) ]]; then
+            echo "applicable=true" >> "${GITHUB_OUTPUT}"
+            echo "base_ref=${GITHUB_REF_NAME}" >> "${GITHUB_OUTPUT}"
+            echo "valid=true" >> "${GITHUB_OUTPUT}"
+            exit 0
+          fi
+
+          if [[ "${HEAD_REPOSITORY}" != "apache/gravitino" && 
"${REPOSITORY_FORK}" != "true" ]]; then
+            echo "This repository is not a fork; it runs its own CI and Fork 
CI is dormant."
+            exit 0
+          fi
+
+          repository_response="$(curl --fail-with-body --silent --show-error 
--retry 3 --retry-all-errors --retry-delay 5 \
+            --header "Accept: application/vnd.github+json" \
+            --header "Authorization: Bearer ${GH_TOKEN}" \
+            --header "X-GitHub-Api-Version: 2022-11-28" \
+            "${GITHUB_API_URL}/repos/${HEAD_REPOSITORY}")"
+          source_repository="$(jq -r '.source.full_name // empty' <<< 
"${repository_response}")"
+
+          if [[ "${HEAD_REPOSITORY}" != "apache/gravitino" && \
+            "${source_repository}" != "apache/gravitino" ]]; then
+            echo "This repository is not in the apache/gravitino fork network; 
Fork CI is dormant."
+            exit 0
+          fi
+
+          echo "applicable=true" >> "${GITHUB_OUTPUT}"
+          matches="[]"
+          match_count=0
+          # Give the contributor a few minutes to open the pull request after 
pushing.
+          for attempt in $(seq 1 12); do
+            response="$(curl --fail-with-body --silent --show-error --retry 3 
--retry-all-errors --retry-delay 5 \
+              --header "Accept: application/vnd.github+json" \
+              --header "Authorization: Bearer ${GH_TOKEN}" \
+              --header "X-GitHub-Api-Version: 2022-11-28" \
+              --get \
+              --data-urlencode "state=open" \
+              --data-urlencode "head=${HEAD_OWNER}:${HEAD_BRANCH}" \
+              --data-urlencode "per_page=10" \
+              "https://api.github.com/repos/apache/gravitino/pulls";)"
+            matches="$(jq -r \
+              --arg head_sha "${HEAD_SHA}" \
+              --arg head_repository "${HEAD_REPOSITORY}" \
+              '[.[] | select(.head.sha == $head_sha and .head.repo.full_name 
== $head_repository)]' \
+              <<< "${response}")"
+            match_count="$(jq -r 'length' <<< "${matches}")"
+            if [[ "${match_count}" == "1" ]]; then
+              break
+            fi
+            if [[ "${match_count}" != "0" || "${attempt}" == "12" ]]; then
+              break
+            fi
+            sleep 15
+          done
+
+          if [[ "${match_count}" == "0" && "${HEAD_REPOSITORY}" == 
"apache/gravitino" ]]; then
+            # Branches in the upstream repository only run for pull requests.
+            echo "No open pull request for ${HEAD_BRANCH}; Fork CI is dormant."
+            echo "applicable=false" >> "${GITHUB_OUTPUT}"
+            exit 0
+          fi
+
+          api() {
+            curl --fail-with-body --silent --show-error --retry 3 
--retry-all-errors --retry-delay 5 \
+              --header "Accept: application/vnd.github+json" \
+              --header "Authorization: Bearer ${GH_TOKEN}" \
+              --header "X-GitHub-Api-Version: 2022-11-28" \
+              "https://api.github.com/repos/apache/gravitino$1";
+          }
+
+          if [[ "${match_count}" == "0" ]]; then

Review Comment:
   Fixed. Eligibility now resolves the head SHA, base ref and base SHA once and 
passes that immutable context through every reusable suite. The final Required 
CI job includes all three values, and the publisher requires an exact match 
with the current PR. A pre-PR run against another base cannot pass.
   
   The PR signal now includes `edited`, so retargeting wakes the trusted 
publisher. It withholds success for a mismatched context; the author must push 
again or obtain a matching trusted run. Added exact-context and 
publication-race regression tests.



##########
.github/workflows/fork-ci-status-update.yml:
##########
@@ -0,0 +1,299 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.
+#
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: Update Fork CI Status
+
+on:
+  schedule:
+    - cron: '*/5 * * * *'
+  workflow_dispatch:
+
+permissions:
+  actions: read
+  checks: write
+  contents: read
+  pull-requests: read
+
+concurrency:
+  group: fork-ci-status-update
+  cancel-in-progress: false
+
+jobs:
+  update:
+    name: Update fork CI checks
+    if: github.repository == 'apache/gravitino'
+    runs-on: ubuntu-slim
+    steps:
+      # The state of the previous sweep lets this sweep skip pull requests 
that have a
+      # final result and no new activity, without spending API calls on them.
+      - name: Restore previous sweep state
+        uses: actions/cache/restore@v4
+        with:
+          path: .fork-ci-status
+          key: fork-ci-status-${{ github.run_id }}-${{ github.run_attempt }}
+          restore-keys: fork-ci-status-
+
+      - name: Synchronize fork CI checks
+        uses: actions/github-script@v7
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          retries: 3
+          script: |
+            const fs = require('fs');
+            const stateFile = '.fork-ci-status/state.json';
+            const HOUR = 60 * 60 * 1000;
+            // A pull request with activity in this window is always checked, 
which covers
+            // the time between a push and the check created by the Fork CI 
Status workflow.
+            const RECENT_ACTIVITY = HOUR;
+            // How long a completed check is still watched for a re-run of the 
fork workflow.
+            // action_required is also set when the fork run had not started 
when the check was created.
+            const WATCH = { action_required: 24 * HOUR, default: 7 * 24 * HOUR 
};
+            // Pull requests without a running check are checked at most this 
often, so sweeping
+            // every 5 minutes spends API calls mainly on the checks that are 
about to change.
+            const IDLE_INTERVAL = 15 * 60 * 1000;
+            const FINAL = ['success', 'neutral', 'skipped'];
+
+            const rateLimit = async () => (await 
github.rest.rateLimit.get()).data.resources.core;
+            const budgetBefore = await rateLimit();
+            let calls = 0;
+            github.hook.before('request', () => { calls++; });
+
+            let previous = {};
+            try {
+              previous = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
+            } catch (error) {
+              core.info('No previous sweep state; checking every pull 
request.');
+            }
+            const next = {};
+            const now = Date.now();
+
+            // The check mirrors the Required CI job rather than the run 
conclusion: a run whose
+            // suites were all skipped (no pull request yet, or Fork CI 
disabled in the fork)
+            // still concludes as success. Failed jobs are listed while the 
run is still going,
+            // so reviewers and contributors can open their logs without 
waiting for the run.
+            const mirror = async (owner, repo, run) => {
+              const jobs = await github.paginate(
+                'GET 
/repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}/jobs',
+                { owner, repo, run_id: run.id, attempt_number: 
run.run_attempt, per_page: 100 });
+              const failed = jobs.filter(job =>
+                job.name !== 'Required CI' && ['failure', 
'timed_out'].includes(job.conclusion));
+              const failedNote = failed.length
+                ? `Failed jobs:\n${failed.map(job => `- 
[${job.name}](${job.html_url})`).join('\n')}`
+                : '';
+              if (run.status !== 'completed') {
+                // A run stays queued while any of its jobs waits for a 
runner, even after other
+                // jobs have finished, so report it as in progress once any 
job has started.
+                const started = jobs.some(job => ['in_progress', 
'completed'].includes(job.status));
+                return {
+                  status: started ? 'in_progress' : 'queued',
+                  title: failed.length ? `Running, ${failed.length} job(s) 
failed so far` : 'Running',
+                  note: failedNote
+                };
+              }
+              if (run.conclusion !== 'success') {
+                // Check runs reject some run conclusions, such as 
startup_failure.
+                const conclusion =
+                  ['action_required', 'cancelled', 'failure', 
'timed_out'].includes(run.conclusion)
+                    ? run.conclusion
+                    : (['neutral', 'skipped'].includes(run.conclusion) ? 
'action_required' : 'failure');
+                return {
+                  status: 'completed', conclusion,
+                  title: failed.length ? `${failed.length} job(s) failed` : 
`Fork CI ${run.conclusion}`,
+                  note: [`The fork workflow run concluded with 
${run.conclusion}.`, failedNote]
+                    .filter(Boolean).join('\n\n')
+                };
+              }
+              const required = jobs.find(job => job.name === 'Required CI');
+              if (required && required.conclusion === 'success') {
+                return { status: 'completed', conclusion: 'success', title: 
'All required suites passed' };
+              }
+              return {
+                status: 'completed', conclusion: 'action_required',
+                title: 'No suites were verified',
+                note: `Required CI was ${required ? required.conclusion : 'not 
run'}, so no suites were verified. ` +
+                  'Open the pull request first and check that Fork CI is 
enabled in the fork, then push the branch again.'
+              };
+            };
+            const NOT_AVAILABLE = 'Enable GitHub Actions in the fork, merge or 
rebase the branch onto the latest ' +
+              'base branch so it contains fork-ci.yml, and push it again.';
+
+            const skippable = (pr, state) => {

Review Comment:
   Fixed by removing persistent cached state and the `skippable` path entirely. 
Scheduled refreshes continue visiting all open PRs, including old successful 
results, with a rotating starting position and a bounded API budget. Manual 
dispatch with a PR number refreshes that PR directly. CONTRIBUTING now 
describes scheduling/queue delays without promising immediate updates.
   
   The publisher observes the current run attempt, replaces stale success when 
a rerun is running or fails, and combines each named job's latest execution 
across attempts to support partial reruns. Added failed-rerun, manual-refresh 
and partial-rerun tests.



##########
.asf.yaml:
##########
@@ -46,7 +46,10 @@ github:
   protected_branches:
     main:
       required_status_checks:
-        strict: true
+        strict: false

Review Comment:
   Fixed: restored `strict: true`; the up-to-date requirement is retained. The 
tested base SHA is now frozen before the suites start, encoded in the final job 
name, recorded in the upstream check metadata, and compared with the current PR 
base before success is published. The publisher also rereads the PR immediately 
before writing the check.
   
   CONTRIBUTING and the PR description explicitly state that a base advance 
requires updating the branch and running CI again. Added base-SHA mismatch and 
publication-race tests.



##########
.github/workflows/fork-ci.yml:
##########
@@ -0,0 +1,310 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Run the complete CI entry point from branch pushes in the contributor's 
repository.
+name: Fork CI
+
+on:
+  push:
+    branches: ['**']
+
+concurrency:
+  group: fork-ci-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  eligibility:
+    name: Resolve fork CI eligibility
+    # Downstream mirrors can set this repository variable to keep the
+    # inherited workflow completely dormant.
+    if: vars.DISABLE_FORK_CI != 'true'
+    runs-on: ubuntu-slim
+    outputs:
+      applicable: ${{ steps.resolve.outputs.applicable }}
+      valid: ${{ steps.resolve.outputs.valid == 'true' || 
steps.merge.outputs.valid == 'true' }}
+    steps:
+      - name: Resolve repository and pull request
+        id: resolve
+        env:
+          GH_TOKEN: ${{ github.token }}
+          HEAD_BRANCH: ${{ github.ref_name }}
+          HEAD_OWNER: ${{ github.repository_owner }}
+          HEAD_REPOSITORY: ${{ github.repository }}
+          HEAD_SHA: ${{ github.sha }}
+          REPOSITORY_FORK: ${{ github.event.repository.fork }}
+        run: |
+          set -euo pipefail
+
+          echo "applicable=false" >> "${GITHUB_OUTPUT}"
+          echo "valid=false" >> "${GITHUB_OUTPUT}"
+
+          if [[ "${HEAD_REPOSITORY}" == "apache/gravitino" && \
+            ("${GITHUB_REF}" == "refs/heads/main" || "${GITHUB_REF}" == 
refs/heads/branch-*) ]]; then
+            echo "applicable=true" >> "${GITHUB_OUTPUT}"
+            echo "base_ref=${GITHUB_REF_NAME}" >> "${GITHUB_OUTPUT}"
+            echo "valid=true" >> "${GITHUB_OUTPUT}"
+            exit 0
+          fi
+
+          if [[ "${HEAD_REPOSITORY}" != "apache/gravitino" && 
"${REPOSITORY_FORK}" != "true" ]]; then

Review Comment:
   Fixed. Standalone repositories and unrelated fork networks now resolve 
`valid=true`, `sync=false` and test their own pushed commit, without fetching 
or merging an Apache base. Added a resolver regression test for the standalone 
path.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to