From b34770ed298ee1332a53320cae979b7a2f3095ca Mon Sep 17 00:00:00 2001 From: "lightning-it-shared-assets-sync[bot]" <307342877+lightning-it-shared-assets-sync[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:36:54 +0000 Subject: [PATCH] chore: sync repository quality assets Shared-Assets-Source-SHA: edec7c2d3c4ef73373408fdbb44c3dd5e9112676 Shared-Assets-Source-Run: 32177576480 Shared-Assets-Sync-App-ID: 4351516 --- .github/workflows/copilot-review-refresh.yml | 15 +- .github/workflows/copilot-review.yml | 776 +++++++++++------- .github/workflows/current-revision-rerun.yml | 202 +++++ .../release-bot-exact-head-review.yml | 26 +- .lit/push-ready.json | 10 +- scripts/lit-push-ready.py | 82 +- scripts/materialize-exact-revision-review.py | 546 ++++++++++++ 7 files changed, 1245 insertions(+), 412 deletions(-) create mode 100644 .github/workflows/current-revision-rerun.yml create mode 100644 scripts/materialize-exact-revision-review.py diff --git a/.github/workflows/copilot-review-refresh.yml b/.github/workflows/copilot-review-refresh.yml index 0b19597..2a3a598 100644 --- a/.github/workflows/copilot-review-refresh.yml +++ b/.github/workflows/copilot-review-refresh.yml @@ -27,8 +27,13 @@ jobs: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository && ( - github.actor == 'copilot-pull-request-reviewer' || - github.actor == 'copilot-pull-request-reviewer[bot]' || + ( + contains(fromJSON('["Copilot","copilot-pull-request-reviewer","copilot-pull-request-reviewer[bot]"]'), github.actor) && + ( + github.event.review.user.login == 'copilot-pull-request-reviewer[bot]' || + github.event.comment.user.login == 'copilot-pull-request-reviewer[bot]' + ) + ) || contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.review.author_association) || contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.comment.author_association) ) @@ -40,7 +45,7 @@ jobs: timeout-minutes: 5 steps: - - name: Rerun the canonical pull request gate when needed + - name: Rerun the canonical protected gate when needed env: GH_TOKEN: ${{ github.token }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} @@ -51,7 +56,7 @@ jobs: runs_url="repos/${REPOSITORY}/actions/runs" for attempt in $(seq 1 10); do - response="$(gh api "${runs_url}?event=pull_request&head_sha=${HEAD_SHA}&per_page=100")" + response="$(gh api "${runs_url}?event=pull_request_target&head_sha=${HEAD_SHA}&per_page=100")" run="$( jq -c --arg head_sha "${HEAD_SHA}" --argjson pr "${PR_NUMBER}" ' [ @@ -82,5 +87,5 @@ jobs: sleep 6 done - echo "No canonical pull_request gate found for PR #${PR_NUMBER} at ${HEAD_SHA}." >&2 + echo "No canonical pull_request_target gate found for PR #${PR_NUMBER} at ${HEAD_SHA}." >&2 exit 1 diff --git a/.github/workflows/copilot-review.yml b/.github/workflows/copilot-review.yml index 02462c3..f11ba23 100644 --- a/.github/workflows/copilot-review.yml +++ b/.github/workflows/copilot-review.yml @@ -1,21 +1,11 @@ -# Managed by lightning-it/shared-assets-lit. -# Do not edit downstream copies directly. +# Canonical protected MLX-90/REP-60 current-revision review controller. # yamllint disable rule:truthy rule:line-length --- -name: Copilot review gate +name: Current revision review gate on: - pull_request: - types: - [ - opened, - synchronize, - reopened, - ready_for_review, - labeled, - unlabeled, - edited, - ] + pull_request_target: + types: [opened, ready_for_review] permissions: contents: read @@ -27,56 +17,33 @@ env: UNABLE_REVIEW_MARKER: unable to review this pull request QUOTA_EXHAUSTED_MARKER: quota exhausted QUOTA_EXCEEDED_MARKER: quota exceeded + SUPPRESSED_COMMENTS_MARKER: suppressed comments concurrency: group: copilot-review-${{ github.event.pull_request.number }} cancel-in-progress: true -# The stable main-to-develop sync branch can carry release content, is never -# ancestry-exempt, and uses the base-controlled exact-revision AI path. Only -# dynamically named backmerge/*-main PRs may use the file-identical exemption. +# Release-App pull requests use only the protected MLX-90 ยง7.2 Exact-Revision +# Codex check, except for a zero-diff ancestry merge proven deterministically +# against both protected tips and the develop tree. This workflow retains the +# final-head GitHub Copilot path for applicable human PRs plus the governed +# automation exemptions. jobs: request-current-revision-review: name: Request Copilot review for current revision - # Only the narrow non-breaking Renovate class is exempt. The subsequent - # live-API classifier binds that exemption to Renovate's own label - # history; this event-time filter merely ensures every other Renovate PR - # actually receives the review that the verification job requires. if: >- - github.event_name == 'pull_request' && + github.event_name == 'pull_request_target' && + (github.event.action == 'opened' || + github.event.action == 'ready_for_review') && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository && - !(github.event.pull_request.user.login == 'renovate[bot]' && - github.actor == 'renovate[bot]' && - startsWith(github.event.pull_request.head.ref, 'renovate/') && - github.event.pull_request.base.ref == 'develop' && - contains(github.event.pull_request.labels.*.name, 'safe-automerge') && - !contains(github.event.pull_request.labels.*.name, 'breaking-update')) && - !(github.event.pull_request.user.login == 'lightning-it-shared-assets-sync[bot]' && - startsWith(github.event.pull_request.head.ref, 'chore/sync-shared-assets-lit-') && - github.event.pull_request.base.ref == 'develop' && - (github.event.pull_request.title == 'chore: sync shared-assets-lit' || - github.event.pull_request.title == 'chore: sync shared assets')) && - !(github.event.pull_request.user.login == 'lightning-it-shared-assets-sync[bot]' && - startsWith(github.event.pull_request.head.ref, 'chore/sync-repository-quality-') && - github.event.pull_request.base.ref == 'develop' && - github.event.pull_request.title == 'chore: sync repository quality assets') && - !(github.event.pull_request.user.login == 'lightning-it-release-automation[bot]' && - github.event.pull_request.head.repo.full_name == github.repository && - startsWith(github.event.pull_request.head.ref, 'backmerge/') && - endsWith(github.event.pull_request.head.ref, '-main') && - github.event.pull_request.base.ref == 'develop' && - startsWith( - github.event.pull_request.title, - 'chore(governance): record main ancestry before ' - )) && - !(github.event.pull_request.user.login == 'lightning-it-release-automation[bot]' && - github.event.pull_request.head.repo.full_name == github.repository && - github.event.pull_request.head.ref == 'develop' && - github.event.pull_request.base.ref == 'main' && - github.event.pull_request.title == 'chore(release): promote develop to main') + github.event.pull_request.user.login == 'litroc' && + github.actor == 'litroc' && + github.triggering_actor == 'litroc' permissions: + actions: read contents: read + issues: write pull-requests: write runs-on: ubuntu-latest timeout-minutes: 5 @@ -86,12 +53,91 @@ jobs: env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} + EXPECTED_BASE: ${{ github.event.pull_request.base.sha }} + EXPECTED_HEAD: ${{ github.event.pull_request.head.sha }} + EXPECTED_HEAD_REF: ${{ github.event.pull_request.head.ref }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} REPOSITORY: ${{ github.repository }} + TRUSTED_WORKFLOW_SHA: ${{ github.workflow_sha }} + TRUSTED_WORKFLOW_REF: ${{ github.workflow_ref }} run: | set -euo pipefail + [[ "${EXPECTED_BASE}" =~ ^[0-9a-f]{40}$ ]] + [[ "${EXPECTED_HEAD}" =~ ^[0-9a-f]{40}$ ]] + [[ "${TRUSTED_WORKFLOW_SHA}" =~ ^[0-9a-f]{40}$ ]] + test "${DEFAULT_BRANCH}" = develop + test "${TRUSTED_WORKFLOW_REF}" = \ + "${REPOSITORY}/.github/workflows/copilot-review.yml@refs/heads/${DEFAULT_BRANCH}" + default_head="$(gh api "repos/${REPOSITORY}/branches/${DEFAULT_BRANCH}" --jq .commit.sha)" + controller_ancestry="$(gh api \ + "repos/${REPOSITORY}/compare/${TRUSTED_WORKFLOW_SHA}...${default_head}")" + jq -e \ + --arg controller "${TRUSTED_WORKFLOW_SHA}" ' + .status == "identical" + or (.status == "ahead" and .behind_by == 0 + and .merge_base_commit.sha == $controller) + ' <<<"${controller_ancestry}" >/dev/null + protected_run="$(gh api "repos/${REPOSITORY}/actions/runs/${GITHUB_RUN_ID}")" + jq -e \ + --arg branch "${EXPECTED_HEAD_REF}" \ + --arg repository "${REPOSITORY}" \ + --arg sha "${EXPECTED_HEAD}" ' + .event == "pull_request_target" + and .name == "Current revision review gate" + and .path == ".github/workflows/copilot-review.yml" + and .head_branch == $branch + and .head_sha == $sha + and .repository.full_name == $repository + and .head_repository.full_name == $repository + ' <<<"${protected_run}" >/dev/null + pr="$(gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}")" + test "$(jq -r .state <<<"${pr}")" = open + test "$(jq -r .draft <<<"${pr}")" = false + base_ref="$(jq -er '.base.ref | select(. == "develop" or . == "main")' <<<"${pr}")" + test -n "${base_ref}" + test "$(jq -r .base.sha <<<"${pr}")" = "${EXPECTED_BASE}" + test "$(jq -r .base.repo.full_name <<<"${pr}")" = "${REPOSITORY}" + test "$(jq -r .head.sha <<<"${pr}")" = "${EXPECTED_HEAD}" + test "$(jq -r .head.repo.full_name <<<"${pr}")" = "${REPOSITORY}" + author="$(jq -r .user.login <<<"${pr}")" + if [ "${author}" != litroc ]; then + echo "Contributor-funded review required; Lightning IT does not request or fund it." + exit 0 + fi reviewer_login="${COPILOT_REVIEWER_LOGIN%\[bot\]}" reviewer="${reviewer_login}[bot]" requested_reviewers_url="repos/${REPOSITORY}/pulls/${PR_NUMBER}/requested_reviewers" + marker="" + review_exists_for_head() { + local reviews + reviews="$(gh api --paginate --slurp \ + "repos/${REPOSITORY}/pulls/${PR_NUMBER}/reviews?per_page=100")" + jq -e \ + --arg reviewer "${reviewer}" \ + --arg head "${EXPECTED_HEAD}" \ + --arg retry_unable "${UNABLE_REVIEW_MARKER}" \ + --arg retry_no_files "${NO_FILES_REVIEW_MARKER}" \ + --arg retry_quota_exhausted "${QUOTA_EXHAUSTED_MARKER}" \ + --arg retry_quota_exceeded "${QUOTA_EXCEEDED_MARKER}" \ + --arg retry_suppressed "${SUPPRESSED_COMMENTS_MARKER}" ' + any(add[]; + .user.login == $reviewer + and .commit_id == $head + and (((.body // "") | ascii_downcase) as $body + | ($body | contains($retry_unable) | not) + and ($body | contains($retry_no_files) | not) + and ($body | contains("able to review any files") | not) + and ($body | contains($retry_quota_exhausted) | not) + and ($body | contains($retry_quota_exceeded) | not) + and ($body | contains($retry_suppressed) | not) + and ($body | contains("encountered an error") | not)) + ) + ' <<<"${reviews}" >/dev/null + } + if review_exists_for_head; then + echo "Copilot already reviewed the exact finalized head." + exit 0 + fi reviewer_is_requested() { local response if ! response="$(gh api "${requested_reviewers_url}")"; then @@ -100,32 +146,74 @@ jobs: jq -e --arg reviewer "${reviewer}" \ 'any(.users[]?; .login == $reviewer)' <<<"${response}" >/dev/null } + wait_for_pending_reviewer() { + local attempt request_status + for attempt in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18; do + if reviewer_is_requested; then + if [ "${attempt}" -eq 18 ]; then + echo "Timed out waiting for the previous revision review to finish." >&2 + return 1 + fi + sleep 10 + continue + fi + request_status=$? + if [ "${request_status}" -ne 1 ]; then + return "${request_status}" + fi + return 0 + done + } + comments="$(gh api --paginate --slurp "repos/${REPOSITORY}/issues/${PR_NUMBER}/comments?per_page=100")" + marker_exists=false + if jq -e --arg marker "${marker}" 'any(add[]; .body | contains($marker))' \ + <<<"${comments}" >/dev/null; then + marker_exists=true + fi if reviewer_is_requested; then - echo "Copilot review is already requested for the current PR." + if [ "${marker_exists}" = true ]; then + echo "Copilot review is already pending for the exact finalized head." + exit 0 + fi + echo "A previous revision review is still pending; waiting before requesting the exact finalized head." + wait_for_pending_reviewer else request_status=$? if [ "${request_status}" -ne 1 ]; then exit "${request_status}" fi - if ! gh api --method POST "${requested_reviewers_url}" \ - -f "reviewers[]=${reviewer}" - then - # A concurrent workflow can win the request race. Accept only - # that verified idempotent outcome; fail on absence/API errors. - if reviewer_is_requested; then - echo "A concurrent workflow already requested Copilot review." - else - verification_status=$? - exit "${verification_status}" - fi - fi + fi + if [ "${marker_exists}" = true ]; then + echo "Recorded request no longer has a pending or completed exact-head review; requesting again." + fi + gh api --method POST "${requested_reviewers_url}" -f "reviewers[]=${reviewer}" + if [ "${marker_exists}" = false ]; then + gh api --method POST "repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" \ + -f body="Copilot review request accepted for finalized head ${EXPECTED_HEAD}." >/dev/null fi - current-revision-reviewed: - name: Successful Copilot review - if: github.event.pull_request.draft == false + verify-current-revision-policy: + name: Verify current revision policy + if: >- + github.event_name == 'pull_request_target' && + github.event.pull_request.draft == false && + github.event.pull_request.head.repo.full_name == github.repository && + (((github.event.action == 'opened' || + github.event.action == 'ready_for_review') && + github.event.pull_request.user.login != 'lightning-it-release-automation[bot]') || + ((github.event.action == 'opened' || + github.event.action == 'ready_for_review') && + github.event.pull_request.user.login == 'lightning-it-release-automation[bot]' && + github.event.pull_request.base.ref == 'develop' && + startsWith(github.event.pull_request.head.ref, 'backmerge/') && + endsWith(github.event.pull_request.head.ref, '-main') && + startsWith( + github.event.pull_request.title, + 'chore(governance): record main ancestry before ' + ))) permissions: actions: read + checks: write contents: read issues: read pull-requests: read @@ -150,7 +238,6 @@ jobs: set -euo pipefail trusted=false trusted_kind=none - release_tag="" live_pr="{}" policy_label_events="[]" if [ "${PR_AUTHOR}" = "renovate[bot]" ] \ @@ -267,172 +354,27 @@ jobs: && [ "${PR_HEAD_REPO}" = "${REPOSITORY}" ] \ && [[ "${PR_HEAD}" == backmerge/*-main ]] \ && [ "${PR_BASE}" = "develop" ] \ - && [[ "${PR_TITLE}" == "chore(governance): record main ancestry before "* ]]; then + && [[ "${PR_TITLE}" == "chore(governance): record main ancestry before "* ]] \ + && live_pr="$(gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}")" \ + && jq -e \ + --arg author "${PR_AUTHOR}" --arg base "${PR_BASE}" \ + --arg head "${PR_HEAD}" --arg head_sha "${PR_HEAD_SHA}" --arg title "${PR_TITLE}" \ + --arg repo "${REPOSITORY}" \ + '(.state == "open") and (.draft == false) + and (.user.login == $author) and (.base.ref == $base) + and (.head.ref == $head) and (.head.sha == $head_sha) and (.title == $title) + and (.head.repo.full_name == $repo)' \ + <<<"${live_pr}" >/dev/null; then trusted=true trusted_kind=ancestry-backmerge - elif [ "${PR_AUTHOR}" = "lightning-it-release-automation[bot]" ] \ - && [ "${PR_HEAD_REPO}" = "${REPOSITORY}" ] \ - && [[ "${PR_HEAD}" == release/v* ]] \ - && [ "${PR_BASE}" = "main" ] \ - && gh api "repos/${REPOSITORY}/contents/galaxy.yml?ref=main" >/dev/null \ - && gh api "repos/${REPOSITORY}/contents/changelogs/config.yaml?ref=main" >/dev/null; then - release_tag="${PR_HEAD#release/}" - if [[ "${release_tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] \ - && [ "${PR_TITLE}" = "Release ${release_tag}" ]; then - trusted=true - trusted_kind=release-preparation - fi - elif [ "${PR_AUTHOR}" = "lightning-it-release-automation[bot]" ] \ - && [ "${PR_HEAD_REPO}" = "${REPOSITORY}" ] \ - && [[ "${PR_HEAD}" == backsync/release-v*-to-develop ]] \ - && [ "${PR_BASE}" = "develop" ]; then - release_tag="${PR_HEAD#backsync/release-}" - release_tag="${release_tag%-to-develop}" - if [[ "${release_tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] \ - && [ "${PR_TITLE}" = "chore: sync ${release_tag} release back to develop" ]; then - trusted=true - trusted_kind=release-backsync - fi - elif [ "${PR_AUTHOR}" = "lightning-it-release-automation[bot]" ] \ - && [ "${PR_HEAD_REPO}" = "${REPOSITORY}" ] \ - && [ "${PR_HEAD}" = "develop" ] \ - && [ "${PR_BASE}" = "main" ] \ - && [ "${PR_TITLE}" = "chore(release): promote develop to main" ]; then - trusted=true - trusted_kind=release-promotion fi { echo "trusted=${trusted}" echo "kind=${trusted_kind}" - echo "release_tag=${release_tag}" } >>"${GITHUB_OUTPUT}" - - name: Verify file-identical ancestry backmerge - if: steps.trusted-automation.outputs.kind == 'ancestry-backmerge' - env: - GH_TOKEN: ${{ github.token }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - set -euo pipefail - gh auth setup-git - git init --quiet . - git remote add origin "https://github.com/${GITHUB_REPOSITORY}.git" - git fetch --quiet --no-tags origin \ - "${HEAD_SHA}" "refs/heads/main" "refs/heads/develop" - [ "$(git rev-list --parents -n 1 "${HEAD_SHA}" | wc -w)" -eq 3 ] - [ "$(git rev-parse "${HEAD_SHA}^1")" = "$(git rev-parse origin/develop)" ] - [ "$(git rev-parse "${HEAD_SHA}^2")" = "$(git rev-parse origin/main)" ] - git diff --quiet "origin/develop" "${HEAD_SHA}" - echo "Verified an exact develop-tree-preserving merge of current develop and main tips." - - - name: Verify protected release promotion - if: steps.trusted-automation.outputs.kind == 'release-promotion' - env: - GH_TOKEN: ${{ github.token }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - set -euo pipefail - gh auth setup-git - git init --quiet . - git remote add origin "https://github.com/${GITHUB_REPOSITORY}.git" - git fetch --quiet --no-tags origin \ - "${HEAD_SHA}" "refs/heads/main" "refs/heads/develop" - [ "$(git rev-parse origin/develop)" = "${HEAD_SHA}" ] - git merge-base --is-ancestor "origin/main" "${HEAD_SHA}" - echo "Verified current protected develop head and main ancestry." - - - name: Verify release preparation commit and bounded files - if: steps.trusted-automation.outputs.kind == 'release-preparation' - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - GH_TOKEN: ${{ github.token }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - RELEASE_TAG: ${{ steps.trusted-automation.outputs.release_tag }} - run: | - set -euo pipefail - gh auth setup-git - git init --quiet . - git remote add origin "https://github.com/${GITHUB_REPOSITORY}.git" - git fetch --quiet --no-tags origin \ - "${HEAD_SHA}" "${BASE_SHA}" "refs/heads/main" - [ "$(git rev-parse origin/main)" = "${BASE_SHA}" ] - [ "$(git rev-list --parents -n 1 "${HEAD_SHA}" | wc -w)" -eq 2 ] - [ "$(git rev-parse "${HEAD_SHA}^1")" = "${BASE_SHA}" ] - [ "$(git log -1 --format=%s "${HEAD_SHA}")" = \ - "chore(release): prepare ${RELEASE_TAG}" ] - if git diff --quiet "${BASE_SHA}" "${HEAD_SHA}"; then - echo "Release preparation does not contain generated release changes." >&2 - exit 1 - fi - unexpected="$( - git diff --name-only "${BASE_SHA}" "${HEAD_SHA}" | - awk '$0 != "CHANGELOG.rst" && $0 != "galaxy.yml" && $0 !~ /^changelogs\//' - )" - [ -z "${unexpected}" ] || { - echo "Release preparation contains files outside the allowed release paths:" >&2 - printf '%s\n' "${unexpected}" >&2 - exit 1 - } - version="${RELEASE_TAG#v}" - [ "$(git show "${HEAD_SHA}:galaxy.yml" | awk '$1 == "version:" {print $2; exit}')" = "${version}" ] - preparation="$(git show "${HEAD_SHA}:changelogs/release-preparation.json")" - jq -e \ - --arg base "${BASE_SHA}" \ - --arg repository "${GITHUB_REPOSITORY}" \ - --arg version "${version}" \ - '.schema_version == 1 - and .base_sha == $base - and .next_version == $version - and .repository == $repository - and .preparer.login == "lightning-it-release-automation[bot]" - and .workflow.source_sha == $base' \ - <<<"${preparation}" >/dev/null - echo "Verified single-parent release preparation and bounded generated files." - - - name: Verify release back-sync merge and bounded files - if: steps.trusted-automation.outputs.kind == 'release-backsync' - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - GH_TOKEN: ${{ github.token }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - RELEASE_TAG: ${{ steps.trusted-automation.outputs.release_tag }} - run: | - set -euo pipefail - gh auth setup-git - git init --quiet . - git remote add origin "https://github.com/${GITHUB_REPOSITORY}.git" - git fetch --quiet --no-tags origin \ - "${HEAD_SHA}" "${BASE_SHA}" "refs/heads/main" "refs/heads/develop" \ - "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}" - [ "$(git rev-parse origin/develop)" = "${BASE_SHA}" ] - [ "$(git rev-list --parents -n 1 "${HEAD_SHA}" | wc -w)" -eq 3 ] - [ "$(git rev-parse "${HEAD_SHA}^1")" = "${BASE_SHA}" ] - release_sha="$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")" - main_sha="$(git rev-parse origin/main)" - [ "$(git rev-parse "${HEAD_SHA}^2")" = "${main_sha}" ] - git merge-base --is-ancestor "${release_sha}" "${main_sha}" - unexpected="$( - git diff --name-only "${BASE_SHA}" "${HEAD_SHA}" | - awk '$0 != "CHANGELOG.rst" && $0 != "galaxy.yml" && $0 !~ /^changelogs\//' - )" - [ -z "${unexpected}" ] || { - echo "Release back-sync contains files outside the allowed release paths:" >&2 - printf '%s\n' "${unexpected}" >&2 - exit 1 - } - if git diff --quiet "${BASE_SHA}" "${HEAD_SHA}"; then - echo "Verified ancestry-only current-main back-sync; release files already match." - else - echo "Verified current-main merge, release ancestry, and bounded release-generated files." - fi - - name: Accept trusted automation exemption - if: >- - steps.trusted-automation.outputs.trusted == 'true' && - steps.trusted-automation.outputs.kind != 'ancestry-backmerge' && - steps.trusted-automation.outputs.kind != 'release-preparation' && - steps.trusted-automation.outputs.kind != 'release-promotion' && - steps.trusted-automation.outputs.kind != 'release-backsync' + if: steps.trusted-automation.outputs.trusted == 'true' run: | if [ "${{ steps.trusted-automation.outputs.kind }}" = renovate ]; then { @@ -448,10 +390,64 @@ jobs: echo "The canonical source change was reviewed before distribution." echo "Exact App identity, source SHA/run provenance, and target gates are enforced by the required guarded-automerge policy." } >>"${GITHUB_STEP_SUMMARY}" + elif [ "${{ steps.trusted-automation.outputs.kind }}" = ancestry-backmerge ]; then + echo "The deterministic ancestry exemption is verified against both live branch tips and the single bound ancestry-evidence file." else echo "Trusted automation PR; Copilot review is delegated to its required guarded-automerge policy." fi + - name: Verify evidence-bound ancestry backmerge + if: steps.trusted-automation.outputs.kind == 'ancestry-backmerge' + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + head_commit="$(gh api "repos/${REPOSITORY}/git/commits/${HEAD_SHA}")" + current_develop="$(gh api "repos/${REPOSITORY}/branches/develop" --jq .commit.sha)" + current_main="$(gh api "repos/${REPOSITORY}/branches/main" --jq .commit.sha)" + jq -e \ + --arg develop "${current_develop}" \ + --arg main "${current_main}" \ + '(.parents | length) == 2 + and .parents[0].sha == $develop + and .parents[1].sha == $main' \ + <<<"${head_commit}" >/dev/null + test "${BASE_SHA}" = "${current_develop}" + commit_response="$(gh api "repos/${REPOSITORY}/commits/${HEAD_SHA}")" + jq -e \ + --arg base "${BASE_SHA}" \ + --arg head "${HEAD_SHA}" \ + --arg main "${current_main}" ' + .sha == $head + and (.parents | length) == 2 + and .parents[0].sha == $base + and .parents[1].sha == $main + and (.files | length) == 1 + and .files[0].filename == ".lit/main-ancestry.json" + and (.files[0].status == "added" or .files[0].status == "modified") + and (.files[0].previous_filename == null) + ' <<<"${commit_response}" >/dev/null + evidence_response="$(gh api \ + "repos/${REPOSITORY}/contents/.lit/main-ancestry.json?ref=${HEAD_SHA}")" + jq -e '.type == "file" and .encoding == "base64" and (.size > 0)' \ + <<<"${evidence_response}" >/dev/null + evidence_json="$(jq -r .content <<<"${evidence_response}" | tr -d '\n' | base64 --decode)" + jq -e \ + --arg repository "${REPOSITORY}" \ + --arg main "${current_main}" \ + --arg develop "${current_develop}" ' + (keys | sort) == ["develop_parent_sha", "main_sha", "purpose", "repository", "schema_version"] + and .schema_version == 1 + and .repository == $repository + and .main_sha == $main + and .develop_parent_sha == $develop + and .purpose == "Bind the reviewed main ancestry backmerge." + ' <<<"${evidence_json}" >/dev/null + echo "Verified an exact evidence-bound merge of current develop and main tips." + - name: Verify current Copilot review and resolved findings if: >- steps.trusted-automation.outputs.trusted != 'true' && @@ -558,6 +554,7 @@ jobs: --arg no_files_marker "${NO_FILES_REVIEW_MARKER}" \ --arg quota_exhausted_marker "${QUOTA_EXHAUSTED_MARKER}" \ --arg quota_exceeded_marker "${QUOTA_EXCEEDED_MARKER}" \ + --arg suppressed_comments_marker "${SUPPRESSED_COMMENTS_MARKER}" \ 'def normalize_review_text: ascii_downcase | gsub("wasn[\u0027\u2019]t"; "was not") @@ -569,6 +566,7 @@ jobs: | ($no_files_marker | normalize_review_text) as $no_files | ($quota_exhausted_marker | normalize_review_text) as $quota_exhausted | ($quota_exceeded_marker | normalize_review_text) as $quota_exceeded + | ($suppressed_comments_marker | normalize_review_text) as $suppressed | [ ([ review_content @@ -579,6 +577,7 @@ jobs: or contains($no_files) or contains($quota_exhausted) or contains($quota_exceeded) + or contains($suppressed) ) ) ] | length), @@ -831,105 +830,246 @@ jobs: echo "GitHub Copilot reviewed current head ${head_sha}; no unresolved Copilot findings remain." - - name: Verify base-controlled exact-head Codex review - if: >- - steps.trusted-automation.outputs.trusted != 'true' && - github.event.pull_request.user.login == 'lightning-it-release-automation[bot]' + - name: Publish bound neutral result env: - EXPECTED_BASE: ${{ github.event.pull_request.base.sha }} - EXPECTED_HEAD: ${{ github.event.pull_request.head.sha }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} - PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + GH_TOKEN: ${{ github.token }} + EVENT_HEAD: ${{ github.event.pull_request.head.sha }} + EVENT_HEAD_REF: ${{ github.event.pull_request.head.ref }} + EVENT_BASE: ${{ github.event.pull_request.base.sha }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} PR_NUMBER: ${{ github.event.pull_request.number }} REPOSITORY: ${{ github.repository }} - GH_TOKEN: ${{ github.token }} + TRUSTED_KIND: ${{ steps.trusted-automation.outputs.kind }} + TRUSTED_WORKFLOW_SHA: ${{ github.workflow_sha }} + TRUSTED_WORKFLOW_REF: ${{ github.workflow_ref }} run: | set -euo pipefail - expected_run_name="Base-controlled exact-head PR #${PR_NUMBER} ${EXPECTED_BASE}..${EXPECTED_HEAD}" - live_pr_matches() { - local live_pr - live_pr="$(gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}")" || return 1 + [[ "${EVENT_BASE}" =~ ^[0-9a-f]{40}$ ]] + [[ "${EVENT_HEAD}" =~ ^[0-9a-f]{40}$ ]] + [[ "${TRUSTED_WORKFLOW_SHA}" =~ ^[0-9a-f]{40}$ ]] + test "${DEFAULT_BRANCH}" = develop + test "${TRUSTED_WORKFLOW_REF}" = \ + "${REPOSITORY}/.github/workflows/copilot-review.yml@refs/heads/${DEFAULT_BRANCH}" + default_head="$(gh api "repos/${REPOSITORY}/branches/${DEFAULT_BRANCH}" --jq .commit.sha)" + controller_ancestry="$(gh api \ + "repos/${REPOSITORY}/compare/${TRUSTED_WORKFLOW_SHA}...${default_head}")" + jq -e \ + --arg controller "${TRUSTED_WORKFLOW_SHA}" ' + .status == "identical" + or (.status == "ahead" and .behind_by == 0 + and .merge_base_commit.sha == $controller) + ' <<<"${controller_ancestry}" >/dev/null + protected_run="$(gh api "repos/${REPOSITORY}/actions/runs/${GITHUB_RUN_ID}")" + jq -e \ + --arg branch "${EVENT_HEAD_REF}" \ + --arg repository "${REPOSITORY}" \ + --arg sha "${EVENT_HEAD}" ' + .event == "pull_request_target" + and .name == "Current revision review gate" + and .path == ".github/workflows/copilot-review.yml" + and .head_branch == $branch + and .head_sha == $sha + and .repository.full_name == $repository + and .head_repository.full_name == $repository + ' <<<"${protected_run}" >/dev/null + pr="$(gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}")" + test "$(jq -r .state <<<"${pr}")" = open + test "$(jq -r .draft <<<"${pr}")" = false + test "$(jq -r .head.sha <<<"${pr}")" = "${EVENT_HEAD}" + test "$(jq -r .base.sha <<<"${pr}")" = "${EVENT_BASE}" + base_ref="$(jq -er '.base.ref | select(. == "develop" or . == "main")' <<<"${pr}")" + test -n "${base_ref}" + test "$(jq -r .base.repo.full_name <<<"${pr}")" = "${REPOSITORY}" + author="$(jq -r .user.login <<<"${pr}")" + review_path="applicable Copilot or governed automation exemption" + external_kind="copilot" + result_title="Current revision review passed" + if [ "${author}" = 'lightning-it-release-automation[bot]' ]; then + test "${TRUSTED_KIND}" = ancestry-backmerge + test "${base_ref}" = develop + [[ "${EVENT_HEAD_REF}" == backmerge/*-main ]] + [[ "$(jq -r .title <<<"${pr}")" == \ + "chore(governance): record main ancestry before "* ]] + head_commit="$(gh api "repos/${REPOSITORY}/git/commits/${EVENT_HEAD}")" + current_main="$(gh api "repos/${REPOSITORY}/branches/main" --jq .commit.sha)" + test "${default_head}" = "${EVENT_BASE}" + jq -e \ + --arg develop "${default_head}" \ + --arg main "${current_main}" \ + '(.parents | length) == 2 + and .parents[0].sha == $develop + and .parents[1].sha == $main' \ + <<<"${head_commit}" >/dev/null + commit_response="$(gh api "repos/${REPOSITORY}/commits/${EVENT_HEAD}")" + jq -e \ + --arg base "${EVENT_BASE}" \ + --arg head "${EVENT_HEAD}" \ + --arg main "${current_main}" ' + .sha == $head + and (.parents | length) == 2 + and .parents[0].sha == $base + and .parents[1].sha == $main + and (.files | length) == 1 + and .files[0].filename == ".lit/main-ancestry.json" + and (.files[0].status == "added" or .files[0].status == "modified") + and (.files[0].previous_filename == null) + ' <<<"${commit_response}" >/dev/null + evidence_response="$(gh api \ + "repos/${REPOSITORY}/contents/.lit/main-ancestry.json?ref=${EVENT_HEAD}")" + jq -e '.type == "file" and .encoding == "base64" and (.size > 0)' \ + <<<"${evidence_response}" >/dev/null + evidence_json="$(jq -r .content <<<"${evidence_response}" | tr -d '\n' | base64 --decode)" jq -e \ - --arg author "${PR_AUTHOR}" \ - --arg base_ref "${PR_BASE_REF}" \ - --arg base_sha "${EXPECTED_BASE}" \ - --arg head_sha "${EXPECTED_HEAD}" \ --arg repository "${REPOSITORY}" \ - '(.state == "open") and (.draft == false) - and (.user.login == $author) and (.user.type == "Bot") - and (.base.ref == $base_ref) and (.base.sha == $base_sha) - and (.head.sha == $head_sha) - and (.base.repo.full_name == $repository) - and (.head.repo.full_name == $repository)' \ - <<<"${live_pr}" >/dev/null - } - live_pr_matches - - for attempt in $(seq 1 100); do - response="$( - gh api --method GET \ - "repos/${REPOSITORY}/actions/workflows/release-bot-exact-head-review.yml/runs" \ - -f event=pull_request_target \ - -f per_page=100 - )" - trusted_run="$( - jq -c \ - --arg display_title "${expected_run_name}" \ - --arg workflow_name "Base-controlled release bot exact-head review" \ - '[.workflow_runs[]? - | select( - .event == "pull_request_target" - and .name == $workflow_name - and .path == ".github/workflows/release-bot-exact-head-review.yml" - and .display_title == $display_title - )] - | sort_by(.id) - | last // {}' <<<"${response}" - )" - status="$(jq -r '.status // "missing"' <<<"${trusted_run}")" - conclusion="$(jq -r '.conclusion // "pending"' <<<"${trusted_run}")" - run_url="$(jq -r '.html_url // "unavailable"' <<<"${trusted_run}")" - if [ "${status}" = completed ]; then - live_pr_matches - if [ "${conclusion}" = success ]; then - run_id="$(jq -r '.id // ""' <<<"${trusted_run}")" - [[ "${run_id}" =~ ^[0-9]+$ ]] || { - echo "Trusted workflow run returned an invalid run id." >&2 - exit 1 - } - jobs_response="$( - gh api --method GET \ - "repos/${REPOSITORY}/actions/runs/${run_id}/jobs" \ - -f filter=latest \ - -f per_page=100 - )" - review_job="$( - jq -c \ - --arg job_name "Base-controlled exact-head Codex review" \ - '[.jobs[]? | select(.name == $job_name)] - | sort_by(.id) - | last // {}' <<<"${jobs_response}" - )" - if jq -e \ - '(.status == "completed") and (.conclusion == "success")' \ - <<<"${review_job}" >/dev/null; then - echo "Trusted exact-head review job passed for ${EXPECTED_HEAD}: ${run_url}" - exit 0 - fi - review_conclusion="$(jq -r '.conclusion // "missing"' <<<"${review_job}")" - echo "Trusted workflow completed without a successful exact-head review job (${review_conclusion}): ${run_url}" >&2 - exit 1 - fi - if [ "${conclusion}" != cancelled ]; then - echo "Trusted base-controlled run concluded ${conclusion}: ${run_url}" >&2 - exit 1 - fi - echo "A matching run was cancelled; waiting for its exact-revision replacement." - fi - if [ "${attempt}" -eq 100 ]; then - echo "No successful trusted workflow run arrived for ${EXPECTED_HEAD}." >&2 + --arg main "${current_main}" \ + --arg develop "${default_head}" ' + (keys | sort) == ["develop_parent_sha", "main_sha", "purpose", "repository", "schema_version"] + and .schema_version == 1 + and .repository == $repository + and .main_sha == $main + and .develop_parent_sha == $develop + and .purpose == "Bind the reviewed main ancestry backmerge." + ' <<<"${evidence_json}" >/dev/null + review_path="deterministic evidence-bound ancestry exemption" + external_kind="ancestry-backmerge" + result_title="Current revision deterministic exemption passed" + else + test "${TRUSTED_KIND}" != ancestry-backmerge + fi + run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + evidence="$(jq -cn \ + --arg base "${EVENT_BASE}" \ + --arg controller "${TRUSTED_WORKFLOW_SHA}" \ + --arg head "${EVENT_HEAD}" \ + --arg review_path "${review_path}" \ + --argjson run_id "${GITHUB_RUN_ID}" \ + --arg run_url "${run_url}" \ + '{schema:4,base_sha:$base,head_sha:$head,controller_sha:$controller, + producer_run_id:$run_id, + review_path:$review_path,run_url:$run_url}')" + publish_once() { + local check_name="$1" external_id="$2" title="$3" + local checks named count check_id check_url created current_external_id updated + checks="$(gh api --paginate --slurp \ + "repos/${REPOSITORY}/commits/${EVENT_HEAD}/check-runs?check_name=$(jq -rn --arg value "${check_name}" '$value|@uri')&per_page=100")" + named="$(jq -c \ + --arg name "${check_name}" \ + '[.[].check_runs[]? | + select(.name == $name) | + select(.app.id == 15368 and .app.slug == "github-actions")]' \ + <<<"${checks}")" + count="$(jq 'length' <<<"${named}")" + if [ "${count}" -gt 1 ]; then + echo "Multiple protected ${check_name} results exist for ${EVENT_HEAD}." >&2 exit 1 fi - echo "Waiting for trusted base-controlled workflow (attempt ${attempt}/100)." - sleep 15 - done + if [ "${count}" -eq 1 ]; then + check_id="$(jq -er '.[0].id | select(type == "number" and . > 0)' <<<"${named}")" + check_url="${GITHUB_SERVER_URL}/${REPOSITORY}/runs/${check_id}" + current_external_id="$(jq -r '.[0].external_id // empty' <<<"${named}")" + if [ "${current_external_id}" != "${external_id}" ]; then + updated="$(gh api --method PATCH "repos/${REPOSITORY}/check-runs/${check_id}" \ + -f status=completed \ + -f conclusion=success \ + -f "details_url=${check_url}" \ + -f "external_id=${external_id}" \ + -f "output[title]=${title}" \ + -f "output[summary]=${evidence}")" + jq -e \ + --arg check_name "${check_name}" \ + --arg evidence "${evidence}" \ + --arg external_id "${external_id}" \ + --arg head "${EVENT_HEAD}" \ + --arg url "${check_url}" \ + --argjson check_id "${check_id}" ' + .id == $check_id + and .name == $check_name + and .app.id == 15368 + and .app.slug == "github-actions" + and .head_sha == $head + and .details_url == $url + and .external_id == $external_id + and .status == "completed" + and .conclusion == "success" + and .output.summary == $evidence + ' <<<"${updated}" >/dev/null + return + fi + jq -e \ + --arg evidence "${evidence}" \ + --arg external_id "${external_id}" \ + --arg head "${EVENT_HEAD}" \ + --arg url "${check_url}" \ + --argjson check_id "${check_id}" ' + .[0].id == $check_id + and .[0].app.id == 15368 + and .[0].app.slug == "github-actions" + and .[0].head_sha == $head + and .[0].details_url == $url + and .[0].external_id == $external_id + and .[0].status == "completed" + and .[0].conclusion == "success" + and .[0].output.summary == $evidence + ' <<<"${named}" >/dev/null + return + fi + created="$(gh api --method POST "repos/${REPOSITORY}/check-runs" \ + -f name="${check_name}" \ + -f head_sha="${EVENT_HEAD}" \ + -f status=completed \ + -f conclusion=success \ + -f external_id="${external_id}" \ + -f "output[title]=${title}" \ + -f "output[summary]=${evidence}")" + check_id="$(jq -er '.id | select(type == "number" and . > 0)' <<<"${created}")" + check_url="${GITHUB_SERVER_URL}/${REPOSITORY}/runs/${check_id}" + created="$(gh api --method PATCH "repos/${REPOSITORY}/check-runs/${check_id}" \ + -f "details_url=${check_url}")" + jq -e \ + --arg evidence "${evidence}" \ + --arg external_id "${external_id}" \ + --arg head "${EVENT_HEAD}" \ + --arg url "${check_url}" \ + --argjson check_id "${check_id}" ' + .id == $check_id + and .app.id == 15368 + and .app.slug == "github-actions" + and .head_sha == $head + and .details_url == $url + and .external_id == $external_id + and .status == "completed" + and .conclusion == "success" + and .output.summary == $evidence + ' <<<"${created}" >/dev/null + } + publish_once \ + 'Current revision review' \ + "mlx90-current-revision:${external_kind}:v5:${GITHUB_RUN_ID}:${EVENT_BASE}:${EVENT_HEAD}" \ + "${result_title}" + + request-protected-verifier-reevaluation: + name: Request protected verifier re-evaluation + needs: verify-current-revision-policy + if: needs.verify-current-revision-policy.result == 'success' + permissions: + actions: write + contents: read + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Dispatch the protected re-evaluation helper from develop + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPOSITORY: ${{ github.repository }} + EXPECTED_BASE: ${{ github.event.pull_request.base.sha }} + EXPECTED_HEAD: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + gh api --method POST \ + "repos/${REPOSITORY}/actions/workflows/current-revision-rerun.yml/dispatches" \ + -f ref=develop \ + -f "inputs[pr_number]=${PR_NUMBER}" \ + -f "inputs[expected_base]=${EXPECTED_BASE}" \ + -f "inputs[expected_head]=${EXPECTED_HEAD}" >/dev/null diff --git a/.github/workflows/current-revision-rerun.yml b/.github/workflows/current-revision-rerun.yml new file mode 100644 index 0000000..74de134 --- /dev/null +++ b/.github/workflows/current-revision-rerun.yml @@ -0,0 +1,202 @@ +# Protected helper: it can rerun only the single verifier reservation bound to +# the exact live PR head, and only once after a neutral PASS exists. +# yamllint disable rule:truthy rule:line-length +--- +name: Re-evaluate protected current-revision evidence + +on: + workflow_dispatch: + inputs: + pr_number: + description: Pull request whose protected verifier must be re-evaluated + required: true + type: number + expected_base: + description: Frozen pull-request base SHA + required: true + type: string + expected_head: + description: Frozen pull-request head SHA + required: true + type: string + +permissions: + contents: read + +jobs: + rerun-protected-verifier: + name: Re-run the one protected verifier attempt + permissions: + actions: write + checks: read + contents: read + pull-requests: read + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Validate the live binding and rerun exactly once + env: + EXPECTED_BASE: ${{ inputs.expected_base }} + EXPECTED_HEAD: ${{ inputs.expected_head }} + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ inputs.pr_number }} + REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + test "${GITHUB_REF}" = refs/heads/develop + [[ "${EXPECTED_BASE}" =~ ^[0-9a-f]{40}$ ]] + [[ "${EXPECTED_HEAD}" =~ ^[0-9a-f]{40}$ ]] + [[ "${PR_NUMBER}" =~ ^[1-9][0-9]*$ ]] + pr="$(gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}")" + jq -e \ + --arg base "${EXPECTED_BASE}" \ + --arg head "${EXPECTED_HEAD}" \ + --arg repository "${REPOSITORY}" ' + .state == "open" + and .draft == false + and .base.sha == $base + and .head.sha == $head + and .base.repo.full_name == $repository + and .head.repo.full_name == $repository + and (.base.ref == "develop" or .base.ref == "main") + ' <<<"${pr}" >/dev/null + + neutral_pages="$(gh api --paginate --slurp \ + "repos/${REPOSITORY}/commits/${EXPECTED_HEAD}/check-runs?check_name=Current%20revision%20review&per_page=100")" + neutral="$(jq -c ' + [.[].check_runs[]? | + select(.name == "Current revision review") | + select(.app.id == 15368 and .app.slug == "github-actions") | + select(.status == "completed" and .conclusion == "success")] + ' <<<"${neutral_pages}")" + test "$(jq 'length' <<<"${neutral}")" -eq 1 + neutral_check_id="$(jq -er '.[0].id | select(type == "number" and . > 0)' <<<"${neutral}")" + neutral_details_url="$(jq -r '.[0].details_url // empty' <<<"${neutral}")" + test "${neutral_details_url}" = "${GITHUB_SERVER_URL}/${REPOSITORY}/runs/${neutral_check_id}" + neutral_external_id="$(jq -er '.[0].external_id | select(type == "string" and length > 0)' <<<"${neutral}")" + neutral_summary="$(jq -er '.[0].output.summary | fromjson | select(type == "object")' <<<"${neutral}")" + producer_id="$(jq -er '.producer_run_id | select(type == "number" and . > 0)' <<<"${neutral_summary}")" + producer_url="$(jq -er '.run_url | select(type == "string" and length > 0)' <<<"${neutral_summary}")" + producer_prefix="${GITHUB_SERVER_URL}/${REPOSITORY}/actions/runs/" + test "${producer_url}" = "${producer_prefix}${producer_id}" + author="$(jq -er '.user.login | select(type == "string" and length > 0)' <<<"${pr}")" + base_ref="$(jq -er '.base.ref | select(. == "develop" or . == "main")' <<<"${pr}")" + head_ref="$(jq -er '.head.ref | select(type == "string" and length > 0)' <<<"${pr}")" + jq -e \ + --arg base "${EXPECTED_BASE}" \ + --arg head "${EXPECTED_HEAD}" \ + --arg run_url "${producer_url}" \ + --argjson run_id "${producer_id}" ' + .schema == 4 + and .base_sha == $base + and .head_sha == $head + and .producer_run_id == $run_id + and .run_url == $run_url + ' <<<"${neutral_summary}" >/dev/null + for attempt in 1 2 3 4 5 6 7 8 9 10; do + producer="$(gh api "repos/${REPOSITORY}/actions/runs/${producer_id}")" + if [ "$(jq -r .status <<<"${producer}")" = completed ]; then + test "$(jq -r .conclusion <<<"${producer}")" = success + break + fi + test "${attempt}" -lt 10 + sleep 2 + done + if [ "${author}" = 'lightning-it-release-automation[bot]' ]; then + [[ "${neutral_external_id}" =~ ^mlx90-current-revision:v4:${producer_id}:[0-9a-f]{64}$ ]] + expected_title="Exact-Revision Codex PR #${PR_NUMBER} ${EXPECTED_BASE}..${EXPECTED_HEAD}" + jq -e \ + --arg actor "${author}" \ + --arg base_ref "${base_ref}" \ + --arg base_sha "${EXPECTED_BASE}" \ + --arg run_url "${producer_url}" \ + --arg title "${expected_title}" ' + .event == "workflow_dispatch" + and .path == ".github/workflows/release-bot-exact-head-review.yml" + and .display_title == $title + and .head_branch == $base_ref + and .head_sha == $base_sha + and .html_url == $run_url + and .actor.login == $actor + and .triggering_actor.login == $actor + ' <<<"${producer}" >/dev/null + else + test "${neutral_external_id}" = "mlx90-current-revision:copilot:v5:${producer_id}:${EXPECTED_BASE}:${EXPECTED_HEAD}" + controller_sha="$(jq -er '.controller_sha | select(type == "string" and test("^[0-9a-f]{40}$"))' \ + <<<"${neutral_summary}")" + default_branch="$(gh api "repos/${REPOSITORY}" --jq .default_branch)" + test "${default_branch}" = develop + default_head="$(gh api "repos/${REPOSITORY}/branches/${default_branch}" --jq .commit.sha)" + controller_ancestry="$(gh api \ + "repos/${REPOSITORY}/compare/${controller_sha}...${default_head}")" + jq -e \ + --arg controller "${controller_sha}" ' + .status == "identical" + or (.status == "ahead" and .behind_by == 0 + and .merge_base_commit.sha == $controller) + ' <<<"${controller_ancestry}" >/dev/null + jq -e \ + --arg actor "${author}" \ + --arg head_ref "${head_ref}" \ + --arg head_sha "${EXPECTED_HEAD}" \ + --arg run_url "${producer_url}" ' + .event == "pull_request_target" + and .path == ".github/workflows/copilot-review.yml" + and .name == "Current revision review gate" + and .head_branch == $head_ref + and .head_sha == $head_sha + and .html_url == $run_url + and .actor.login == $actor + and .triggering_actor.login == $actor + ' <<<"${producer}" >/dev/null + fi + + reservations_pages="$(gh api --paginate --slurp \ + "repos/${REPOSITORY}/commits/${EXPECTED_HEAD}/check-runs?check_name=Protected%20current-revision%20verifier&per_page=100")" + reservations="$(jq -c \ + --arg head "${EXPECTED_HEAD}" \ + --arg prefix "rep60-required-workflow:v2:" \ + --arg suffix ":${PR_NUMBER}:${EXPECTED_HEAD}" ' + [.[].check_runs[]? | + select(.name == "Protected current-revision verifier") | + select(.app.id == 15368 and .app.slug == "github-actions") | + select(.head_sha == $head) | + select((.external_id | type) == "string") | + select(.external_id | startswith($prefix) and endswith($suffix))] + ' <<<"${reservations_pages}")" + test "$(jq 'length' <<<"${reservations}")" -eq 1 + reservation_id="$(jq -er '.[0].id | select(type == "number" and . > 0)' <<<"${reservations}")" + reservation_url="$(jq -r '.[0].details_url // empty' <<<"${reservations}")" + test "${reservation_url}" = "${GITHUB_SERVER_URL}/${REPOSITORY}/runs/${reservation_id}" + reservation_external_id="$(jq -er '.[0].external_id | select(type == "string" and length > 0)' <<<"${reservations}")" + [[ "${reservation_external_id}" =~ ^rep60-required-workflow:v2:([1-9][0-9]*):${PR_NUMBER}:${EXPECTED_HEAD}$ ]] + run_id="${BASH_REMATCH[1]}" + verifier_run_url="${GITHUB_SERVER_URL}/${REPOSITORY}/actions/runs/${run_id}" + run="$(gh api "repos/${REPOSITORY}/actions/runs/${run_id}")" + jq -e \ + --arg api_url "${GITHUB_API_URL}" \ + --arg actor "${author}" \ + --arg head_ref "${head_ref}" \ + --arg head_sha "${EXPECTED_HEAD}" \ + --arg repository "${REPOSITORY}" \ + --arg run_url "${verifier_run_url}" ' + .event == "pull_request_target" + and .path == ".github/workflows/supplementary-current-revision-required.yml" + and (.workflow_id | type == "number" and . > 0) + and .workflow_url == ($api_url + "/repos/" + $repository + + "/actions/required_workflows/" + (.workflow_id | tostring)) + and .head_branch == $head_ref + and .head_sha == $head_sha + and .html_url == $run_url + and .actor.login == $actor + and .triggering_actor.login == $actor + and .status == "completed" + ' <<<"${run}" >/dev/null + if [ "$(jq -r .conclusion <<<"${run}")" = success ]; then + echo "Protected verifier already passed; no rerun is needed." + exit 0 + fi + test "$(jq -r .conclusion <<<"${run}")" = failure + test "$(jq -r .run_attempt <<<"${run}")" -eq 1 + gh api --method POST "repos/${REPOSITORY}/actions/runs/${run_id}/rerun" >/dev/null diff --git a/.github/workflows/release-bot-exact-head-review.yml b/.github/workflows/release-bot-exact-head-review.yml index ebf9844..52d3463 100644 --- a/.github/workflows/release-bot-exact-head-review.yml +++ b/.github/workflows/release-bot-exact-head-review.yml @@ -42,7 +42,7 @@ concurrency: # the full binary diff, protected prompt, and protected schema. jobs: exact-revision-codex-review: - name: Protected Exact-Revision Codex review + name: Current revision review if: >- github.event_name == 'workflow_dispatch' && github.actor == 'lightning-it-release-automation[bot]' @@ -371,7 +371,8 @@ jobs: named="$(jq -c \ --arg name "${check_name}" \ '[.[].check_runs[]? | - select(.name == $name)]' \ + select(.name == $name) | + select(.app.id == 15368 and .app.slug == "github-actions")]' \ <<<"${checks}")" count="$(jq 'length' <<<"${named}")" if [ "${count}" -gt 1 ]; then @@ -379,11 +380,6 @@ jobs: exit 1 fi if [ "${count}" -eq 1 ]; then - if ! jq -e '.[0].app.id == 15368 and .[0].app.slug == "github-actions"' \ - <<<"${named}" >/dev/null; then - echo "${check_name} exists under an unauthorized GitHub App." >&2 - exit 1 - fi check_id="$(jq -er '.[0].id | select(type == "number" and . > 0)' <<<"${named}")" check_url="${GITHUB_SERVER_URL}/${REPOSITORY}/runs/${check_id}" current_external_id="$(jq -r '.[0].external_id // empty' <<<"${named}")" @@ -416,14 +412,12 @@ jobs: return fi jq -e \ - --arg check_name "${check_name}" \ --arg evidence "${evidence}" \ --arg external_id "${external_id}" \ --arg head "${EXPECTED_HEAD}" \ --arg url "${check_url}" \ --argjson check_id "${check_id}" ' .[0].id == $check_id - and .[0].name == $check_name and .[0].app.id == 15368 and .[0].app.slug == "github-actions" and .[0].head_sha == $head @@ -448,14 +442,12 @@ jobs: created="$(gh api --method PATCH "repos/${REPOSITORY}/check-runs/${check_id}" \ -f "details_url=${check_url}")" jq -e \ - --arg check_name "${check_name}" \ --arg evidence "${evidence}" \ --arg external_id "${external_id}" \ --arg head "${EXPECTED_HEAD}" \ --arg url "${check_url}" \ --argjson check_id "${check_id}" ' .id == $check_id - and .name == $check_name and .app.id == 15368 and .app.slug == "github-actions" and .head_sha == $head @@ -470,18 +462,6 @@ jobs: 'Current revision review' \ "mlx90-current-revision:v4:${producer_run_id}:${input_sha256}" \ 'Protected Exact-Revision Codex review passed' - # One-time compatibility alias for protected Shared Assets promotion - # PR #1047. It is derived from this same Codex PASS and never starts a - # second reviewer. The develop version replaces this bootstrap file - # during the promotion, removing the legacy Copilot-named context. - if [ "${REPOSITORY}" = lightning-it/shared-assets-lit ] \ - && [ "${PR_NUMBER}" = 1047 ] \ - && [ "${BASE_REF}" = main ]; then - publish_once \ - 'Successful Copilot review' \ - "mlx90-legacy-transition:v4:${producer_run_id}:${input_sha256}" \ - 'Exact-Revision Codex PASS (temporary legacy context; no Copilot)' - fi request-protected-verifier-reevaluation: name: Request protected verifier re-evaluation diff --git a/.lit/push-ready.json b/.lit/push-ready.json index 32ea3ba..941844b 100644 --- a/.lit/push-ready.json +++ b/.lit/push-ready.json @@ -18,7 +18,7 @@ { "id": "copilot-current-head-review", "workflow": ".github/workflows/copilot-review.yml", - "job": "current-revision-reviewed", + "job": "verify-current-revision-policy", "reason": "The authoritative Copilot pull-request review is produced and bound to the current head SHA by GitHub.", "owner": "Lightning IT Application Platform Maintainers" } @@ -31,14 +31,14 @@ }, "agents": { "copilot": { - "enabled": true, - "required": true, + "enabled": false, + "required": false, "command": ["copilot"], "timeout_seconds": 600 }, "codex": { - "enabled": true, - "required": true, + "enabled": false, + "required": false, "command": ["codex"], "timeout_seconds": 900 } diff --git a/scripts/lit-push-ready.py b/scripts/lit-push-ready.py index 9f626ed..52cfcc4 100755 --- a/scripts/lit-push-ready.py +++ b/scripts/lit-push-ready.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Create exact-diff local pipeline and dual-agent review evidence.""" +"""Create deterministic local pipeline evidence without local AI egress.""" from __future__ import annotations @@ -135,9 +135,9 @@ PARITY_GAPS = ( { "id": "copilot-review-surface", - "local": "GitHub Copilot CLI read-only exact-diff review", - "remote": "GitHub Copilot pull-request code review on the current head SHA", - "status": "not-identical-by-product-design", + "local": "prohibited; deterministic checks only", + "remote": "protected current-revision review on the exact head SHA", + "status": "remote-only-by-policy", "remote_gate_required": True, }, { @@ -746,9 +746,10 @@ def validate_agent_config(name: str, value: Any) -> None: raise RuntimeError( f"agents.{name}.enabled and agents.{name}.required must be booleans" ) - if enabled is not True or required is not True: + if enabled is not False or required is not False: raise RuntimeError( - f"agents.{name} must be enabled and required by the v2 policy" + f"agents.{name} must remain disabled and not required by the " + "local no-AI-egress policy" ) command = validate_command(value.get("command"), f"agents.{name}.command") if command != [name]: @@ -2656,7 +2657,7 @@ def ensure_workspace_review_safe( workspace: Path, documented: Optional[dict[str, dict[int, tuple[str, str]]]] = None, ) -> None: - """Scan the complete tracked review snapshot before external model use.""" + """Scan the complete tracked snapshot before local evidence is accepted.""" names = git_output_at(workspace, "ls-files", "-z").split("\0") total = 0 unsafe_paths: list[str] = [] @@ -3521,65 +3522,21 @@ def run_agent_reviews( expected = change.tree_fingerprint if tree_fingerprint() != expected: raise RuntimeError("exact planned push patch is stale before local review") - reviews: list[dict[str, Any]] = [] + if any( + agent["enabled"] or agent["required"] + for agent in config["agents"].values() + ): + raise RuntimeError("local AI execution is prohibited by policy") + # Materialize the exact-patch snapshot so the deterministic secret and + # topology guards still fail closed, without invoking any local reviewer. with sanitized_review_workspace( change, fixture_manifest_bootstrap=fixture_manifest_bootstrap, - ) as ( - workspace, - state_root, - topology, ): - instructions = tracked_instruction_bundle(workspace) - workspace_fingerprint = integration_worktree_fingerprint( - workspace, - include_ignored=True, - ) - reviews.append( - copilot_review( - config, - change, - expected, - workspace=workspace, - state_root=state_root, - instructions=instructions, - topology=topology, - ) - ) - if ( - integration_worktree_fingerprint( - workspace, - include_ignored=True, - ) - != workspace_fingerprint - ): - raise RuntimeError( - "Copilot review changed the sanitized exact-patch workspace" - ) - reviews.append( - codex_review( - config, - change, - expected, - workspace=workspace, - state_root=state_root, - instructions=instructions, - topology=topology, - ) - ) - if ( - integration_worktree_fingerprint( - workspace, - include_ignored=True, - ) - != workspace_fingerprint - ): - raise RuntimeError( - "Codex review changed the sanitized exact-patch workspace" - ) + pass if tree_fingerprint() != expected: - raise RuntimeError("local agent review changed the reviewed Git tree") - return reviews + raise RuntimeError("local deterministic review changed the Git tree") + return [] def command_version(command: list[str]) -> str: @@ -3698,6 +3655,7 @@ def write_evidence( "push_scope": "clean-head", "fixture_manifest_bootstrap": fixture_manifest_bootstrap, "evidence_trust": LOCAL_EVIDENCE_TRUST, + "local_ai_egress": "prohibited", } evidence.write_text( json.dumps(payload, indent=2, sort_keys=True) + "\n", @@ -3781,6 +3739,7 @@ def verify_evidence(config: dict[str, Any]) -> dict[str, Any]: "push_scope": "clean-head", "fixture_manifest_bootstrap": fixture_manifest_bootstrap, "evidence_trust": LOCAL_EVIDENCE_TRUST, + "local_ai_egress": "prohibited", } for key, value in expected.items(): if payload.get(key) != value: @@ -4068,6 +4027,7 @@ def main() -> int: change, fixture_manifest_bootstrap=args.fixture_manifest_bootstrap, ) + print("Deterministic local review passed; no local AI was invoked.") return 0 require_clean_head() original_head = git_output("rev-parse", "HEAD").strip() diff --git a/scripts/materialize-exact-revision-review.py b/scripts/materialize-exact-revision-review.py new file mode 100644 index 0000000..0f285ca --- /dev/null +++ b/scripts/materialize-exact-revision-review.py @@ -0,0 +1,546 @@ +"""Materialize and re-verify the bounded MLX-90 exact-revision review input.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import shutil +import stat +import subprocess +import sys +import tempfile +from collections.abc import Sequence +from pathlib import Path +from typing import Any, NoReturn + +SHA1_PATTERN = re.compile(r"^[0-9a-f]{40}$") +REPOSITORY_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") +RELEASE_BOT = "lightning-it-release-automation[bot]" +MAX_REVIEW_BYTES = 200_000 +MAX_PROTECTED_ASSET_BYTES = 1_000_000 +ASSET_ARGUMENTS = { + "materializer_sha256": "materializer_path", + "prompt_sha256": "prompt_path", + "schema_sha256": "schema_path", + "workflow_sha256": "workflow_path", +} +IMMUTABLE_METADATA_KEYS = ( + "schema_version", + "repository", + "pull_request", + "base_ref", + "base_sha", + "head_sha", + "merge_base_sha", + "integration_tree_sha", + "diff_sha256", + "review_bytes", + "trusted_workflow_sha", + "trigger", + "materializer_sha256", + "prompt_sha256", + "schema_sha256", + "workflow_sha256", + "input_sha256", +) + + +class MaterializationError(RuntimeError): + """Raised when the exact review input cannot be proven.""" + + +def fail(message: str) -> NoReturn: + raise MaterializationError(message) + + +def executable(name: str) -> str: + resolved = shutil.which(name, path=os.defpath) + if resolved is None: + fail(f"Required executable is unavailable in the system path: {name}") + return resolved + + +def command_environment(*, home: Path, include_token: bool) -> dict[str, str]: + environment = { + "GIT_CONFIG_GLOBAL": os.devnull, + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_TERMINAL_PROMPT": "0", + "HOME": str(home), + "LANG": "C.UTF-8", + "LC_ALL": "C.UTF-8", + "PATH": os.defpath, + "XDG_CONFIG_HOME": str(home / ".config"), + } + if include_token: + token = os.environ.get("GH_TOKEN", "") + if not token: + fail("GH_TOKEN is required for live GitHub verification.") + environment["GH_TOKEN"] = token + return environment + + +def run( + arguments: Sequence[str], + *, + environment: dict[str, str], + cwd: Path | None = None, + binary: bool = False, +) -> subprocess.CompletedProcess[Any]: + result = subprocess.run( # noqa: S603 + list(arguments), + cwd=cwd, + env=environment, + check=False, + capture_output=True, + text=not binary, + ) + if result.returncode != 0: + stderr = ( + result.stderr + if isinstance(result.stderr, str) + else result.stderr.decode(errors="replace") + ) + command = " ".join(arguments) or "" + fail(f"Command failed closed: {command}: {stderr.strip()}") + return result + + +def require_sha(value: str, name: str) -> str: + if not SHA1_PATTERN.fullmatch(value): + fail(f"{name} must be a full lowercase SHA-1 object ID.") + return value + + +def require_single_sha_output(value: str, name: str) -> str: + lines = value.splitlines() + if len(lines) != 1: + fail(f"{name} must contain exactly one Git object ID.") + return require_sha(lines[0], name) + + +def protected_asset_bytes(path: Path, name: str) -> bytes: + """Read one bounded regular protected asset without following a symlink.""" + no_follow = getattr(os, "O_NOFOLLOW", None) + if not isinstance(no_follow, int) or no_follow == 0: + fail("Protected asset reading requires O_NOFOLLOW support.") + flags = os.O_RDONLY + flags |= getattr(os, "O_CLOEXEC", 0) + flags |= no_follow + try: + descriptor = os.open(path, flags) + except OSError as error: + fail(f"Protected {name} is unavailable: {error}") + try: + details = os.fstat(descriptor) + if not stat.S_ISREG(details.st_mode): + fail(f"Protected {name} must be a regular non-symlink file.") + if details.st_size <= 0 or details.st_size > MAX_PROTECTED_ASSET_BYTES: + fail(f"Protected {name} must contain 1..{MAX_PROTECTED_ASSET_BYTES} bytes.") + with os.fdopen(descriptor, "rb", closefd=False) as protected_asset: + payload = protected_asset.read(MAX_PROTECTED_ASSET_BYTES + 1) + if len(payload) != details.st_size: + fail(f"Protected {name} changed while reading.") + return payload + finally: + os.close(descriptor) + + +def bind_protected_assets( + metadata: dict[str, Any], asset_paths: dict[str, Path] +) -> dict[str, Any]: + """Bind every base-controlled review asset into one canonical input hash.""" + if set(asset_paths) != set(ASSET_ARGUMENTS): + fail("The complete protected review-asset set is required.") + bound = dict(metadata) + for metadata_key, path in asset_paths.items(): + asset_name = metadata_key.removesuffix("_sha256").replace("_", " ") + bound[metadata_key] = hashlib.sha256( + protected_asset_bytes(path, asset_name) + ).hexdigest() + canonical = json.dumps(bound, sort_keys=True, separators=(",", ":")).encode("utf-8") + bound["input_sha256"] = hashlib.sha256(canonical).hexdigest() + return bound + + +def asset_paths_from_arguments(arguments: argparse.Namespace) -> dict[str, Path]: + paths: dict[str, Path] = {} + for metadata_key, argument_name in ASSET_ARGUMENTS.items(): + path = getattr(arguments, argument_name, None) + if not isinstance(path, Path): + fail(f"Protected asset argument is required: {argument_name}") + paths[metadata_key] = path + return paths + + +def validate_inputs(arguments: argparse.Namespace) -> None: + if not REPOSITORY_PATTERN.fullmatch(arguments.repository): + fail("Repository must use the owner/name form.") + if arguments.pull_request <= 0: + fail("Pull-request number must be positive.") + if arguments.base_ref not in {"develop", "main"}: + fail("Base ref must be develop or main.") + require_sha(arguments.expected_base, "Expected base") + require_sha(arguments.expected_head, "Expected head") + require_sha(arguments.trusted_workflow_sha, "Trusted workflow") + if arguments.expected_base != arguments.trusted_workflow_sha: + fail("The protected workflow SHA must equal the live pull-request base SHA.") + if arguments.trigger not in {"ready_for_review", "app_dispatch"}: + fail("Unsupported exact-review trigger.") + if ( + arguments.trigger == "app_dispatch" + and arguments.dispatch_ref != f"refs/heads/{arguments.base_ref}" + ): + fail("App dispatch must execute from the protected pull-request base ref.") + + +def read_live_pull_request( + arguments: argparse.Namespace, *, home: Path +) -> dict[str, Any]: + gh = executable("gh") + result = run( + [ + gh, + "api", + f"repos/{arguments.repository}/pulls/{arguments.pull_request}", + ], + environment=command_environment(home=home, include_token=True), + ) + try: + pull_request = json.loads(result.stdout) + except json.JSONDecodeError as error: + fail(f"GitHub returned malformed pull-request JSON: {error}") + expected = { + "state": "open", + "draft": False, + "author": RELEASE_BOT, + "author_type": "Bot", + "base_ref": arguments.base_ref, + "base_sha": arguments.expected_base, + "base_repository": arguments.repository, + "head_sha": arguments.expected_head, + "head_repository": arguments.repository, + } + user = pull_request.get("user") or {} + base = pull_request.get("base") or {} + head = pull_request.get("head") or {} + base_repository = base.get("repo") or {} + head_repository = head.get("repo") or {} + observed = { + "state": pull_request.get("state"), + "draft": pull_request.get("draft"), + "author": user.get("login"), + "author_type": user.get("type"), + "base_ref": base.get("ref"), + "base_sha": base.get("sha"), + "base_repository": base_repository.get("full_name"), + "head_sha": head.get("sha"), + "head_repository": head_repository.get("full_name"), + } + if observed != expected: + fail( + f"Live pull-request binding changed or is unauthorized: {json.dumps(observed, sort_keys=True)}" + ) + return pull_request + + +def git_output( + git: str, + git_dir: Path, + arguments: Sequence[str], + *, + environment: dict[str, str], + binary: bool = False, +) -> bytes | str: + result = run( + [git, f"--git-dir={git_dir}", *arguments], + environment=environment, + binary=binary, + ) + return result.stdout + + +def materialize( + arguments: argparse.Namespace, output_directory: Path +) -> dict[str, Any]: + validate_inputs(arguments) + if output_directory.exists(): + fail(f"Review workspace already exists: {output_directory}") + try: + output_directory.mkdir(mode=0o700, parents=False) + except OSError as error: + fail(f"Unable to create the exact-revision review workspace: {error}") + + runner_temp = Path(os.environ.get("RUNNER_TEMP", tempfile.gettempdir())).resolve() + if not runner_temp.is_dir(): + fail("RUNNER_TEMP must identify an existing directory.") + with tempfile.TemporaryDirectory( + prefix="exact-revision-materializer.", dir=runner_temp + ) as temporary: + temporary_root = Path(temporary) + home = temporary_root / "home" + home.mkdir(mode=0o700) + read_live_pull_request(arguments, home=home) + + git = executable("git") + git_dir = temporary_root / "objects.git" + git_environment = command_environment(home=home, include_token=True) + run([git, "init", "--bare", str(git_dir)], environment=git_environment) + git_output( + git, + git_dir, + ["config", "credential.helper", "!gh auth git-credential"], + environment=git_environment, + ) + git_output( + git, + git_dir, + [ + "remote", + "add", + "origin", + f"https://github.com/{arguments.repository}.git", + ], + environment=git_environment, + ) + git_output( + git, + git_dir, + [ + "fetch", + "--quiet", + "--no-tags", + "--no-recurse-submodules", + "origin", + f"+{arguments.expected_base}:refs/review/base", + f"+{arguments.expected_head}:refs/review/head", + ], + environment=git_environment, + ) + for name, expected in ( + ("base", arguments.expected_base), + ("head", arguments.expected_head), + ): + resolved = str( + git_output( + git, + git_dir, + ["rev-parse", f"refs/review/{name}^{{commit}}"], + environment=git_environment, + ) + ).strip() + if resolved != expected: + fail(f"Fetched {name} object does not equal the expected object ID.") + + merge_base = require_single_sha_output( + str( + git_output( + git, + git_dir, + [ + "merge-base", + "--all", + arguments.expected_base, + arguments.expected_head, + ], + environment=git_environment, + ) + ), + "Merge base", + ) + + integration_tree = require_single_sha_output( + str( + git_output( + git, + git_dir, + [ + "merge-tree", + "--write-tree", + arguments.expected_base, + arguments.expected_head, + ], + environment=git_environment, + ) + ), + "Integration tree", + ) + object_type = str( + git_output( + git, + git_dir, + ["cat-file", "-t", integration_tree], + environment=git_environment, + ) + ).strip() + if object_type != "tree": + fail("The integration object is not a Git tree.") + + diff = git_output( + git, + git_dir, + [ + "diff", + "--binary", + "--full-index", + "--no-color", + "--no-ext-diff", + "--no-textconv", + f"{arguments.expected_base}^{{tree}}", + integration_tree, + ], + environment=git_environment, + binary=True, + ) + if not isinstance(diff, bytes): + fail("Git returned an invalid diff representation.") + review_bytes = len(diff) + if review_bytes <= 0 or review_bytes >= MAX_REVIEW_BYTES: + fail( + "Exact-revision review input must contain " + f"1..{MAX_REVIEW_BYTES - 1} bytes; observed {review_bytes}." + ) + diff_sha256 = hashlib.sha256(diff).hexdigest() + + read_live_pull_request(arguments, home=home) + metadata = { + "schema_version": 3, + "repository": arguments.repository, + "pull_request": arguments.pull_request, + "base_ref": arguments.base_ref, + "base_sha": arguments.expected_base, + "head_sha": arguments.expected_head, + "merge_base_sha": merge_base, + "integration_tree_sha": integration_tree, + "diff_sha256": diff_sha256, + "review_bytes": review_bytes, + "trusted_workflow_sha": arguments.trusted_workflow_sha, + "trigger": arguments.trigger, + } + patch = output_directory / "change.patch" + metadata_path = output_directory / "review-metadata.json" + patch.write_bytes(diff) + metadata_path.write_text( + json.dumps(metadata, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + patch.chmod(0o600) + metadata_path.chmod(0o600) + return metadata + + +def bind_assets(review_directory: Path, asset_paths: dict[str, Path]) -> dict[str, Any]: + metadata_path = review_directory / "review-metadata.json" + if not metadata_path.is_file() or metadata_path.is_symlink(): + fail("Review metadata must be a regular, non-symlink file.") + try: + metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, UnicodeDecodeError) as error: + fail(f"Review metadata is malformed: {error}") + if any(key in metadata for key in (*ASSET_ARGUMENTS, "input_sha256")): + fail("Review metadata already contains protected asset bindings.") + bound = bind_protected_assets(metadata, asset_paths) + metadata_path.write_text( + json.dumps(bound, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + return bound + + +def verify( + arguments: argparse.Namespace, + review_directory: Path, + asset_paths: dict[str, Path], +) -> dict[str, Any]: + validate_inputs(arguments) + patch = review_directory / "change.patch" + metadata_path = review_directory / "review-metadata.json" + if ( + not patch.is_file() + or patch.is_symlink() + or not metadata_path.is_file() + or metadata_path.is_symlink() + ): + fail("The review diff and metadata must be regular, non-symlink files.") + patch_size = patch.stat().st_size + if patch_size <= 0 or patch_size >= MAX_REVIEW_BYTES: + fail(f"The review diff must be between 1 and {MAX_REVIEW_BYTES - 1} bytes.") + try: + expected_metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, UnicodeDecodeError) as error: + fail(f"Review metadata is malformed: {error}") + if not isinstance(expected_metadata, dict): + fail("Review metadata must be a JSON object.") + expected_keys = set(IMMUTABLE_METADATA_KEYS) + observed_keys = set(expected_metadata) + if observed_keys != expected_keys: + missing = sorted(expected_keys - observed_keys) + unexpected = sorted(observed_keys - expected_keys) + fail( + "Review metadata keys differ from the protected materializer output: " + f"missing={missing}, unexpected={unexpected}" + ) + + runner_temp = Path(os.environ.get("RUNNER_TEMP", tempfile.gettempdir())).resolve() + if not runner_temp.is_dir(): + fail("RUNNER_TEMP must identify an existing directory.") + with tempfile.TemporaryDirectory( + prefix="exact-revision-recheck.", dir=runner_temp + ) as temporary: + regenerated = Path(temporary) / "review" + actual_metadata = bind_protected_assets( + materialize(arguments, regenerated), asset_paths + ) + if patch.read_bytes() != (regenerated / "change.patch").read_bytes(): + fail("The full binary diff changed during exact-revision verification.") + for key in IMMUTABLE_METADATA_KEYS: + if expected_metadata.get(key) != actual_metadata.get(key): + fail(f"Exact-revision metadata changed during verification: {key}") + return actual_metadata + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("mode", choices=("materialize", "bind-assets", "verify")) + parser.add_argument("--repository", required=True) + parser.add_argument("--pull-request", required=True, type=int) + parser.add_argument("--base-ref", required=True) + parser.add_argument("--expected-base", required=True) + parser.add_argument("--expected-head", required=True) + parser.add_argument("--trusted-workflow-sha", required=True) + parser.add_argument( + "--trigger", required=True, choices=("ready_for_review", "app_dispatch") + ) + parser.add_argument("--dispatch-ref", default="") + parser.add_argument("--review-directory", required=True, type=Path) + parser.add_argument("--materializer-path", type=Path) + parser.add_argument("--prompt-path", type=Path) + parser.add_argument("--schema-path", type=Path) + parser.add_argument("--workflow-path", type=Path) + return parser.parse_args() + + +def main() -> int: + arguments = parse_arguments() + try: + if arguments.mode == "materialize": + metadata = materialize(arguments, arguments.review_directory) + elif arguments.mode == "bind-assets": + metadata = bind_assets( + arguments.review_directory, asset_paths_from_arguments(arguments) + ) + else: + metadata = verify( + arguments, + arguments.review_directory, + asset_paths_from_arguments(arguments), + ) + except MaterializationError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + print(json.dumps(metadata, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())