diff --git a/.github/workflows/agent-files-detect.yaml b/.github/workflows/agent-files-detect.yaml index 9f1d609a2..dc490535a 100644 --- a/.github/workflows/agent-files-detect.yaml +++ b/.github/workflows/agent-files-detect.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: fetch-depth: 0 persist-credentials: false @@ -86,9 +86,22 @@ jobs: # $1 = diff range expression (e.g. "A..B" incremental or "A...B" net) check_range() { local range="$1" - local changed - changed=$(git diff --no-ext-diff --name-only "${range}") - DIRECT_MATCH=$(echo "${changed}" | grep -E '(^|/)(AGENTS\.md|CLAUDE\.md|GEMINI\.md)$|(^|/)skills/|^\.github/workflows/agent-files-(detect|enforce)\.yaml$' || true) + local changed all_paths + # Use --name-status to catch renames/deletes (exposes old path via R/D status lines). + # Format: "A\tpath", "M\tpath", "R100\told\tnew", "D\tpath" + changed=$(git diff --no-ext-diff --name-status "${range}") + # Extract all paths (both old and new for renames) to check against protected patterns. + all_paths=$(echo "${changed}" | awk '{ + if ($1 ~ /^R/) { + # Rename: $2 = old path, $3 = new path + print $2 + print $3 + } else { + # Add/Modify/Delete: $2 = path + print $2 + } + }') + DIRECT_MATCH=$(echo "${all_paths}" | grep -E '(^|/)(AGENTS\.md|CLAUDE\.md|GEMINI\.md)$|(^|/)skills/|^\.github/workflows/agent-files-(detect|enforce)\.yaml$' || true) if [ -n "${DIRECT_MATCH}" ]; then RANGE_RESULT="true" elif git diff --no-ext-diff "${range}" | grep -qiE 'AGENTS\.md|CLAUDE\.md|GEMINI\.md|\.claude|\.cursor|\.vscode|\.agents|skills/'; then diff --git a/.github/workflows/agent-files-enforce.yaml b/.github/workflows/agent-files-enforce.yaml index 138c38286..470419702 100644 --- a/.github/workflows/agent-files-enforce.yaml +++ b/.github/workflows/agent-files-enforce.yaml @@ -69,9 +69,13 @@ jobs: repo: context.repo.repo, pull_number: parseInt(process.env.PR_NUMBER, 10), }); + const workflowPaths = [ + '.github/workflows/agent-files-detect.yaml', + '.github/workflows/agent-files-enforce.yaml' + ]; const changed = files.some(f => - f.filename === '.github/workflows/agent-files-detect.yaml' || - f.filename === '.github/workflows/agent-files-enforce.yaml' + workflowPaths.includes(f.filename) || + (f.previous_filename && workflowPaths.includes(f.previous_filename)) ); core.setOutput('changed', changed.toString()); @@ -120,6 +124,32 @@ jobs: console.log(`PR opener: ${u.login} (id ${u.id}, type ${u.type}); trusted_bot=${trusted}`); core.setOutput('trusted_bot', trusted.toString()); + # Verify workflow run head matches current PR head. If the PR was force-pushed or + # received new commits after the detect workflow started, the artifact is stale and + # must not be used to mutate labels or post status (would incorrectly approve/block + # a different commit than what the artifact describes). + - name: Verify artifact freshness + id: freshness-check + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + env: + PR_NUMBER: ${{ needs.resolve-pr.outputs.pr_number }} + WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + with: + script: | + const prNumber = parseInt(process.env.PR_NUMBER, 10); + const workflowRunHeadSha = process.env.WORKFLOW_RUN_HEAD_SHA; + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + const currentHeadSha = pr.head.sha; + const fresh = workflowRunHeadSha === currentHeadSha; + if (!fresh) { + console.log(`Stale artifact detected: workflow run analyzed ${workflowRunHeadSha.substring(0,7)} but PR head is now ${currentHeadSha.substring(0,7)}. Skipping label mutation.`); + } + core.setOutput('fresh', fresh.toString()); + - name: Download detection artifact id: download continue-on-error: true @@ -149,6 +179,14 @@ jobs: echo "${value}" } + # Stale artifact: the workflow run analyzed a different commit than the PR's + # current head. Do not mutate labels (a newer detect run may already be queued). + if [ "${{ steps.freshness-check.outputs.fresh }}" != "true" ]; then + echo "Artifact is stale — skipping label mutation." + echo "label_action=neutral" >> "${GITHUB_OUTPUT}" + exit 0 + fi + # Trusted bot (verified identity + all commits verified-signed): never label. if [ "${{ steps.bot-check.outputs.trusted_bot }}" = "true" ]; then echo "PR opened by a trusted bot — skipping label."