diff --git a/.github/workflows/abi-contract.yml b/.github/workflows/abi-contract.yml index 5c65495..536ec5c 100644 --- a/.github/workflows/abi-contract.yml +++ b/.github/workflows/abi-contract.yml @@ -32,7 +32,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Zig 0.15.2 # Pinned + sha256-verified tarball; no third-party action (#103). @@ -75,7 +75,7 @@ jobs: image: ghcr.io/stefan-hoeck/idris2-pack@sha256:370e2ab066251cf278ac6928d9201ade0aca70c4e7ee1cc434d25bcea1669b29 steps: - name: Checkout - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Zig download prerequisites in the Idris image run: | @@ -95,7 +95,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Symbol-level contract only (pure grep/comm — no toolchain): every # extern in src/ui/tea/gsa_ffi.affine must be a real Zig export, and diff --git a/.github/workflows/actions-lock.yml b/.github/workflows/actions-lock.yml new file mode 100644 index 0000000..d4e0ec8 --- /dev/null +++ b/.github/workflows/actions-lock.yml @@ -0,0 +1,419 @@ +# This workflow is managed by gh actions-lock. +# SPDX-License-Identifier: MPL-2.0 +# +# actions-lock.yml — keep .github/workflows/actions.lock in step with the +# workflows themselves. +# +# Why this exists: an out-of-date lockfile is not a soft failure. Every +# workflow listed in actions.lock dies with `startup_failure` before a single +# step runs, so it produces no check runs at all — which is how +# game-server-admin#103 took out both required-check producers (`ABI Contract` +# and `Cross-Platform Build & Test`) in one commit. Dependabot bumps action +# tags weekly and cannot run `gh actions-lock`, so without a gate here the next +# group bump re-breaks the whole CI estate, silently. +# +# The lockfile is not decoration for another reason: this repo's Actions policy +# requires every action to be pinned to a full-length commit SHA, and a tag ref +# such as actions/checkout@v7.0.1 is accepted *only because the lockfile pins +# it*. Delete the lockfile and even actions/checkout is refused. +# +# The gate has two halves: +# verify — read-only (`gh actions-lock --verify`). Fails when the lockfile +# and the workflows disagree, so a bump cannot merge unrelocked. +# relock — runs when verify asked it to. Regenerates the lockfile and +# delivers it: pushed straight back to the branch on a pull +# request, or opened as a PR when the target is the default branch +# (main requires signed commits, so a bot cannot push there). +# +# A push made with GITHUB_TOKEN does not start a new workflow run, so the +# relock commit cannot loop. + +name: Actions Lockfile + +on: + pull_request: + # NO `push: branches: [main, master]`, deliberately — see the header block. + # A push to the protected branch makes this a protected-branch-triggered + # workflow, and Hypatia then refuses `egress-policy: audit` as + # non-containment, which is the one hardening step this file can carry + # without an endpoint allow-list nobody can write correctly. The default + # branch is still covered: `schedule` runs against it, and every route to + # it goes through a pull request that this gate blocks. + schedule: + # Mondays 04:17 UTC, offset off the hour: the estate's other scheduled + # jobs cluster on :00 and contend for the same rate-limit bucket. + - cron: '17 4 * * 1' + workflow_dispatch: + inputs: + mode: + description: 'verify = report only; relock = regenerate and deliver' + required: false + default: 'verify' + type: choice + options: + - verify + - relock + +permissions: + contents: read + +concurrency: + group: actions-lockfile-${{ github.ref }} + cancel-in-progress: false + +env: + # Bump deliberately: the lockfile format is pre-1.0 and its shape can + # change between releases. + ACTIONS_LOCK_EXTENSION_VERSION: v0.1.7-rc.1 + +jobs: + verify: + name: Verify actions.lock + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + outputs: + # `relock` is computed in the shell, not in an `if:` expression: + # `inputs.mode` is not a recognised named value outside + # workflow_dispatch, and referencing it there is a start-up error. + in_sync: ${{ steps.check.outputs.in_sync }} + relock: ${{ steps.check.outputs.relock }} + steps: + # Egress audit: Hypatia's RE001 asks for it on any job that reaches for + # secrets.*, and both jobs here pass GITHUB_TOKEN to gh. + # Egress audit: Hypatia's RE001 asks for it on any job that reaches for + # secrets.*, and both jobs here pass GITHUB_TOKEN to gh. Audit, not + # block — see the trigger note in the header. + - name: Harden runner (egress audit) + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + egress-policy: audit + + - name: Checkout + id: checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install gh actions-lock + id: install + run: | + set -euo pipefail + gh extension install github/gh-actions-lock --pin "$ACTIONS_LOCK_EXTENSION_VERSION" + gh actions-lock --help + + - name: Check the lockfile matches the workflows + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + DISPATCH_MODE: ${{ github.event.inputs.mode }} + run: | + # The runner's default shell is `bash -e {0}`, and `set -uo pipefail` + # does NOT clear errexit — so every step that inspects an exit code + # has to switch it off explicitly, or a failing command aborts the + # script before the handler below it can report anything. + set -uo pipefail + set +e + code=0 + gh actions-lock --verify --no-interactive \ + --json=valid,findings >actions-lock-report.json 2>actions-lock-report.err \ + || code=$? + echo "gh actions-lock --verify exited ${code}" | tee -a "$GITHUB_STEP_SUMMARY" + + if [ "$code" -eq 0 ]; then + echo "in_sync=true" >> "$GITHUB_OUTPUT" + # An explicit relock dispatch still wins even when the file looks + # fine: it is how a maintainer refreshes pins that have moved. + if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$DISPATCH_MODE" = "relock" ]; then + echo "relock=true" >> "$GITHUB_OUTPUT" + else + echo "relock=false" >> "$GITHUB_OUTPUT" + fi + echo "actions.lock agrees with every workflow." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + if [ "$code" -ne 1 ]; then + # 1 = blocking findings (out of sync). Anything else is the tool + # failing, not the repo: do not let the relock job paper over it. + # Re-emit the tool's stderr as annotations — a bare exit code on a + # runner nobody can read logs from is worse than a verbose diff. + echo "in_sync=unknown" >> "$GITHUB_OUTPUT" + echo "relock=false" >> "$GITHUB_OUTPUT" + echo "::error::gh actions-lock --verify exited ${code} (tool failure, not a lockfile mismatch)" + while IFS= read -r line; do + if [ -n "$line" ]; then echo "::error::[gh actions-lock] ${line}"; fi + done < <(tail -n 30 actions-lock-report.err | tr -d '\r' | cut -c1-200) + exit "$code" + fi + + echo "in_sync=false" >> "$GITHUB_OUTPUT" + echo "relock=true" >> "$GITHUB_OUTPUT" + echo "::error::actions.lock is out of step with .github/workflows — every onboarded workflow will fail with startup_failure until it is regenerated" + { + echo '## actions.lock is out of sync' + echo + echo '```json' + jq . actions-lock-report.json 2>/dev/null || cat actions-lock-report.json + echo '```' + echo + echo 'Fix locally with `gh actions-lock`, or run this workflow in `relock` mode.' + } >> "$GITHUB_STEP_SUMMARY" + exit 1 + + relock: + name: Regenerate actions.lock + needs: verify + if: always() && needs.verify.outputs.relock == 'true' + runs-on: ubuntu-latest + timeout-minutes: 20 + # The write lives here, on the only job that writes. `contents: write` is + # what lets the PAT-backed push land, and `pull-requests: write` is what + # lets the fallback publish the regenerated file as a comment. + permissions: + contents: write + pull-requests: write + steps: + - name: Decide where the fix goes + id: target + env: + EVENT_NAME: ${{ github.event_name }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + REPO: ${{ github.repository }} + REF: ${{ github.ref }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + RELOCK_TOKEN: ${{ secrets.ACTIONS_LOCK_TOKEN }} + run: | + set -euo pipefail + if [ -n "${RELOCK_TOKEN:-}" ]; then + echo "has_token=true" >> "$GITHUB_OUTPUT" + else + echo "has_token=false" >> "$GITHUB_OUTPUT" + fi + if [ "$EVENT_NAME" = "pull_request" ]; then + fork=no + if [ "$HEAD_REPO" != "$REPO" ]; then fork=yes; fi + echo "skipped=false" >> "$GITHUB_OUTPUT" + echo "fork=${fork}" >> "$GITHUB_OUTPUT" + echo "branch=${HEAD_REF}" >> "$GITHUB_OUTPUT" + echo "on_default=false" >> "$GITHUB_OUTPUT" + else + branch="${REF#refs/heads/}" + echo "skipped=false" >> "$GITHUB_OUTPUT" + echo "fork=no" >> "$GITHUB_OUTPUT" + echo "branch=${branch}" >> "$GITHUB_OUTPUT" + if [ "$branch" = "$DEFAULT_BRANCH" ]; then + # main requires signed commits, so a bot commit cannot land + # there even with a token: deliver the fix as a pull request. + echo "on_default=true" >> "$GITHUB_OUTPUT" + else + echo "on_default=false" >> "$GITHUB_OUTPUT" + fi + fi + + # Egress audit: Hypatia's RE001 asks for it on any job that reaches for + # secrets.*, and both jobs here pass GITHUB_TOKEN to gh. + # Egress audit: Hypatia's RE001 asks for it on any job that reaches for + # secrets.*, and both jobs here pass GITHUB_TOKEN to gh. Audit, not + # block — see the trigger note in the header. + - name: Harden runner (egress audit) + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + egress-policy: audit + + - name: Checkout the branch that needs the lockfile + id: checkout-lock + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ steps.target.outputs.branch }} + persist-credentials: false + + - name: Install gh actions-lock + id: install + run: | + set -euo pipefail + gh extension install github/gh-actions-lock --pin "$ACTIONS_LOCK_EXTENSION_VERSION" + + # NOT --relock. --relock means "bump moved branch/version refs to their + # current upstream SHA", so it upgrades every ref to latest and writes a + # lockfile keyed by TAG — which is the wrong shape for a repo whose + # workflows are SHA-pinned. The estate's validator keys on + # `owner/repo@<40-hex>` exactly as written in the workflow, so a + # tag-keyed lockfile contradicts every workflow in the repo and GitHub + # rejects the lot at startup with "The lockfile could not be validated." + # + # Plain fix mode with --no-migrate-local-actions --no-narrow is the + # canonical regeneration: it keys each entry by the ref the workflow + # actually names. See hyperpolymath/standards + # .githooks/validate-actions-lock.sh. + - name: Regenerate the lockfile + id: regenerate + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -uo pipefail + set +e + code=0 + gh actions-lock --no-migrate-local-actions --no-narrow --no-interactive \ + >relock.out 2>relock.err || code=$? + cat relock.out || true + cat relock.err || true + # A non-zero exit here does NOT mean nothing was written. The tool + # writes the lockfile and then reports whatever it still objects to + # (a bare SHA with no symbolic ref, say) — findings that do not + # invalidate the file. "The file did not change" is the real + # failure, and that is checked below. + if [ "$code" -ne 0 ]; then + echo "::warning::gh actions-lock exited ${code}; the regenerated lockfile is still delivered if it changed" + while IFS= read -r line; do + if [ -n "$line" ]; then echo "::warning::[gh actions-lock] ${line}"; fi + done < <(tail -n 30 relock.err | tr -d '\r' | cut -c1-200) + fi + + # Regenerating the lockfile must not rewrite the workflows. If it + # did, the file it just wrote describes a tree that is not on disk — + # name the files, then put them back and keep only the lockfile. + touched="$(git status --porcelain -- '.github/workflows/*.yml' '.github/workflows/*.yaml' || true)" + if [ -n "$touched" ]; then + echo "::warning::gh actions-lock modified workflow files while regenerating the lockfile; they have been reverted and only actions.lock is kept" + while IFS= read -r line; do + if [ -n "$line" ]; then echo "::warning::[workflow touched] ${line}"; fi + done <<< "$touched" + git checkout -- .github/workflows || true + fi + + if git diff --quiet -- .github/workflows/actions.lock; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "::error::gh actions-lock produced no change to actions.lock — the branch stays unstartable and the file must be regenerated by hand" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + git --no-pager diff --stat -- .github/workflows/actions.lock || true + fi + + - name: Upload the regenerated lockfile + id: upload + if: steps.regenerate.outputs.changed == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: actions-lock-regenerated + path: .github/workflows/actions.lock + if-no-files-found: error + retention-days: 14 + + # A GitHub App cannot write under .github/workflows/ at all: `permissions:` + # has no `workflows` key, and a GITHUB_TOKEN push is rejected outright with + # "refusing to allow a GitHub App to create or update workflow + # `.github/workflows/actions.lock` without `workflows` permission". So the + # push path needs a PAT (fine-grained, Workflows + Contents write) supplied + # as ACTIONS_LOCK_TOKEN. Without it the file is still delivered — as an + # artefact and, on a pull request, as a comment anyone can apply. + - name: Deliver the fix with a PAT + id: deliver + if: steps.regenerate.outputs.changed == 'true' && steps.target.outputs.has_token == 'true' + env: + RELOCK_TOKEN: ${{ secrets.ACTIONS_LOCK_TOKEN }} + GH_TOKEN: ${{ secrets.ACTIONS_LOCK_TOKEN }} + BRANCH: ${{ steps.target.outputs.branch }} + run: | + set -uo pipefail + set +e + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git remote set-url origin "https://x-access-token:${RELOCK_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + code=0 + git commit --only -m "chore(ci): regenerate actions.lock (gh actions-lock --relock) + + The lockfile and the workflows had drifted apart. While they do, + every onboarded workflow ends in startup_failure and produces no + check runs at all. See game-server-admin#103." -- .github/workflows/actions.lock >commit.out 2>&1 \ + || code=$? + if [ "$code" -ne 0 ]; then + cat commit.out || true + while IFS= read -r line; do + if [ -n "$line" ]; then echo "::error::[git commit] ${line}"; fi + done < <(tail -n 20 commit.out | tr -d '\r' | cut -c1-200) + exit 1 + fi + if [ "${{ steps.target.outputs.on_default }}" = "true" ]; then + branch="ci/actions-lock-relock-${GITHUB_RUN_ID}" + git switch --create "$branch" >/dev/null 2>&1 + code=0 + git push --set-upstream origin "$branch" >push.out 2>&1 || code=$? + if [ "$code" -ne 0 ]; then + cat push.out || true + while IFS= read -r line; do + if [ -n "$line" ]; then echo "::error::[git push] ${line}"; fi + done < <(tail -n 20 push.out | tr -d '\r' | cut -c1-200) + exit 1 + fi + gh pr create --base "$BRANCH" --head "$branch" \ + --title 'chore(ci): regenerate actions.lock' \ + --body '`gh actions-lock --verify` failed because `actions.lock` no longer matches the workflows. While they disagree, every onboarded workflow ends in `startup_failure` and produces no check runs at all. This is the output of `gh actions-lock --relock`. See game-server-admin#103.' + else + code=0 + git push origin "HEAD:refs/heads/${BRANCH}" >push.out 2>&1 || code=$? + if [ "$code" -ne 0 ]; then + cat push.out || true + while IFS= read -r line; do + if [ -n "$line" ]; then echo "::error::[git push] ${line}"; fi + done < <(tail -n 20 push.out | tr -d '\r' | cut -c1-200) + exit 1 + fi + echo "::notice::Regenerated actions.lock pushed to ${BRANCH}; this pull request will re-verify on the next run." + fi + + - name: Publish the regenerated lockfile on the pull request + id: publish + if: always() && steps.regenerate.outputs.changed == 'true' && github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + RELOCK_TOKEN: ${{ secrets.ACTIONS_LOCK_TOKEN }} + run: | + set -uo pipefail + set +e + body="$(mktemp)" + { + echo '## regenerated `.github/workflows/actions.lock`' + echo + echo '`gh actions-lock --verify` failed on this pull request: the lockfile and the workflows disagree, so every onboarded workflow ends in `startup_failure` and produces no check runs at all.' + echo + if [ -n "${RELOCK_TOKEN:-}" ]; then + echo 'The workflow tried to deliver this automatically; see the `deliver` step for whether it landed.' + else + echo 'Automatic delivery is not configured: a GitHub App cannot write under `.github/workflows/` (`permissions:` has no `workflows` key), so pushing the fix needs `ACTIONS_LOCK_TOKEN` — a fine-grained PAT with Contents and Workflows write. Until that secret exists, apply the file below by hand:' + echo + echo '```sh' + echo 'gh run download ${{ github.run_id }} -n actions-lock-regenerated' + echo 'mv actions.lock .github/workflows/actions.lock && gh actions-lock --verify' + echo '```' + fi + echo + echo '
actions.lock' + echo + echo '```yaml' + cat .github/workflows/actions.lock + echo '```' + echo + echo '
' + } > "$body" + gh pr comment "$PR_NUMBER" --body-file "$body" + + - name: Diagnostics + if: always() + run: | + set -uo pipefail + set +e + echo "--- git state ---" + git --no-pager log --oneline -1 || true + git status --short || true + outcomes="target=${{ steps.target.outcome }} checkout=${{ steps.checkout-lock.outcome }} install=${{ steps.install.outcome }} regenerate=${{ steps.regenerate.outcome }} upload=${{ steps.upload.outcome }} deliver=${{ steps.deliver.outcome }} publish=${{ steps.publish.outcome }}" + echo "::notice::relock outcomes — ${outcomes}" + { + echo '## Regenerate actions.lock — step outcomes' + echo + echo '```' + echo "${outcomes}" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index d562bb6..1bd5601 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -4,258 +4,144 @@ version: 'v0.0.2' workflows: '.github/workflows/abi-contract.yml': - - 'actions/checkout@v4.2.2' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + '.github/workflows/actions-lock.yml': + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + - 'step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1' '.github/workflows/boj-build.yml': - - 'actions/checkout@v4.1.7' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/casket-pages.yml': - - 'actions/cache@v4.3.0' - - 'actions/checkout@v4.1.1' - - 'actions/configure-pages@v5.0.0' - - 'actions/deploy-pages@v4.0.5' - - 'actions/upload-pages-artifact@v3.0.1' - - 'haskell-actions/setup@v2.7.5' + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d' + - 'actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346' + - 'actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9' '.github/workflows/codeql.yml': - - 'actions/checkout@v6.0.2' - - 'github/codeql-action@v4.34.0' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'github/codeql-action@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2' '.github/workflows/cross-platform.yml': - - 'actions/cache@v4.2.0' - - 'actions/checkout@v4.2.2' + - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/dogfood-gate.yml': - - 'actions/checkout@v4.3.1' - - 'hyperpolymath/deed-ecosystem@main' - - 'hyperpolymath/k9-ecosystem@main' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'hyperpolymath/deed-ecosystem@3e69929a4b0b5610b477732ee125a156cbc8a040' + - 'hyperpolymath/k9-ecosystem@20f6be5b5a14a48680b236955b5c4ad9033d00d4' '.github/workflows/governance.yml': [] '.github/workflows/hypatia-scan.yml': - - 'actions/checkout@v6.0.2' - - 'actions/github-script@v8.0.0' - - 'actions/upload-artifact@v4.6.2' - - 'erlef/setup-beam@v1.24.0' - - 'github/codeql-action@v4.32.6' - '.github/workflows/instant-sync.yml': - - 'peter-evans/repository-dispatch@v4.0.1' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3' + - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' + - 'github/codeql-action@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2' + '.github/workflows/instant-sync.yml': [] '.github/workflows/label-triage.yml': [] '.github/workflows/labels.yml': [] '.github/workflows/mirror.yml': [] '.github/workflows/push-email-notify.yml': - - 'hyperpolymath/smtp-notify-action@v0.2.0' + - 'hyperpolymath/smtp-notify-action@22e7bdb322c430c1d0dac6b3bb307f4bb139d0be' '.github/workflows/release.yml': - - 'actions/attest-build-provenance@v2.4.0' - - 'actions/checkout@v6.0.2' - - 'actions/download-artifact@v4.3.0' - - 'actions/upload-artifact@v4.6.2' - - 'softprops/action-gh-release@v2.5.0' + - 'actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' + - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + - 'step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1' '.github/workflows/rhodibot.yml': - - 'actions/checkout@v7.0.1' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/scorecard.yml': [] '.github/workflows/secret-scanner.yml': [] '.github/workflows/static-analysis-gate.yml': - - 'actions/checkout@v6.0.2' - - 'actions/download-artifact@v4.1.8' - - 'actions/upload-artifact@v4.6.2' - - 'erlef/setup-beam@v1.20.4' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' + - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' + - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' dependencies: - 'actions/attest-build-provenance@1176ef556905f349f669722abf30bce1a6e16e01': - ref: 'predicate@1.1.5' - commit: 'sha1-1176ef556905f349f669722abf30bce1a6e16e01' - owner_id: 44036562 - repo_id: 760702757 - 'actions/attest-build-provenance@v2.4.0': - ref: 'v2.4.0' - commit: 'sha1-e8998f949152b193b063cb0ec769d69d929409be' + 'actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8': + ref: 'v4.2.2' + commit: 'sha1-4d101475d8b20a2381f78447822ac1eab6504dd8' owner_id: 44036562 repo_id: 760702757 uses: - - 'actions/attest-build-provenance@1176ef556905f349f669722abf30bce1a6e16e01' - - 'actions/attest@ce27ba3b4a9a139d9a20a4a07d69fabb52f1e5bc' - 'actions/attest@ce27ba3b4a9a139d9a20a4a07d69fabb52f1e5bc': - ref: 'v2.4.0' - commit: 'sha1-ce27ba3b4a9a139d9a20a4a07d69fabb52f1e5bc' + - 'actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d' + 'actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d': + ref: 'v4.2.1' + commit: 'sha1-508db95dd578ae2727ebd6217d5ba78e4fbda05d' owner_id: 44036562 repo_id: 760701061 - 'actions/cache@v4.2.0': - ref: 'v4.2.0' - commit: 'sha1-1bd1e32a3bdc45362d1e726936510720a7c30a57' - owner_id: 44036562 - repo_id: 215566462 - 'actions/cache@v4.3.0': - ref: 'v4.3.0' - commit: 'sha1-0057852bfaa89a56745cba8c7296529d2fc39830' + 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9': + ref: 'v6.1.0' + commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' owner_id: 44036562 repo_id: 215566462 - 'actions/checkout@v4.1.1': - ref: 'v4.1.1' - commit: 'sha1-b4ffde65f46336ab88eb53be808477a3936bae11' - owner_id: 44036562 - repo_id: 197814629 - 'actions/checkout@v4.1.7': - ref: 'v4.1.7' - commit: 'sha1-692973e3d937129bcbf40652eb9f2f61becf3332' - owner_id: 44036562 - repo_id: 197814629 - 'actions/checkout@v4.2.2': - ref: 'v4.2.2' - commit: 'sha1-11bd71901bbe5b1630ceea73d27597364c9af683' - owner_id: 44036562 - repo_id: 197814629 - 'actions/checkout@v4.3.1': - ref: 'v4.3.1' - commit: 'sha1-34e114876b0b11c390a56381ad16ebd13914f8d5' - owner_id: 44036562 - repo_id: 197814629 - 'actions/checkout@v6.0.2': - ref: 'v6.0.2' - commit: 'sha1-de0fac2e4500dabe0009e67214ff5f5447ce83dd' - owner_id: 44036562 - repo_id: 197814629 - 'actions/checkout@v7.0.1': + 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1': ref: 'v7.0.1' commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' owner_id: 44036562 repo_id: 197814629 - 'actions/configure-pages@v5.0.0': - ref: 'v5.0.0' - commit: 'sha1-983d7736d9b0ae728b81ab479565c72886d7745b' + 'actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d': + ref: 'v6.0.0' + commit: 'sha1-45bfe0192ca1faeb007ade9deae92b16b8254a0d' owner_id: 44036562 repo_id: 513659658 - 'actions/deploy-pages@v4.0.5': - ref: 'v4.0.5' - commit: 'sha1-d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e' + 'actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346': + ref: 'v5.0.1' + commit: 'sha1-368f82528645a54fb793d4d04e342629a3f51346' owner_id: 44036562 repo_id: 438112499 - 'actions/download-artifact@v4.1.8': - ref: 'v4.1.8' - commit: 'sha1-fa0a91b85d4f404e444e00e005971372dc801d16' + 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c': + ref: 'v8.0.1' + commit: 'sha1-3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' owner_id: 44036562 repo_id: 192626254 - 'actions/download-artifact@v4.3.0': - ref: 'v4.3.0' - commit: 'sha1-d3f86a106a0bac45b974a628896c90dbdf5c8093' - owner_id: 44036562 - repo_id: 192626254 - 'actions/github-script@v8.0.0': - ref: 'v8.0.0' - commit: 'sha1-ed597411d8f924073f98dfc5c65a23a2325f34cd' + 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3': + ref: 'v9.0.0' + commit: 'sha1-3a2844b7e9c422d3c10d287c895573f7108da1b3' owner_id: 44036562 repo_id: 205262760 - 'actions/upload-artifact@v4': - ref: 'v4' - commit: 'sha1-ea165f8d65b6e75b540449e92b4886f43607fa02' + 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a': + ref: 'v7.0.1' + commit: 'sha1-043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' owner_id: 44036562 repo_id: 192625955 - 'actions/upload-artifact@v4.6.2': - ref: 'v4.6.2' - commit: 'sha1-ea165f8d65b6e75b540449e92b4886f43607fa02' + 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f': + ref: 'v7.0.0' + commit: 'sha1-bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' owner_id: 44036562 repo_id: 192625955 - 'actions/upload-pages-artifact@v3.0.1': - ref: 'v3.0.1' - commit: 'sha1-56afc609e74202658d3ffba0e8f6dda462b719fa' + 'actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9': + ref: 'v5.0.0' + commit: 'sha1-fc324d3547104276b827a68afc52ff2a11cc49c9' owner_id: 44036562 repo_id: 496012378 uses: - - 'actions/upload-artifact@v4' - 'erlef/setup-beam@v1.20.4': - ref: 'v1.20.4' - commit: 'sha1-e6d7c94229049569db56a7ad5a540c051a010af9' - owner_id: 47606891 - repo_id: 331103973 - 'erlef/setup-beam@v1.24.0': - ref: 'v1.24.0' - commit: 'sha1-fc68ffb90438ef2936bbb3251622353b3dcb2f93' + - 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f' + 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': + ref: 'v1.24.1' + commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' owner_id: 47606891 repo_id: 331103973 - 'github/codeql-action@v4.32.6': - ref: 'v4.32.6' - commit: 'sha1-0d579ffd059c29b07949a3cce3983f0780820c98' + 'github/codeql-action@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2': + ref: 'v4.38.2' + commit: 'sha1-2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2' owner_id: 9919 repo_id: 259445878 - 'github/codeql-action@v4.34.0': - ref: 'v4.34.0' - commit: 'sha1-c6f931105cb2c34c8f901cc885ba1e2e259cf745' - owner_id: 9919 - repo_id: 259445878 - 'haskell-actions/setup@v2.7.5': - ref: 'v2.7.5' - commit: 'sha1-ec49483bfc012387b227434aba94f59a6ecd0900' - owner_id: 75048950 - repo_id: 623796603 - 'hyperpolymath/deed-ecosystem@main': + 'hyperpolymath/deed-ecosystem@3e69929a4b0b5610b477732ee125a156cbc8a040': ref: 'main' - commit: 'sha1-aa4b836bd969df2bc58128cb8e3d20bbc88d5e79' + commit: 'sha1-3e69929a4b0b5610b477732ee125a156cbc8a040' owner_id: 6759885 repo_id: 1275649586 - 'hyperpolymath/k9-ecosystem@main': + 'hyperpolymath/k9-ecosystem@20f6be5b5a14a48680b236955b5c4ad9033d00d4': ref: 'main' - commit: 'sha1-89f3c2702f4f650a92aa7411502f38da06abd562' + commit: 'sha1-20f6be5b5a14a48680b236955b5c4ad9033d00d4' owner_id: 6759885 repo_id: 1275650185 - 'hyperpolymath/smtp-notify-action@v0.2.0': - ref: 'v0.2.0' - commit: 'sha1-ede1191ef6ff3ac02c4f4d9efdf837ee517e11d7' + 'hyperpolymath/smtp-notify-action@22e7bdb322c430c1d0dac6b3bb307f4bb139d0be': + ref: 'v0.3.0' + commit: 'sha1-22e7bdb322c430c1d0dac6b3bb307f4bb139d0be' owner_id: 6759885 repo_id: 1352485172 - 'peter-evans/repository-dispatch@v4.0.1': - ref: 'v4.0.1' - commit: 'sha1-28959ce8df70de7be546dd1250a005dd32156697' - owner_id: 18365890 - repo_id: 220359305 - 'softprops/action-gh-release@v2.5.0': - ref: 'v2.5.0' - commit: 'sha1-a06a81a03ee405af7f2048a818ed3f03bbf83c7b' - owner_id: 2242 - repo_id: 204253808 - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9': - ref: 'v6.1.0' - commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' - owner_id: 44036562 - repo_id: 215566462 - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1': - ref: 'v7.0.1' - commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' - owner_id: 44036562 - repo_id: 197814629 - 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a': - ref: 'v7.0.1' - commit: 'sha1-043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' - owner_id: 44036562 - repo_id: 192625955 - 'actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08': - ref: 'v4.6.0' - commit: 'sha1-65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08' - owner_id: 44036562 - repo_id: 192625955 - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed': - ref: 'v2.0.5' - commit: 'sha1-22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' - owner_id: 42048915 - repo_id: 356423100 - 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772': - ref: 'stable' - commit: 'sha1-6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' - owner_id: 1940490 - repo_id: 260749683 - 'editorconfig-checker/action-editorconfig-checker@840e866d93b8e032123c23bac69dece044d4d84c': - ref: 'v2.2.0' - commit: 'sha1-840e866d93b8e032123c23bac69dece044d4d84c' - owner_id: 26415196 - repo_id: 297874902 - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124': - ref: 'v1.24.1' - commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' - owner_id: 47606891 - repo_id: 331103973 - 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc': - ref: 'v2.4.4' - commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' - owner_id: 67707773 - repo_id: 421101922 - 'softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda': - ref: 'v2.2.1' - commit: 'sha1-c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda' - owner_id: 2242 - repo_id: 204253808 - 'webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555': - ref: 'v0.10.0' - commit: 'sha1-e83874834305fe9a4a2997156cb26c5de65a8555' - owner_id: 135788 - repo_id: 208510314 + 'step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1': + ref: 'v2.21.1' + commit: 'sha1-e14015d583714f6e62063499dc959a02595150a1' + owner_id: 88700172 + repo_id: 422287306 diff --git a/.github/workflows/boj-build.yml b/.github/workflows/boj-build.yml index c3ad3dc..9fe3fb4 100644 --- a/.github/workflows/boj-build.yml +++ b/.github/workflows/boj-build.yml @@ -18,7 +18,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Trigger BoJ Server (Casket/ssg-mcp) env: BOJ_URL: ${{ secrets.BOJ_SERVER_URL || vars.BOJ_SERVER_URL }} diff --git a/.github/workflows/casket-pages.yml b/.github/workflows/casket-pages.yml index 8746d9f..b5601b9 100644 --- a/.github/workflows/casket-pages.yml +++ b/.github/workflows/casket-pages.yml @@ -20,31 +20,49 @@ concurrency: jobs: build: runs-on: ubuntu-latest - timeout-minutes: 30 + # casket-ssg pulls in pandoc, a from-source build. Measured on main: run + # 35361659693 took 1h40m before failing. 30 minutes was never going to be + # enough. + timeout-minutes: 120 steps: - name: Checkout - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Checkout casket-ssg - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: hyperpolymath/casket-ssg path: .casket-ssg - - name: Setup GHCup - uses: haskell-actions/setup@v2.12.1 - with: - ghc-version: '9.8.2' - cabal-version: '3.10' + # MEASURED, not assumed. A throwaway probe workflow on this branch + # (since deleted) loaded both actions and GitHub refused both in one + # message: + # + # The actions editorconfig-checker/action-editorconfig-checker@51f6331 + # and haskell-actions/setup@0f8e8c99 are not allowed in + # hyperpolymath/game-server-admin because all actions must be from a + # repository owned by hyperpolymath, created by GitHub, or verified in + # the GitHub Marketplace. + # + # So the toolchain is installed from a pinned, hash-verified script — + # the same remedy used for setup-zig in scripts/install-zig.sh. + - name: Setup GHC and cabal + run: bash scripts/setup-haskell.sh + + # The checked-out casket-ssg tracks its default branch, so hashing only + # the .cabal file can restore a store built against different source. + - name: Record casket-ssg revision + id: casket + run: echo "sha=$(git -C .casket-ssg rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Cache Cabal - uses: actions/cache@v6.1.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cabal/packages ~/.cabal/store .casket-ssg/dist-newstyle - key: ${{ runner.os }}-casket-${{ hashFiles('.casket-ssg/casket-ssg.cabal') }} + key: ${{ runner.os }}-casket-9.8.2-${{ steps.casket.outputs.sha }} - name: Build casket-ssg working-directory: .casket-ssg @@ -100,10 +118,10 @@ jobs: touch ../_site/.nojekyll - name: Setup Pages - uses: actions/configure-pages@v6.0.0 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Upload artifact - uses: actions/upload-pages-artifact@v5.0.0 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: '_site' @@ -117,4 +135,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v5.0.1 + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3e3d82f..dac3dd8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -42,15 +42,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Initialize CodeQL - uses: github/codeql-action/init@v4.38.2 + uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.38.2 + uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/cross-platform.yml b/.github/workflows/cross-platform.yml index 9220ca3..4d688d3 100644 --- a/.github/workflows/cross-platform.yml +++ b/.github/workflows/cross-platform.yml @@ -33,14 +33,14 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install Zig 0.15.2 # Pinned + sha256-verified tarball; no third-party action (#103). run: bash scripts/install-zig.sh - name: Cache Zig - uses: actions/cache@v6.1.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cache/zig diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index bda69e5..51c1e8f 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check for manifest files (.a2ml/.deed) id: detect @@ -41,7 +41,7 @@ jobs: - name: Validate DEED manifests if: steps.detect.outputs.count > 0 - uses: hyperpolymath/deed-ecosystem/validate-action@main + uses: hyperpolymath/deed-ecosystem/validate-action@3e69929a4b0b5610b477732ee125a156cbc8a040 # main @ 2026-10-04 with: path: '.' strict: 'false' @@ -73,7 +73,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check for K9 files id: detect @@ -90,7 +90,7 @@ jobs: - name: Validate K9 contracts if: steps.detect.outputs.k9_count > 0 - uses: hyperpolymath/k9-ecosystem/validate-action@main + uses: hyperpolymath/k9-ecosystem/validate-action@20f6be5b5a14a48680b236955b5c4ad9033d00d4 # main @ 2026-10-04 with: path: '.' strict: 'false' @@ -123,7 +123,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Scan for invisible characters id: lint @@ -188,7 +188,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check for Groove manifest id: groove @@ -247,7 +247,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check and validate eclexiaiser manifest id: eclex @@ -313,7 +313,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Generate dogfooding scorecard run: | diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index eaad1cb..6cdff30 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -49,12 +49,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # Full history for better pattern analysis - name: Setup Elixir for Hypatia scanner - uses: erlef/setup-beam@v1.24.1 + uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 with: elixir-version: '1.18' otp-version: '27' @@ -108,7 +108,7 @@ jobs: echo "- Medium: $MEDIUM" >> $GITHUB_STEP_SUMMARY - name: Upload findings artifact - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: hypatia-findings path: hypatia-findings.json @@ -244,7 +244,7 @@ jobs: always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork != true) - uses: github/codeql-action/upload-sarif@v4.38.2 + uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: sarif_file: hypatia.sarif # Distinct category so Hypatia results coexist with CodeQL's @@ -384,7 +384,7 @@ jobs: # the pull-requests: write permission above: a token/API hiccup or # a fork PR (read-only token) skips the comment, not the check. continue-on-error: true - uses: actions/github-script@v9.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const fs = require('fs'); diff --git a/.github/workflows/instant-sync.yml b/.github/workflows/instant-sync.yml index 7f96acc..0c2cb53 100644 --- a/.github/workflows/instant-sync.yml +++ b/.github/workflows/instant-sync.yml @@ -26,18 +26,30 @@ jobs: steps: - name: Trigger Propagation if: ${{ env.FARM_DISPATCH_TOKEN != '' }} - uses: peter-evans/repository-dispatch@v4.0.1 - with: - token: ${{ env.FARM_DISPATCH_TOKEN }} - repository: hyperpolymath/.git-private-farm - event-type: propagate - client-payload: |- - { - "repo": "${{ github.event.repository.name }}", - "ref": "${{ github.ref }}", - "sha": "${{ github.sha }}", - "forges": "" - } + # peter-evans/repository-dispatch is not from a hyperpolymath-owned, + # GitHub-created or Marketplace-verified-creator repo, so the allow-list + # rejects the action and the workflow dies with startup_failure + # (game-server-admin#103). The dispatch endpoint is one POST; `gh` is + # preinstalled on every runner, so no action is needed at all. + env: + GH_TOKEN: ${{ env.FARM_DISPATCH_TOKEN }} + DISPATCH_REPO: hyperpolymath/.git-private-farm + SRC_REPO: ${{ github.event.repository.name }} + SRC_REF: ${{ github.ref }} + SRC_SHA: ${{ github.sha }} + run: | + set -euo pipefail + jq -n \ + --arg repo "$SRC_REPO" \ + --arg ref "$SRC_REF" \ + --arg sha "$SRC_SHA" \ + '{event_type: "propagate", + client_payload: {repo: $repo, ref: $ref, sha: $sha, forges: ""}}' \ + | gh api --method POST \ + -H 'Accept: application/vnd.github+json' \ + "/repos/${DISPATCH_REPO}/dispatches" \ + --input - + echo "::notice::Propagation dispatched to ${DISPATCH_REPO}" - name: Confirm if: ${{ env.FARM_DISPATCH_TOKEN != '' }} diff --git a/.github/workflows/push-email-notify.yml b/.github/workflows/push-email-notify.yml index 438d01a..2080615 100644 --- a/.github/workflows/push-email-notify.yml +++ b/.github/workflows/push-email-notify.yml @@ -40,7 +40,7 @@ jobs: timeout-minutes: 5 steps: - name: Send push notification email - uses: hyperpolymath/smtp-notify-action@v0.3.0 + uses: hyperpolymath/smtp-notify-action@22e7bdb322c430c1d0dac6b3bb307f4bb139d0be # v0.3.0 with: server_address: ${{ secrets.SMTP_HOST }} server_port: ${{ secrets.SMTP_PORT }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cef733c..b1647af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Build run: | @@ -38,7 +38,7 @@ jobs: cp src/interface/ffi/zig-out/lib/libgsa.so release-artifacts/ 2>/dev/null || true tar -czf release-artifacts/gsa-linux-x86_64.tar.gz -C release-artifacts gsa - - uses: actions/upload-artifact@v7.0.1 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-artifacts path: release-artifacts/ @@ -54,7 +54,7 @@ jobs: changelog: ${{ steps.cliff.outputs.content }} version: ${{ steps.version.outputs.version }} steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -84,7 +84,7 @@ jobs: git cliff --output CHANGELOG.md - name: Upload updated CHANGELOG.md - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: changelog path: CHANGELOG.md @@ -100,27 +100,46 @@ jobs: id-token: write attestations: write steps: - - uses: actions/checkout@v7.0.1 + # Egress audit. Hypatia's RE001 flags any job that reaches for secrets.* + # without installing harden-runner, and this one holds the release token. + - name: Harden runner (egress audit) + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + egress-policy: audit + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/download-artifact@v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: release-artifacts path: artifacts/ - name: Create GitHub Release - uses: softprops/action-gh-release@v3.0.3 - with: - body: ${{ needs.changelog.outputs.changelog }} - draft: false - prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }} - generate_release_notes: false - files: | - artifacts/gsa-linux-x86_64.tar.gz + # softprops/action-gh-release is not from a hyperpolymath-owned, + # GitHub-created or Marketplace-verified-creator repo, so the allow-list + # rejects the action and the workflow dies with startup_failure + # (game-server-admin#103). `gh release create` covers the same ground and + # `gh` is preinstalled on every runner. env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ github.ref_name }} + RELEASE_NOTES: ${{ needs.changelog.outputs.changelog }} + run: | + set -euo pipefail + case "$RELEASE_TAG" in + *-rc*|*-beta*|*-alpha*) prerelease=(--prerelease) ;; + *) prerelease=() ;; + esac + printf '%s\n' "$RELEASE_NOTES" > release-notes.md + gh release create "$RELEASE_TAG" \ + --title "$RELEASE_TAG" \ + --notes-file release-notes.md \ + --verify-tag \ + "${prerelease[@]}" \ + artifacts/gsa-linux-x86_64.tar.gz - name: Attest build provenance - uses: actions/attest-build-provenance@v4.2.2 + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-path: | artifacts/gsa-linux-x86_64.tar.gz diff --git a/.github/workflows/rhodibot.yml b/.github/workflows/rhodibot.yml index 29dc13e..cb9dad3 100644 --- a/.github/workflows/rhodibot.yml +++ b/.github/workflows/rhodibot.yml @@ -34,7 +34,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 - name: Rhodibot — detect drift (no mutations) diff --git a/.github/workflows/static-analysis-gate.yml b/.github/workflows/static-analysis-gate.yml index 3255b45..e2db20c 100644 --- a/.github/workflows/static-analysis-gate.yml +++ b/.github/workflows/static-analysis-gate.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -128,7 +128,7 @@ jobs: echo "Skipped: panic-attack not available in this environment." >> "$GITHUB_STEP_SUMMARY" - name: Upload panic-attack findings - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: panic-attack-findings path: panic-attack-findings.json @@ -150,14 +150,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Setup Elixir for Hypatia scanner id: beam continue-on-error: true - uses: erlef/setup-beam@v1.24.1 + uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 with: elixir-version: '1.19.4' otp-version: '28.3' @@ -263,7 +263,7 @@ jobs: echo "Skipped: Hypatia scanner not available in this environment." >> "$GITHUB_STEP_SUMMARY" - name: Upload hypatia findings - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: hypatia-findings path: hypatia-findings.json @@ -285,7 +285,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -352,7 +352,7 @@ jobs: echo "Skipped: panic-attack not available in this environment." >> "$GITHUB_STEP_SUMMARY" - name: Upload bridge report - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: bridge-report path: bridge-report.json @@ -377,19 +377,19 @@ jobs: steps: - name: Download panic-attack findings - uses: actions/download-artifact@v8.0.1 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: panic-attack-findings path: findings/ - name: Download hypatia findings - uses: actions/download-artifact@v8.0.1 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: hypatia-findings path: findings/ - name: Download bridge report - uses: actions/download-artifact@v8.0.1 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: bridge-report path: findings/ @@ -451,7 +451,7 @@ jobs: echo "low=$LOW" >> "$GITHUB_OUTPUT" - name: Upload unified findings (fleet scanner picks these up) - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: unified-findings path: findings/unified-findings.json diff --git a/container/deploy.k9.ncl b/container/deploy.k9.ncl index 43415a6..9d7d922 100644 --- a/container/deploy.k9.ncl +++ b/container/deploy.k9.ncl @@ -2,72 +2,26 @@ K9! # SPDX-License-Identifier: MPL-2.0 # deploy.k9.ncl — game-server-admin deployment component (Hunt level) # -# k9-svc deployment specification with full pedigree (L1-L5). +# k9-svc deployment specification with full pedigree. # Security Level: 'Hunt (requires cryptographic handshake for execution). # # WARNING: This component can execute shell commands! # It requires explicit authorisation via the Leash system. # +# Layout note: the pedigree is written inline, in the canonical K9 shape +# (`pedigree = { schema_version, security, metadata, … }`), not let-bound and +# exported. K9's validators are lexical — they read the record, they do not +# evaluate Nickel — so the component's own name, version and leash have to be +# visible inside the `pedigree = { … }` record. Factoring the pedigree through +# `let component_pedigree = { … }` hid all three and turned +# `Validate K9 contracts` red (game-server-admin#103). `deployment` and +# `scripts` carry no pedigree fields, so they stay let-bound. +# # Usage: # nickel typecheck container/deploy.k9.ncl # k9-svc validate container/deploy.k9.ncl # k9-svc deploy container/deploy.k9.ncl --env production -# The component's pedigree (self-description across five layers) -let component_pedigree = { - # ───────────────────────────────────────────────────────────── - # L1: The Snout — Identity - # ───────────────────────────────────────────────────────────── - metadata = { - name = "gsa-deploy", - version = "{{VERSION}}", - breed = "application/vnd.k9+nickel", - magic_number = "K9!", - description = "game-server-admin deployment component (Hunt level)", - }, - - # ───────────────────────────────────────────────────────────── - # L2: The Scent — Target Environment - # ───────────────────────────────────────────────────────────── - target = { - os = 'Linux, - is_edge = false, - requires_podman = true, - min_memory_mb = 256, - }, - - # ───────────────────────────────────────────────────────────── - # L3: The Leash — Security - # ───────────────────────────────────────────────────────────── - security = { - trust_level = 'Hunt, - allow_network = true, - allow_filesystem_write = true, - allow_subprocess = true, - # In production, replace with a real Ed25519 signature. - signature = "PLACEHOLDER-SIGNATURE-REQUIRED-FOR-HUNT", - }, - - # ───────────────────────────────────────────────────────────── - # L4: The Gut — Self-Validation - # ───────────────────────────────────────────────────────────── - validation = { - checksum = "sha256:placeholder", - pedigree_version = "1.0.0", - hunt_authorized = false, # Must be set true after handshake - }, - - # ───────────────────────────────────────────────────────────── - # L5: The Muscle — Deployment Recipes - # ───────────────────────────────────────────────────────────── - recipes = { - install = "just container-build", - validate = "just container-verify", - deploy = "just container-up", - migrate = "just container-build && just container-up", - }, -} in - # Deployment configuration let deployment = { # Target environments (dev / staging / production) @@ -144,7 +98,73 @@ echo "K9: Rollback complete." # Export the component { - pedigree = component_pedigree, + # The component's pedigree (self-description) + pedigree = { + # ───────────────────────────────────────────────────────── + # L1: The Snout — Identity + # ───────────────────────────────────────────────────────── + metadata = { + name = "gsa-deploy", + version = "1.0.0", + description = "game-server-admin deployment component (Hunt level)", + author = "Jonathan D.A. Jewell ", + }, + + # K9 schema conformance and format marker + schema_version = "1.0.0", + component_type = "deployment", + breed = "application/vnd.k9+nickel", + magic_number = "K9!", + + # ───────────────────────────────────────────────────────── + # L2: The Scent — Target Environment + # ───────────────────────────────────────────────────────── + target = { + os = 'Linux, + is_edge = false, + requires_podman = true, + min_memory_mb = 256, + }, + + # ───────────────────────────────────────────────────────── + # L3: The Leash — Security + # ───────────────────────────────────────────────────────── + security = { + leash = 'Hunt, + trust_level = "full-system-access", + allow_network = true, + allow_filesystem_write = true, + allow_subprocess = true, + signature_required = true, + # In production, replace with a real Ed25519 signature. + signature = "PLACEHOLDER-SIGNATURE-REQUIRED-FOR-HUNT", + }, + + # ───────────────────────────────────────────────────────── + # L4: The Gut — Self-Validation + # ───────────────────────────────────────────────────────── + validation = { + checksum = "sha256:placeholder", + hunt_authorized = false, # Must be set true after handshake + }, + + # ───────────────────────────────────────────────────────── + # L5: The Muscle — Deployment Recipes + # ───────────────────────────────────────────────────────── + recipes = { + install = "just container-build", + validate = "just container-verify", + deploy = "just container-up", + migrate = "just container-build && just container-up", + }, + + warnings = [ + "This component has full system access", + "Only run from trusted sources with verified signatures", + "Review the deployment scripts above before execution", + ], + }, + deployment = deployment, scripts = scripts, diff --git a/scripts/install-zig.sh b/scripts/install-zig.sh index 008b9b8..53bf421 100755 --- a/scripts/install-zig.sh +++ b/scripts/install-zig.sh @@ -30,7 +30,7 @@ ZIG_SHA256_X86_64_LINUX="02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9e os="$(uname -s)" arch="$(uname -m)" -if [ "$os" != "Linux" ] || [ "$arch" != "x86_64" ]; then +if [[ "$os" != "Linux" ]] || [[ "$arch" != "x86_64" ]]; then echo "::error::install-zig.sh pins only x86_64-linux; got ${os}/${arch}. Add a pinned sha256 for this platform." >&2 exit 1 fi @@ -52,7 +52,7 @@ tar -xJf "${work}/${tarball}" -C "$dest" --strip-components=1 rm -rf "$work" got="$("${dest}/zig" version)" -if [ "$got" != "$ZIG_VERSION" ]; then +if [[ "$got" != "$ZIG_VERSION" ]]; then echo "::error::installed zig reports '${got}', expected '${ZIG_VERSION}'" >&2 exit 1 fi diff --git a/scripts/setup-haskell.sh b/scripts/setup-haskell.sh new file mode 100755 index 0000000..cfc788b --- /dev/null +++ b/scripts/setup-haskell.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# Install a pinned, hash-verified GHC + cabal-install toolchain for CI — +# no third-party action. +# +# Why this exists: the repo's Actions policy admits only actions from +# hyperpolymath-owned repos, GitHub-created repos, or Marketplace-verified +# creators — and every action must be pinned to a full-length commit SHA or a +# full semver tag. `haskell-actions/setup` is none of those, so `GitHub Pages` +# ended in startup_failure before running a single step +# (game-server-admin#103). This script needs no action at all. +# +# Trust chain: both tarballs are fetched from downloads.haskell.org over HTTPS +# and must match the sha256 pinned below, or the step fails. The pins are +# transcribed from GHCup's official release metadata +# (haskell/ghcup-metadata, ghcup-0.0.7.yaml, published with a minisign +# signature in ghcup-0.0.7.yaml.sig) — the same table ghcup itself installs +# from. The deb11 bindists are chosen because they are the generic Linux +# builds and run unchanged on the ubuntu-24.04 runner image. +# +# To bump: take the new dlUri/dlHash pair out of ghcup-0.0.7.yaml and change +# the version and digest together. Never change one without the other. +# +# Usage: bash scripts/setup-haskell.sh +# Puts `ghc` and `cabal` on PATH for subsequent steps via $GITHUB_PATH. + +set -euo pipefail + +GHC_VERSION="9.8.2" +GHC_TARBALL="ghc-${GHC_VERSION}-x86_64-deb11-linux.tar.xz" +GHC_URL="https://downloads.haskell.org/~ghc/${GHC_VERSION}/${GHC_TARBALL}" +GHC_SHA256="ee9d424c614dd4b92b0104e812fb92016bf3d3ffd5e51a8af544634b9d817028" + +CABAL_VERSION="3.10.2.0" +CABAL_TARBALL="cabal-install-${CABAL_VERSION}-x86_64-linux-deb11.tar.xz" +CABAL_URL="https://downloads.haskell.org/cabal/cabal-install-${CABAL_VERSION}/${CABAL_TARBALL}" +CABAL_SHA256="9ca5625c89e8fcada02edced5048c3a3db0254e2bef1eb792d549d633222b108" + +tmp_root="${RUNNER_TEMP:?RUNNER_TEMP must be set (GitHub Actions)}/haskell-setup" +prefix="${RUNNER_TEMP}/haskell" +mkdir -p "$tmp_root" "$prefix/bin" + +# The GHC bindist is dynamically linked against gmp/ncurses/zlib, and cabal +# needs the matching C headers to build packages such as pandoc. The runner +# image does not guarantee all of them. +sudo apt-get update -qq +sudo apt-get install -y --no-install-recommends \ + libgmp-dev libtinfo6 libncurses-dev zlib1g-dev + +fetch_verify() { + # fetch_verify + # Download an HTTPS tarball to the destination and check its expected SHA-256. + # Overwrites an existing destination; a checksum failure leaves the file there. + # Returns zero on a match. With this script's set -e, download or verification + # failures abort setup without removing the destination file. + local url="$1" want="$2" dest="$3" + curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-delay 5 \ + -o "$dest" "$url" + echo "${want} ${dest}" | sha256sum -c - +} + +fetch_verify "$GHC_URL" "$GHC_SHA256" "${tmp_root}/${GHC_TARBALL}" +fetch_verify "$CABAL_URL" "$CABAL_SHA256" "${tmp_root}/${CABAL_TARBALL}" + +tar -xJf "${tmp_root}/${GHC_TARBALL}" -C "$tmp_root" +tar -xJf "${tmp_root}/${CABAL_TARBALL}" -C "$tmp_root" + +# A GHC bindist is relocatable only after `configure` rewrites the wrapper +# scripts, so do the real install into a throwaway prefix rather than using it +# in place. +( cd "${tmp_root}/ghc-${GHC_VERSION}-x86_64-unknown-linux" \ + && ./configure --prefix="$prefix" \ + && make install ) + +install -m 0755 "${tmp_root}/cabal" "${prefix}/bin/cabal" + +rm -rf "$tmp_root" + +got_ghc="$("${prefix}/bin/ghc" --numeric-version)" +if [[ "$got_ghc" != "$GHC_VERSION" ]]; then + echo "::error::installed ghc reports '${got_ghc}', expected '${GHC_VERSION}'" >&2 + exit 1 +fi + +got_cabal="$("${prefix}/bin/cabal" --numeric-version)" +if [[ "$got_cabal" != "$CABAL_VERSION" ]]; then + echo "::error::installed cabal reports '${got_cabal}', expected '${CABAL_VERSION}'" >&2 + exit 1 +fi + +echo "${prefix}/bin" >> "${GITHUB_PATH:?GITHUB_PATH must be set (GitHub Actions)}" +echo "Installed ghc ${got_ghc} and cabal-install ${got_cabal} into ${prefix}"