From 28ac2e57163e18266581fac1dd3a26f50ae0eb60 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 23:28:17 +0000 Subject: [PATCH] chore(ci): remove the unused signed-push action and its smoke test signed-push re-pushed a workflow's commits as GitHub-Verified through a GitHub App. That App was never created: vars.APP_ID and secrets.APP_PRIVATE_KEY have never been set on this repo. Nothing else used the action either. The 2026-10-14 workflow audit (docs/reorg/2026-10-14-tidy/workflow-audit.adoc) counts 0 external consumers, and here only its own smoke test called it. That smoke test has failed on every run since 2026-08-24. A manual run today (run 39333160936) failed for the same reason: an empty app-id. The owner asked for it to be removed, since it is not attached to anything in use. Removed: - .github/actions/signed-push/ (action.yml, push-verified.sh, README.md) - .github/workflows/signed-push-smoke.yml - scripts/tests/signed-push-test.sh - the smoke workflow's actions.lock entry, dropped by prune-stale.sh References updated: - SIGNING-POLICY.adoc: the bot-commit row and the "git push from a workflow" path now name createCommitOnBranch directly, which is what the action wrapped. scripts/regen-dependabot-locks.sh is the example. The rule itself is unchanged. - config/README.adoc, tag-ruleset-canon.yml, dependabot-lock-regen.yml: signed-push is gone from the lists of APP_ID readers. Left as they are: - historical notes in .githooks and docs/reorg. - the local-action placeholder string in validate-sha-pins-test.sh, which never needed the real directory. Checks: - check-lock-sync, validate-actions-lock, validate-sha-pins and the actions-lock gate (gh-actions-lock v0.2.0) pass. - prune-stale makes no further change. check-workflow-duplicate-keys, check-standards-map and link-rot-guard pass. actionlint is clean. - Shell suite: 86/89. The 3 failures fail the same way on origin/main for missing local tools (deno, ssh-keygen, the pinned gh-actions-lock binary). gh-actions-lock-contract-test passes 19/19 when that binary is supplied. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014h2gFLLQibfB5N5hFz9ZCD --- .github/actions/signed-push/README.md | 70 ---------- .github/actions/signed-push/action.yml | 47 ------- .github/actions/signed-push/push-verified.sh | 138 ------------------- .github/workflows/actions.lock | 4 - .github/workflows/dependabot-lock-regen.yml | 4 +- .github/workflows/signed-push-smoke.yml | 83 ----------- .github/workflows/tag-ruleset-canon.yml | 2 +- config/README.adoc | 10 +- docs/SIGNING-POLICY.adoc | 5 +- scripts/tests/signed-push-test.sh | 126 ----------------- 10 files changed, 10 insertions(+), 479 deletions(-) delete mode 100644 .github/actions/signed-push/README.md delete mode 100644 .github/actions/signed-push/action.yml delete mode 100755 .github/actions/signed-push/push-verified.sh delete mode 100644 .github/workflows/signed-push-smoke.yml delete mode 100755 scripts/tests/signed-push-test.sh diff --git a/.github/actions/signed-push/README.md b/.github/actions/signed-push/README.md deleted file mode 100644 index 8a7a9812a..000000000 --- a/.github/actions/signed-push/README.md +++ /dev/null @@ -1,70 +0,0 @@ - -# `signed-push` — verified commits from CI/AI tooling - -Estate composite action that pushes a branch's local commits as GitHub **Verified** -commits, so they satisfy `required_signatures` branch rulesets. - -## Why - -Commits made by automation with a **PAT** or the default `GITHUB_TOKEN` show as -**Unverified**, and any branch whose ruleset requires signed commits then **cannot -merge** (e.g. `mergeStateStatus: BLOCKED`). The only reliable way for automation to -produce Verified commits is the GraphQL [`createCommitOnBranch`] mutation -authenticated as a **GitHub App installation** — GitHub signs those commits itself. - -This action mints an App installation token and runs -[`push-verified.sh`](push-verified.sh) (bash, `gh` and `jq`), which diffs local -vs. remote and replays the local commits through `createCommitOnBranch`, oldest -first, each call naming the remote head it expects so that a concurrent push -fails closed. On success the local branch is reset to the remote. - -## Prerequisites (one-time, account owner) - -1. Register a **GitHub App** on the org (Settings → Developer settings → GitHub Apps). - Repository permissions: **Contents: Read & write**, **Workflows: Read & write** - (to edit `.github/workflows`), optionally **Administration: Read & write** (for - `delete_repo`). -2. **Install** the App on the org/repos and **generate a private key** (`.pem`). -3. Store **`APP_ID`** and **`APP_PRIVATE_KEY`** as org (or repo) secrets. -4. If the branch ruleset also enforces **pull-request-only merges** or a **merge - queue**, add the App to that ruleset's **bypass list** — `required_signatures` - is satisfied by this action, but those are separate rules. - -## Usage - -```yaml -jobs: - update: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - name: Make changes - run: | - echo "generated" >> CHANGELOG.md - git config user.name "estate-bot[bot]" - git config user.email "estate-bot[bot]@users.noreply.github.com" - git commit -am "chore: regenerate changelog" - - name: Push as Verified - uses: hyperpolymath/standards/.github/actions/signed-push@main - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - # branch: defaults to the current ref -``` - -## Notes - -- `create-github-app-token` is SHA-pinned (v3.2.0) per estate supply-chain policy. - The push itself is `push-verified.sh`, so no other third-party action runs. (It - replaced `Asana/push-signed-commits`, a Python action with no licence file.) -- The remote branch must already exist: `createCommitOnBranch` commits onto an - existing ref. -- `createCommitOnBranch` cannot express merge commits, symlinks, submodules or file - modes, so a push containing any of them is refused before anything is sent, and - the pushed tree is checked against the local one. -- Run **after** local commits exist in the job — the action pushes the delta - between your local branch and its remote. - -[`createCommitOnBranch`]: https://github.blog/changelog/2021-09-13-a-simpler-api-for-authoring-commits/ diff --git a/.github/actions/signed-push/action.yml b/.github/actions/signed-push/action.yml deleted file mode 100644 index 88a4eb721..000000000 --- a/.github/actions/signed-push/action.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Signed push (verified commits via GitHub App) -description: > - Re-push the current branch's local commits as GitHub-"Verified" commits using a - GitHub App installation token and the createCommitOnBranch GraphQL mutation (via - push-verified.sh beside this file: bash, gh and jq, no third-party action). This - satisfies `required_signatures` branch rulesets that a PAT or the default - GITHUB_TOKEN cannot. Run this AFTER you have made local commits in the job - (checkout -> edit -> git commit -> this action). - -inputs: - app-id: - description: GitHub App ID (pass from a repository/organization variable or secret). - required: true - private-key: - description: GitHub App private key, PEM contents (pass from a secret). - required: true - branch: - description: Branch to push as verified (local name == remote name). Defaults to the current ref. - required: false - default: '' - -runs: - using: composite - steps: - - name: Mint GitHub App installation token - id: app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 - with: - app-id: ${{ inputs.app-id }} - private-key: ${{ inputs.private-key }} - - - name: Resolve branch name - id: branch - shell: bash - env: - BRANCH_INPUT: ${{ inputs.branch }} - run: echo "name=${BRANCH_INPUT:-$GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" - - # createCommitOnBranch extracts authorship from the App installation credential - # and auto-marks the resulting commits as "Verified". A PAT would leave them - # Unverified — which is why this must be an App token, not GITHUB_TOKEN/PAT. - - name: Push local commits as Verified - shell: bash - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - BRANCH: ${{ steps.branch.outputs.name }} - run: bash "$GITHUB_ACTION_PATH/push-verified.sh" "$BRANCH" diff --git a/.github/actions/signed-push/push-verified.sh b/.github/actions/signed-push/push-verified.sh deleted file mode 100755 index e16d4328c..000000000 --- a/.github/actions/signed-push/push-verified.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# -# push-verified.sh — re-push a branch's local commits as GitHub-"Verified" -# commits through the GraphQL createCommitOnBranch mutation. -# -# Replaces Asana/push-signed-commits, which was Python (banned here), had no -# licence file, pulled the tag-pinned actions/setup-python@v2 and ran an -# unpinned `pip install requests` at run time. The contract is the same: -# -# * the remote branch must already exist and must not have diverged: every -# commit on it must already be in the local branch; -# * each local commit not on the remote is recreated, oldest first, with -# its message, by one createCommitOnBranch call that names the remote -# head it expects (expectedHeadOid), so a concurrent push fails closed; -# * on success the local branch is reset to the remote, whose commits now -# carry GitHub's authorship and verification. -# -# Two things are stricter than the Python action: -# * file contents are base64'd from the raw blob, so binary files survive -# (the Python action decoded them as UTF-8 text); -# * createCommitOnBranch can express neither file modes nor symlinks nor -# submodules, so a commit that adds or changes one is REFUSED rather than -# silently flattened, and the pushed tree is compared with the local one. -# -# Usage: push-verified.sh BRANCH -# Env: GH_TOKEN (App installation token), GITHUB_REPOSITORY (owner/repo), -# REMOTE (default origin), GH_BIN (default gh; the test suite stubs it). -set -euo pipefail - -BRANCH="${1:?usage: push-verified.sh BRANCH}" -REMOTE="${REMOTE:-origin}" -GH_BIN="${GH_BIN:-gh}" -REPO="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY (owner/repo) is required}" - -# die: report an error in Actions annotation form and stop. -die() { echo "::error::signed-push: $*" >&2; exit 1; } - -WORK="$(mktemp -d)" -trap 'rm -rf "$WORK"' EXIT - -git fetch --quiet "$REMOTE" "$BRANCH" -REMOTE_HEAD="$(git rev-parse "$REMOTE/$BRANCH")" -LOCAL_HEAD="$(git rev-parse --verify "refs/heads/$BRANCH")" - -[ "$(git merge-base "$LOCAL_HEAD" "$REMOTE_HEAD")" = "$REMOTE_HEAD" ] \ - || die "$REMOTE/$BRANCH has commits that $BRANCH lacks; refusing to push over them" - -mapfile -t COMMITS < <(git rev-list --reverse "$REMOTE_HEAD..$LOCAL_HEAD") -if [ "${#COMMITS[@]}" -eq 0 ]; then - echo "signed-push: $BRANCH has no commits that $REMOTE/$BRANCH lacks; nothing to push" - exit 0 -fi - -# changes_json: write the commit's file changes as {additions, deletions} -# JSON to $2. Renames arrive as a deletion plus an addition (--no-renames). -changes_json() { - local c="$1" out="$2" meta path old_mode new_mode new_blob status n=0 - : > "$out.adds"; : > "$out.dels" - while IFS= read -r -d '' meta && IFS= read -r -d '' path; do - read -r old_mode new_mode _ new_blob status <<<"${meta#:}" - case "$status" in - D) jq -nc --arg p "$path" '{path: $p}' >> "$out.dels" ;; - A|M|T) - case "$new_mode" in - 100644|100755) ;; - 120000) die "$c adds or changes the symlink $path; createCommitOnBranch cannot express symlinks" ;; - 160000) die "$c adds or changes the submodule $path; createCommitOnBranch cannot express submodules" ;; - *) die "$c gives $path the unsupported mode $new_mode" ;; - esac - if [ "$status" = A ] && [ "$new_mode" != 100644 ]; then - die "$c adds $path as executable ($new_mode); createCommitOnBranch would drop the mode" - fi - if [ "$status" != A ] && [ "$old_mode" != "$new_mode" ]; then - die "$c changes the mode of $path ($old_mode -> $new_mode); createCommitOnBranch cannot express modes" - fi - n=$((n + 1)) - git cat-file blob "$new_blob" | base64 -w0 > "$out.blob.$n" - jq -nc --arg p "$path" --rawfile b "$out.blob.$n" '{path: $p, contents: $b}' >> "$out.adds" - ;; - *) die "$c has an unsupported change ($status) to $path" ;; - esac - done < <(git diff --raw -z --no-renames --no-abbrev "$c^" "$c") - jq -n --slurpfile a "$out.adds" --slurpfile d "$out.dels" \ - '{additions: $a, deletions: $d}' > "$out" -} - -# Validate and prepare EVERY commit before sending any, so a commit that -# cannot be expressed refuses the whole push instead of leaving it partial. -i=0 -for c in "${COMMITS[@]}"; do - [ "$(git rev-list --parents -n 1 "$c" | wc -w)" -eq 2 ] \ - || die "$c is a merge commit; createCommitOnBranch can only create single-parent commits" - i=$((i + 1)) - changes_json "$c" "$WORK/changes.$i.json" -done - -expected="$REMOTE_HEAD" -pushed=0 -i=0 -for c in "${COMMITS[@]}"; do - i=$((i + 1)) - jq -n \ - --arg repo "$REPO" --arg branch "$BRANCH" --arg oid "$expected" \ - --arg headline "$(git log -1 --format=%s "$c")" \ - --arg body "$(git log -1 --format=%b "$c")" \ - --slurpfile fc "$WORK/changes.$i.json" \ - '{query: "mutation($in: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $in) { commit { oid } } }", - variables: {in: { - branch: {repositoryNameWithOwner: $repo, branchName: $branch}, - expectedHeadOid: $oid, - message: {headline: $headline, body: $body}, - fileChanges: $fc[0]}}}' > "$WORK/payload.json" - new="$("$GH_BIN" api graphql --input "$WORK/payload.json" --jq '.data.createCommitOnBranch.commit.oid' 2>"$WORK/gh.err" || true)" - if ! [[ "$new" =~ ^[0-9a-f]{40}$ ]]; then - msg="createCommitOnBranch failed for $c: $(tr '\n' ' ' < "$WORK/gh.err")" - if [ "$pushed" -gt 0 ]; then - msg="$msg -- PARTIAL PUSH: $pushed of ${#COMMITS[@]} commit(s) are on $REMOTE/$BRANCH; reconcile by hand" - fi - die "$msg" - fi - expected="$new" - pushed=$((pushed + 1)) - echo "signed-push: $c -> $new" -done - -git fetch --quiet "$REMOTE" "$BRANCH" -[ "$(git rev-parse "$REMOTE/$BRANCH")" = "$expected" ] \ - || die "$REMOTE/$BRANCH moved after the push (expected $expected)" -[ "$(git rev-parse "$REMOTE/$BRANCH^{tree}")" = "$(git rev-parse "$LOCAL_HEAD^{tree}")" ] \ - || die "the pushed tree differs from the local one; $REMOTE/$BRANCH needs inspection" - -if [ "$(git symbolic-ref --quiet --short HEAD || true)" = "$BRANCH" ]; then - git reset --quiet --hard "$REMOTE/$BRANCH" -else - git branch --quiet --force "$BRANCH" "$REMOTE/$BRANCH" -fi -echo "signed-push: pushed $pushed commit(s) to $REMOTE/$BRANCH as Verified; $BRANCH now matches it" diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index a997efd99..ebb88ef79 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -156,10 +156,6 @@ workflows: - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9' - 'step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25' - '.github/workflows/signed-push-smoke.yml': - - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' - - 'step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25' '.github/workflows/spark-theatre-gate.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/tag-ruleset-canon.yml': diff --git a/.github/workflows/dependabot-lock-regen.yml b/.github/workflows/dependabot-lock-regen.yml index c086d2dbd..16fda6a38 100644 --- a/.github/workflows/dependabot-lock-regen.yml +++ b/.github/workflows/dependabot-lock-regen.yml @@ -8,8 +8,8 @@ # credentials and runs it. # # CREDENTIAL: the estate's dedicated applier App — vars.APP_ID and -# secrets.APP_PRIVATE_KEY, the same slot apply-workflow-pins.yml and -# signed-push read (owner ruling STD-R-3: NOT OikosBot, which is a ruleset +# secrets.APP_PRIVATE_KEY, the same slot apply-workflow-pins.yml reads +# (owner ruling STD-R-3: NOT OikosBot, which is a ruleset # bypass actor). Needs Contents R/W, Workflows R/W, Pull requests R/W (the # default-branch repair opens PRs), and must be installed on BOTH owners. Until # it exists the run warns and exits 0. diff --git a/.github/workflows/signed-push-smoke.yml b/.github/workflows/signed-push-smoke.yml deleted file mode 100644 index 9ab2744bb..000000000 --- a/.github/workflows/signed-push-smoke.yml +++ /dev/null @@ -1,83 +0,0 @@ -# This workflow is managed by gh actions-lock. -# SPDX-License-Identifier: MPL-2.0 -# This workflow is managed by gh actions-lock. -# End-to-end check for the signed-push composite action: pushes a throwaway commit -# via the GitHub App and asserts GitHub marks it "Verified". Manual trigger only. -# Requires repo/org secret APP_PRIVATE_KEY and variable APP_ID (the estate App). -name: signed-push smoke test - -on: - workflow_dispatch: - -# Manual workflow: use concurrency group to prevent concurrent runs but -# do NOT cancel-in-progress since this is a manually triggered mutation. -concurrency: - group: ${{ github.workflow }} - -permissions: - contents: read - -jobs: - smoke: - runs-on: ubuntu-latest - # Safe here because `smoke` is a steps-job. Never add timeout-minutes to a - # job that `uses:` a reusable workflow — it is an illegal key there and - # GitHub refuses to parse the ENTIRE file, killing every job in it. - timeout-minutes: 10 - permissions: - contents: write - steps: - - name: Harden runner (egress audit) - uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 - with: - egress-policy: audit - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - - name: Create throwaway branch + a local commit - id: prep - shell: bash - run: | - set -euo pipefail - BRANCH="ci/signed-push-smoke-${GITHUB_RUN_ID}" - echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" - git config user.name "estate-committer[bot]" - git config user.email "estate-committer[bot]@users.noreply.github.com" - git checkout -b "$BRANCH" - # Create the remote branch at the current base so createCommitOnBranch has - # an existing ref to commit onto, then fetch its tracking ref for the diff. - git push origin "$BRANCH" - git fetch origin "$BRANCH" - # Trivial change to be pushed as a Verified commit. - mkdir -p .ci-smoke - date -u +"%Y-%m-%dT%H:%M:%SZ" > .ci-smoke/last-signed-push.txt - git add .ci-smoke/last-signed-push.txt - git commit -m "test(signed-push): verified-commit smoke [run ${GITHUB_RUN_ID}]" - - - name: Push the local commit as Verified (via the App) - uses: ./.github/actions/signed-push - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - branch: ${{ steps.prep.outputs.branch }} - - - name: Assert Verified, then delete the throwaway branch - shell: bash - env: - GH_TOKEN: ${{ github.token }} - BRANCH: ${{ steps.prep.outputs.branch }} - run: | - set -euo pipefail - SHA=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${BRANCH}" --jq '.sha') - read -r VERIFIED REASON < <(gh api "repos/${GITHUB_REPOSITORY}/commits/${SHA}" \ - --jq '.commit.verification | "\(.verified) \(.reason)"') - echo "head ${SHA} → verified=${VERIFIED} reason=${REASON}" - # Always clean up the throwaway branch. - git push origin --delete "$BRANCH" || true - if [ "$VERIFIED" != "true" ]; then - echo "::error::signed-push produced an UNVERIFIED commit (reason=${REASON}). Check that APP_ID/APP_PRIVATE_KEY are set and the App is installed on this repo." - exit 1 - fi - echo "✅ signed-push produced a Verified commit — Arm B1 works end-to-end." diff --git a/.github/workflows/tag-ruleset-canon.yml b/.github/workflows/tag-ruleset-canon.yml index 693e9d432..a9444ce24 100644 --- a/.github/workflows/tag-ruleset-canon.yml +++ b/.github/workflows/tag-ruleset-canon.yml @@ -61,7 +61,7 @@ jobs: # installed on BOTH owners (hyperpolymath and metadatastician). # # It is ALSO not the pin applier's App (vars.APP_ID, used by - # apply-workflow-pins, dependabot-lock-regen and signed-push-smoke). That App + # apply-workflow-pins and dependabot-lock-regen). That App # holds contents/workflows/pull-requests write; giving it administration:write # would let the thing that re-points pins also rewrite the rulesets that gate # it. Owner ruling 2026-10-08: two Apps, two slots. diff --git a/config/README.adoc b/config/README.adoc index 8b1025182..0e3552e95 100644 --- a/config/README.adoc +++ b/config/README.adoc @@ -347,17 +347,17 @@ tag. Creation is reserved to admins and to OikosBot (2538504) in `always` mode, so release workflows create tags *through the App*. That App credential does not exist yet. `hyperpolymath/standards` holds no -`APP_ID` variable and no `APP_PRIVATE_KEY` secret (checked 2026-09-02; -`signed-push-smoke.yml` has failed on every run since 2026-08-24 for that -reason). Until the owner plants them (decision O11), the periodic +`APP_ID` variable and no `APP_PRIVATE_KEY` secret (checked 2026-09-02, and +again on 2026-10-19 when the last run of the since-removed `signed-push-smoke.yml` +failed for that reason). Until the owner plants them (decision O11), the periodic `lock-refresh` and App-created tags cannot run; tags stay admin-only. There are *two* App slots on `hyperpolymath/standards`, one per duty (owner ruling 2026-10-08), and neither is OikosBot: * `vars.APP_ID` + `secrets.APP_PRIVATE_KEY` — the pin applier (contents, - workflows and pull-requests write). Read by `apply-workflow-pins.yml`, - `dependabot-lock-regen.yml` and `signed-push-smoke.yml`. + workflows and pull-requests write). Read by `apply-workflow-pins.yml` and + `dependabot-lock-regen.yml`. * `vars.RULESET_APP_ID` + `secrets.RULESET_APP_PRIVATE_KEY` — the ruleset applier (`administration: write`). Read by `tag-ruleset-canon.yml`. diff --git a/docs/SIGNING-POLICY.adoc b/docs/SIGNING-POLICY.adoc index d13a69cd4..8438fe2a8 100644 --- a/docs/SIGNING-POLICY.adoc +++ b/docs/SIGNING-POLICY.adoc @@ -17,8 +17,7 @@ it supersedes the sequencing in D90. gitbot-fleet, AI tools running unattended) | GitHub's web-flow **GPG** key | Write through the API so GitHub signs the commit itself: GraphQL - `createCommitOnBranch` or the estate - link:../.github/actions/signed-push/README.md[`signed-push`] action. Never + `createCommitOnBranch` (as `scripts/regen-dependabot-locks.sh` does). Never `git push` a locally made commit. A squash merge is *not* a way round this: see <>. @@ -50,7 +49,7 @@ These were each measured on default branches between 2026-07 and 2026-09. Do not * **The contents API (`PUT /repos/.../contents`) with a user OAuth token or PAT.** The commit is unsigned. Use `createCommitOnBranch` instead. * **`git push` from a workflow** using `GITHUB_TOKEN` or a PAT. The commit is - unsigned. Use `signed-push`. + unsigned. Use `createCommitOnBranch`. * **Bots pushing fixes onto PR branches** (CodeRabbit autofix, agent commit-suggestions). Turn that off; the bot suggests, and a human applies. * **A second machine or container without signing configured.** Configure it diff --git a/scripts/tests/signed-push-test.sh b/scripts/tests/signed-push-test.sh deleted file mode 100755 index 212ae792a..000000000 --- a/scripts/tests/signed-push-test.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# -# signed-push-test.sh — fixture suite for .github/actions/signed-push/push-verified.sh. -# -# A stub `gh` stands in for GitHub: it applies each createCommitOnBranch payload -# to a local bare "remote", refusing it when expectedHeadOid is not the branch -# head, exactly as the real mutation does. The suite checks that local commits -# arrive in order with their messages and byte-identical contents (binary -# included), that deletions and renames survive, and that every refusal -# (divergence, a moved head, merges, modes, symlinks) happens before anything -# is pushed or is reported as a partial push. -# -# Run: bash scripts/tests/signed-push-test.sh -set -uo pipefail -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -TARGET="${TARGET:-$SCRIPT_DIR/../../.github/actions/signed-push/push-verified.sh}" -WORK="$(mktemp -d)" -trap 'rm -rf "$WORK"' EXIT -pass=0; fail=0 - -# ok: record and print a passing assertion. -ok() { echo "PASS: $1"; pass=$((pass + 1)); } -# bad: record and print a failing assertion. -bad() { echo "FAIL: $1"; fail=$((fail + 1)); } - -export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid -export GITHUB_REPOSITORY=o/r - -# The stub: apply a createCommitOnBranch payload to $STUB_REMOTE, print the oid. -cat > "$WORK/gh" <<'STUB' -#!/usr/bin/env bash -set -euo pipefail -[ "$1 $2" = "api graphql" ] || { echo "stub: unexpected call: $*" >&2; exit 2; } -payload="$4" -echo x >> "$STUB_CALLS" -in=$(jq -c '.variables.in' "$payload") -branch=$(jq -r '.branch.branchName' <<<"$in") -want=$(jq -r '.expectedHeadOid' <<<"$in") -head=$(git -C "$STUB_REMOTE" rev-parse "refs/heads/$branch") -[ -n "${STUB_FAIL_ON_CALL:-}" ] && [ "$(wc -l < "$STUB_CALLS")" -eq "$STUB_FAIL_ON_CALL" ] && { - echo "stub: simulated outage" >&2; exit 1; } -[ "$want" = "$head" ] || { echo "Expected branch to point to $want but it did not" >&2; exit 1; } -wt=$(mktemp -d); git -C "$STUB_REMOTE" worktree add -q --detach "$wt" "$head" -jq -r '.fileChanges.deletions[].path' <<<"$in" | while IFS= read -r p; do rm -f "$wt/$p"; done -n=$(jq '.fileChanges.additions | length' <<<"$in") -for i in $(seq 0 $((n - 1))); do - p=$(jq -r ".fileChanges.additions[$i].path" <<<"$in"); mkdir -p "$wt/$(dirname "$p")" - jq -r ".fileChanges.additions[$i].contents" <<<"$in" | base64 -d > "$wt/$p" -done -msg=$(jq -r '.message.headline' <<<"$in"); body=$(jq -r '.message.body' <<<"$in") -git -C "$wt" add -A -git -C "$wt" -c user.name=github -c user.email=noreply@github.com commit -q --allow-empty -m "$msg" ${body:+-m "$body"} -new=$(git -C "$wt" rev-parse HEAD) -git -C "$STUB_REMOTE" update-ref "refs/heads/$branch" "$new" "$head" -git -C "$STUB_REMOTE" worktree remove --force "$wt" -echo "$new" -STUB -chmod +x "$WORK/gh" -export GH_BIN="$WORK/gh" - -# fresh: a bare remote with branch b, and a clone with b checked out. -fresh() { - rm -rf "$WORK/remote.git" "$WORK/clone"; : > "$WORK/calls" - git init -q --bare -b main "$WORK/remote.git" - git clone -q "$WORK/remote.git" "$WORK/clone" 2>/dev/null - ( cd "$WORK/clone" && printf 'keep\n' > keep.txt && printf 'old\n' > gone.txt && printf 'move me\n' > from.txt \ - && git add -A && git commit -q -m base && git push -q origin HEAD:refs/heads/b 2>/dev/null && git checkout -q -b b origin/b 2>/dev/null ) - export STUB_REMOTE="$WORK/remote.git" STUB_CALLS="$WORK/calls" -} -# calls: how many mutations the stub has received since the last fresh. -calls() { wc -l < "$WORK/calls" | tr -d " "; } -# run: push branch b from the clone, capturing all output in $WORK/out. -run() { ( cd "$WORK/clone" && bash "$TARGET" b ) > "$WORK/out" 2>&1; } - -# 1. The planted positive: three commits, every change shape. -fresh -( cd "$WORK/clone" || exit 1 - printf 'hello\n' > keep.txt && head -c 300 /dev/urandom > bin.dat && git add -A && git commit -q -m "one: text and binary" -m "a body line" - git rm -q gone.txt && git commit -q -m "two: delete" - git mv from.txt sub/to.txt 2>/dev/null || { mkdir sub; git mv from.txt sub/to.txt; } - git commit -q -m "three: rename" ) -LOCAL_TREE=$(git -C "$WORK/clone" rev-parse 'b^{tree}') -run; st=$? -[ "$st" = 0 ] && ok "three commits pushed" || bad "three commits pushed — $(cat "$WORK/out")" -[ "$(calls)" = 3 ] && ok "one mutation per commit" || bad "one mutation per commit (got $(calls))" -[ "$(git -C "$WORK/remote.git" rev-parse 'b^{tree}')" = "$LOCAL_TREE" ] && ok "remote tree identical, binary included" || bad "remote tree identical" -[ "$(git -C "$WORK/remote.git" log --format=%s b -3 | tr '\n' '|')" = "three: rename|two: delete|one: text and binary|" ] \ - && ok "messages kept, in order" || bad "messages kept, in order" -git -C "$WORK/remote.git" log --format=%b -1 'b~2' | grep -qx "a body line" && ok "commit body kept" || bad "commit body kept" -[ "$(git -C "$WORK/clone" rev-parse b)" = "$(git -C "$WORK/remote.git" rev-parse b)" ] && ok "local branch reset to the remote" || bad "local branch reset to the remote" -run && grep -q "nothing to push" "$WORK/out" && [ "$(calls)" = 3 ] && ok "second run pushes nothing" || bad "second run pushes nothing" - -# 2. Diverged remote: refused before any mutation. -fresh -( cd "$WORK/clone" && printf 'x\n' > a.txt && git add a.txt && git commit -q -m local ) -( d=$(mktemp -d) && git clone -q -b b "$WORK/remote.git" "$d" 2>/dev/null && cd "$d" && printf 'y\n' > other.txt && git add other.txt && git commit -q -m elsewhere && git push -q origin b 2>/dev/null ) -run; st=$? -[ "$st" != 0 ] && grep -q "has commits that b lacks" "$WORK/out" && [ "$(calls)" = 0 ] && ok "diverged remote refused, nothing sent" || bad "diverged remote refused — $(cat "$WORK/out")" - -# 3. The head moves mid-push: the second mutation is refused, reported as partial. -fresh -( cd "$WORK/clone" && for i in 1 2; do printf '%s\n' "$i" > n.txt && git add n.txt && git commit -q -m "c$i"; done ) -STUB_FAIL_ON_CALL=2 run; st=$? -[ "$st" != 0 ] && grep -q "PARTIAL PUSH: 1 of 2" "$WORK/out" && ok "failure after one commit reported as a partial push" || bad "partial push reported — $(cat "$WORK/out")" -unset STUB_FAIL_ON_CALL - -# 4. Shapes createCommitOnBranch cannot express are refused before any mutation. -for shape in exec symlink modechange merge; do - fresh - ( cd "$WORK/clone" || exit 1 - case $shape in - exec) printf '#!/bin/sh\n' > run.sh && chmod +x run.sh && git add run.sh && git commit -q -m exec ;; - symlink) ln -s keep.txt link && git add link && git commit -q -m symlink ;; - modechange) chmod +x keep.txt && git add keep.txt && git commit -q -m mode ;; - merge) git checkout -q -b side && printf 's\n' > s.txt && git add s.txt && git commit -q -m side \ - && git checkout -q b && printf 't\n' > t.txt && git add t.txt && git commit -q -m main-side \ - && git merge -q --no-edit side >/dev/null ;; - esac ) - run; st=$? - [ "$st" != 0 ] && [ "$(calls)" = 0 ] && [ "$(git -C "$WORK/remote.git" rev-parse b)" = "$(git -C "$WORK/clone" rev-parse origin/b)" ] \ - && ok "$shape refused before anything is pushed" || bad "$shape refused — $(cat "$WORK/out")" -done - -echo "signed-push-test: $pass passed, $fail failed" -[ "$fail" -eq 0 ]