GitNexus/.github/workflows/pr-autofix-publish.yml
Gergő Magyar fa70fd8343
chore(deps): consolidate ten Dependabot updates and fix compatibility (#3592)
* chore(deps): consolidate Dependabot GitHub Actions updates

* chore(deps): consolidate web updates and secure transitive dependencies

* fix(web): preserve diagram appearance with Mermaid 12

* test(ci): align workflow security tests with updated action pins

* fix(ci): split coverage across eight shards to avoid job timeout

CI run 38693066214 cancelled Ubuntu coverage shard 1 after 25 minutes while tests were still passing. The missing coverage blob then caused the execution audit and CI gate to fail.

Increase only the coverage shard count from four to eight. Keep the timeout, full suite, coverage thresholds, and execution audit intact.

Validation: all 1,247 test files form a complete, disjoint eight-shard partition using the configured Vitest sequencer; 117 targeted tests passed (2 existing skips); workflow generator, Prettier, concurrency validation, and diff checks passed.

* fix(ci): leave headroom for Windows test shards

Run 38697469283 exhausted the 20-minute Windows watchdog. Use ten platform shards while retaining eight coverage shards and all existing timeouts, coverage gates, and execution audits. The accompanying timing refresh and recorded-profile regression keep all 145 platform-sensitive test files covered exactly once.

* fix(ci): refresh underestimated Windows suite timings

Refresh eight materially underestimated timing weights using Windows runs 38697469283 and 38693066214. The old partition placed native suites ahead of cli-e2e with insufficient time left under the watchdog. Keep every suite and the partitioning algorithm unchanged.

* test(ci): replay the Windows timeout workload

Add a deterministic regression from recorded Windows CI timings, independent of the scheduling table. It failed before the fix with a 1,426-second shard and now protects three minutes of watchdog headroom.

Validation: 118 targeted tests passed (2 existing skips); typecheck, formatting, workflow generation, concurrency and diff checks passed. Independent review found no actionable issues. All 145 platform test files are selected exactly once; replaying the complete observed profile gives a 973-second maximum across ten shards. Hosted CI remains the final elapsed-time check.
2026-10-15 10:43:04 +01:00

287 lines
13 KiB
YAML

name: PR Autofix (publish)
# TRUSTED HALF of the autofix pipeline.
#
# Triggered by `pr-autofix.yml` completing on a PR (including fork PRs).
# Downloads the diff artifact produced by the untrusted job, verifies
# its claimed PR identity against the workflow_run authority, then
# posts (or edits) a single sticky summary comment plus a
# `gitnexus/autofix` Check Run. This job NEVER checks out fork code —
# it only consumes the diff (data) and calls the GitHub API. That
# isolation is what makes it safe to run under `pull-requests: write`
# on fork-triggered events.
#
# The sticky comment is the contributor signal: heading
# "## :sparkles: PR Autofix" in the PR's top-level comments, with a
# fenced `gitnexus-autofix` JSON block carrying machine-readable state
# for AI agents. Contributors apply the patch by commenting `/autofix`
# on the PR — handled by the separate `pr-autofix-apply.yml` workflow.
on:
workflow_run:
workflows: ['PR Autofix']
types: [completed]
concurrency:
# Key on PR identity, NOT workflow_run.id — workflow_run.id is per-run
# unique, which would defeat serialization and let two parallel
# publishes both POST a sticky summary comment. CONTRIBUTING.md
# § GitHub Actions — Concurrency Convention names this anti-pattern
# explicitly. For fork PRs, `pull_requests[]` is empty in the
# workflow_run payload, so we fall back to head-repo + head-branch.
group: ${{ github.workflow }}-${{ github.event.workflow_run.pull_requests[0].number || format('{0}/{1}', github.event.workflow_run.head_repository.full_name, github.event.workflow_run.head_branch) }}
cancel-in-progress: false
permissions: {}
jobs:
publish:
name: publish-autofix
if: >-
github.event.workflow_run.event == 'pull_request'
&& github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pull-requests: write
# Required by actions/download-artifact to fetch artifacts produced
# by a different workflow run.
actions: read
# Required to create the `gitnexus/autofix` Check Run that reports
# the outcome (clean / fixes-available) to the PR's Checks tab.
# Branch protection or agents can grep the conclusion + output
# title without parsing the sticky comment.
checks: write
steps:
# Pinned to v8.0.1. Verify SHA via:
# gh api repos/actions/download-artifact/git/refs/tags/v8.0.1
- name: Download autofix artifact
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: autofix
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: autofix-in
- name: Read and validate metadata
id: meta
shell: bash
run: |
set -euo pipefail
test -f autofix-in/metadata.json
jq . autofix-in/metadata.json
# The artifact comes from the untrusted half running fork code.
# Every field is allowlist-validated before it can flow into
# $GITHUB_OUTPUT. A newline in head_ref would otherwise let a
# malicious branch name inject a second `pr_number=N` line and
# redirect this job's reviewdog suggestions / sticky summary
# comment onto a victim PR under github-actions[bot] with
# pull-requests: write.
assert_field() {
local key="$1" pattern="$2" value
value=$(jq -r ".${key} // empty" autofix-in/metadata.json)
if [ -z "$value" ] || ! [[ "$value" =~ $pattern ]]; then
echo "::error::metadata.${key} failed allowlist (got: $(printf '%q' "$value"))"
exit 1
fi
printf '%s' "$value"
}
SCHEMA=$(assert_field schema '^gitnexus\.pr-autofix/v[0-9]+$')
PR_NUMBER=$(assert_field pr_number '^[0-9]+$')
HEAD_SHA=$(assert_field head_sha '^[0-9a-f]{40}$')
HEAD_REF=$(assert_field head_ref '^[A-Za-z0-9._/-]+$')
HEAD_REPO=$(assert_field head_repo '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$')
BASE_REPO=$(assert_field base_repo '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$')
CHANGED=$(assert_field changed_lines '^[0-9]+$')
# Defence-in-depth: refuse to act if the artifact claims to
# belong to a different repo than the one that triggered us.
if [ "$BASE_REPO" != "${GITHUB_REPOSITORY}" ]; then
echo "::error::Artifact base_repo does not match \$GITHUB_REPOSITORY — refusing to publish."
exit 1
fi
{
echo "schema=${SCHEMA}"
echo "pr_number=${PR_NUMBER}"
echo "head_sha=${HEAD_SHA}"
echo "head_ref=${HEAD_REF}"
echo "head_repo=${HEAD_REPO}"
echo "base_repo=${BASE_REPO}"
echo "changed_lines=${CHANGED}"
} >> "$GITHUB_OUTPUT"
# Cross-verify the artifact's claimed identity against the
# GitHub-controlled workflow_run event. The previous step's
# allowlist only proves the fields are well-formed — not that
# they refer to the PR/SHA that actually triggered this run.
# A fork-controlled `npm run lint:fix` could plausibly mutate
# metadata.json to reference another PR or SHA, redirecting our
# write-scoped sticky/check-run onto an attacker-chosen target.
#
# Authority is workflow_run.head_sha + head_repository.full_name +
# head_branch, resolved via pulls?head={owner}:{branch}. That query
# works for same-repo PRs and forks; commits/{sha}/pulls is empty
# for fork SHAs. The script comes from THIS default-branch checkout
# (the same trust anchor as this workflow file).
#
# Always verify — including the changed_lines=0 path — so the
# check-run SHA cannot be an unverified artifact field.
# Mismatch => fail loud BEFORE any sticky/check-run side effect.
- name: Checkout identity verifier
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: .github/scripts/verify-workflow-run-pr-identity.cjs
sparse-checkout-cone-mode: false
path: trusted
- name: Verify metadata against workflow_run authority
id: verify
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
META_PATH: autofix-in/metadata.json
SCHEMA_PATTERN: '^gitnexus\.pr-autofix/v[0-9]+$'
WF_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
WF_HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
WF_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
shell: bash
run: node trusted/.github/scripts/verify-workflow-run-pr-identity.cjs
- name: Upsert sticky summary comment
# Only post when ci-quality found something fixable (= the
# autofix patch is non-empty). When prettier/eslint are clean
# the patch is zero bytes and the sticky comment is pure noise,
# so we skip it.
if: >-
always()
&& steps.verify.outcome == 'success'
&& steps.meta.outputs.changed_lines != '0'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
PR: ${{ steps.verify.outputs.pr_number }}
CHANGED: ${{ steps.meta.outputs.changed_lines }}
HEAD_SHA: ${{ steps.verify.outputs.head_sha }}
RUN_ID: ${{ github.run_id }}
shell: bash
run: |
set -euo pipefail
# Stable heading + marker — agents grep for these exact strings.
marker="<!-- gitnexus:pr-autofix-summary -->"
heading="## :sparkles: PR Autofix"
# Single state. The /autofix slash command works for any diff
# size — there's no 3K cap and no no-overlap dead-end because
# the apply workflow uses `git apply` + push, not the GitHub
# review-comment API.
ui_state="fixes-available"
prose="Found fixable formatting / unused-import issues across **${CHANGED}** changed lines. **Comment \`/autofix\` on this PR to apply them**, or run \`npm run lint:fix && npm run format\` locally."
# Machine-readable JSON block — agents parse this instead of
# regexing English. Fenced code-block info string is
# `gitnexus-autofix` so agents can locate it without ambiguity.
# Schema bumped from v1 -> v2: adds `apply_command`. The v1
# field set is preserved as a superset, but the `state` enum
# is redefined (v1: suggestions-posted | skipped-too-large |
# diff-no-overlap; v2: fixes-available). v1 readers checking
# `schema == 'gitnexus.pr-autofix/v1'` see an unfamiliar version
# and fall back to prose, which is the intended migration path.
json=$(jq -n -c \
--arg state "${ui_state}" \
--argjson pr_number "${PR}" \
--argjson changed_lines "${CHANGED}" \
--arg head_sha "${HEAD_SHA}" \
--arg run_id "${RUN_ID}" \
'{schema:"gitnexus.pr-autofix/v2", state:$state, pr_number:$pr_number, changed_lines:$changed_lines, head_sha:$head_sha, run_id:$run_id, apply_command:"/autofix"}')
# Multi-line quoted string instead of a column-0 heredoc — YAML's
# `run: |` block ends as soon as a content line dedents below the
# block's first-line indent, which would mis-parse the workflow.
body="${marker}
${heading}
${prose}
\`\`\`gitnexus-autofix
${json}
\`\`\`"
# Strip the leading 10-space indent that the YAML block requires
# so the rendered comment body starts at column 0.
body="$(printf '%s\n' "$body" | sed 's/^ //')"
# Small retry wrapper for transient 5xx / rate-limit responses
# on the GitHub REST API. Three tries with linear backoff. We
# only retry GET (idempotent) and PATCH on a known comment id
# (idempotent). POST is NOT wrapped — retrying a comment-create
# would create duplicates if the first attempt actually landed.
gh_retry() {
local n=0 max=3
while true; do
if gh "$@"; then return 0; fi
n=$((n+1))
if [ "$n" -ge "$max" ]; then return 1; fi
sleep $((n * 2))
done
}
# Find existing bot comment by the marker and edit-in-place; else create.
# CRITICAL: filter by `.user.login == "github-actions[bot]"`. A regular
# user posting a comment containing the marker would otherwise be the
# `head -n1` match; PATCH on someone else's comment 403s, `set -e`
# aborts, and the bot is permanently DoS'd for that PR.
existing=$(gh_retry api "repos/${GH_REPO}/issues/${PR}/comments" \
--paginate --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | contains(\"${marker}\"))) | .id" \
| head -n1 || true)
if [ -n "${existing}" ]; then
gh_retry api -X PATCH "repos/${GH_REPO}/issues/comments/${existing}" \
-f body="${body}" >/dev/null
echo "Updated comment ${existing}."
else
gh api -X POST "repos/${GH_REPO}/issues/${PR}/comments" \
-f body="${body}" >/dev/null
echo "Created summary comment."
fi
- name: Emit gitnexus/autofix Check Run
# Stable check name `gitnexus/autofix` so PR-watching agents can
# `gh pr checks <pr>` and read the conclusion + title without
# parsing the sticky comment. Two outcomes:
# clean → conclusion: success
# fixes-available → conclusion: neutral
# `neutral` does not block branch-protection required-checks but
# is visually distinct from a green pass.
if: always() && steps.verify.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
HEAD_SHA: ${{ steps.verify.outputs.head_sha }}
CHANGED: ${{ steps.meta.outputs.changed_lines }}
shell: bash
run: |
set -euo pipefail
if [ "${CHANGED}" = "0" ]; then
conclusion="success"
title="Formatting clean"
summary="Prettier and ESLint --fix produced no changes."
else
conclusion="neutral"
title="Autofix available — comment /autofix to apply"
summary="Comment \`/autofix\` on this PR to apply formatter + unused-import fixes (works at any diff size). Or run \`npm run lint:fix && npm run format\` locally."
fi
gh api -X POST "repos/${GH_REPO}/check-runs" \
-f name="gitnexus/autofix" \
-f head_sha="${HEAD_SHA}" \
-f status="completed" \
-f conclusion="${conclusion}" \
-f "output[title]=${title}" \
-f "output[summary]=${summary}" \
>/dev/null
echo "Posted check-run gitnexus/autofix=${conclusion} (${title})"