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}"