Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 0 additions & 43 deletions .github/actions/setup-git-committer/action.yml

This file was deleted.

66 changes: 66 additions & 0 deletions .github/nix-hashes/publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# publish.sh <hashes.json>: run from a checkout of v1. Puts <hashes.json> on bot/v1-nix-hashes as one commit on top of v1,
# opens or updates the pull request into v1, dispatches its required checks and enables auto-merge. Closes the pull
# request instead when v1 already has these hashes. Uses git, gh and jq only; never runs repository code.
set -euo pipefail

here=$(cd "$(dirname "$0")" && pwd)
new=$(realpath "$1")
branch=bot/v1-nix-hashes
file=nix/hashes.json
title="chore: update nix node_modules hashes"
: "${GH_REPO:?}" "${GITHUB_SHA:?}"
git=(git -c core.hooksPath=/dev/null -c user.name='github-actions[bot]' -c user.email='41898282+github-actions[bot]@users.noreply.github.com')

bash "$here/validate.sh" "$new"

"${git[@]}" fetch --no-tags --depth=1 origin "+refs/heads/v1:refs/remotes/origin/v1" "$GITHUB_SHA"

# A newer push that changed the inputs has its own run, which publishes hashes for it. Mirrors on.push.paths.
if ! git diff --quiet "$GITHUB_SHA" origin/v1 -- bun.lock package.json ':(glob)packages/*/package.json' \
flake.lock nix/node_modules.nix nix/scripts patches; then
echo "::notice::v1 changed the hash inputs since $GITHUB_SHA; leaving it to the newer run"
exit 0
fi

# Only a same-repository pull request is the bot's; a fork can use the same branch name.
find_pr() {
gh pr list --repo "$GH_REPO" --base v1 --head "$branch" --state open --json number,isCrossRepository \
--jq 'map(select(.isCrossRepository | not)) | .[0].number // empty'
}
pr=$(find_pr)

"${git[@]}" checkout -B "$branch" origin/v1
if cmp -s "$new" "$file"; then
echo "v1 already has these hashes"
if [ -n "$pr" ]; then
gh pr close "$pr" --repo "$GH_REPO" --delete-branch --comment "v1 already has these hashes."
fi
exit 0
fi

cp "$new" "$file"
bash "$here/validate.sh" "$file"
"${git[@]}" add -- "$file"
"${git[@]}" commit --no-verify -m "$title"
[ "$(git diff --name-only origin/v1 HEAD)" = "$file" ] || {
echo "::error::the commit changes more than $file" >&2
exit 1
}
"${git[@]}" push --no-verify --force origin "HEAD:refs/heads/$branch"

if [ -z "$pr" ]; then
gh pr create --repo "$GH_REPO" --base v1 --head "$branch" --title "$title" \
--body "Updates \`$file\` to the node_modules hashes computed for $GITHUB_SHA by bot-nix-hashes. check-bot-scope keeps this branch to that file."
pr=$(find_pr)
fi

# Pushes and pull requests made with GITHUB_TOKEN start no workflows; dispatches do. These produce v1's pull request
# checks on the new head.
for workflow in typecheck.yml test.yml nix-eval.yml check-bot-scope.yml; do
gh workflow run "$workflow" --repo "$GH_REPO" --ref "$branch"
done

gh pr merge "$pr" --repo "$GH_REPO" --auto --squash
echo "### Nix hashes" >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
echo "Updated pull request #$pr" >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
27 changes: 27 additions & 0 deletions .github/nix-hashes/validate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# validate.sh <file>: fails unless <file> has exactly the shape of nix/hashes.json, so the hashes bot can only ever publish
# three SRI hashes. Used by bot-nix-hashes.yml before it commits and by check-bot-scope.yml on the bot's pull request.
# Adding a system to the flake means adding it to SYSTEMS here.
set -euo pipefail

file=${1:?usage: validate.sh <file>}
SYSTEMS='["aarch64-darwin","aarch64-linux","x86_64-linux"]'

fail() {
echo "::error::$file: $*" >&2
exit 1
}

if [ ! -f "$file" ] || [ -L "$file" ]; then fail "not a regular file"; fi
[ "$(wc -c < "$file")" -lt 4096 ] || fail "4096 bytes or larger"
jq -se 'length == 1' "$file" > /dev/null 2>&1 || fail "not a single JSON value"
jq -e --argjson systems "$SYSTEMS" '
type == "object"
and keys == ["nodeModules"]
and (.nodeModules | type == "object")
and (.nodeModules | keys == $systems)
and (.nodeModules | all(.[]; type == "string" and test("\\Asha256-[A-Za-z0-9+/]{43}=\\z")))
' "$file" > /dev/null || fail "expected {\"nodeModules\": {<$SYSTEMS>: \"sha256-<44 base64 chars>\"}}"
# The canonical form rules out duplicate keys and anything else jq would drop on parse.
jq . "$file" | cmp -s - "$file" || fail "not formatted as jq . prints it"
echo "$file: valid"
202 changes: 202 additions & 0 deletions .github/workflows/bot-nix-hashes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
name: bot-nix-hashes

# Keeps nix/hashes.json in step with the lockfile through a pull request, using only GITHUB_TOKEN.
# - hash and compute run repository code (nix build) with a read-only token and no secrets. compute uploads only the
# new nix/hashes.json.
# - publish holds the write token. It runs only .github/nix-hashes/ from this commit, treats the artifact as data,
# validates its exact shape and commits only that file to bot/v1-nix-hashes. It never runs repository code.
# - check-bot-scope.yml, a required check, fails the bot's pull request unless it changes only a valid nix/hashes.json,
# so a tampered branch cannot auto-merge once v1 requires that check.
# This file, check-bot-scope.yml and .github/nix-hashes/ are the security boundary.

permissions:
contents: read

on:
workflow_dispatch:
push:
branches: [v1]
# Mirrored in .github/nix-hashes/publish.sh.
paths:
- "bun.lock"
- "package.json"
- "packages/*/package.json"
- "flake.lock"
- "nix/node_modules.nix"
- "nix/scripts/**"
- "patches/**"
- ".github/workflows/bot-nix-hashes.yml"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
# Native runners required: bun install cross-compilation flags (--os/--cpu)
# do not produce byte-identical node_modules as native installs.
hash:
strategy:
fail-fast: false
matrix:
include:
- system: x86_64-linux
runner: blacksmith-4vcpu-ubuntu-2404
- system: aarch64-linux
runner: blacksmith-4vcpu-ubuntu-2404-arm
- system: aarch64-darwin
runner: macos-latest
runs-on: ${{ matrix.runner }}

steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Setup Nix
uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34

- name: Compute node_modules hash
id: hash
env:
SYSTEM: ${{ matrix.system }}
run: |
set -euo pipefail

BUILD_LOG=$(mktemp)
trap 'rm -f "$BUILD_LOG"' EXIT

HASH=""
MAX_ATTEMPTS=3
for ((ATTEMPT = 1; ATTEMPT <= MAX_ATTEMPTS; ATTEMPT++)); do
# Build with fakeHash to trigger hash mismatch and reveal correct hash
nix build ".#packages.${SYSTEM}.node_modules_updater" --no-link 2>&1 | tee "$BUILD_LOG" || true

HASH="$(nix run --inputs-from . nixpkgs#gnugrep -- -oP 'got:\s*\Ksha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)"

[ -n "$HASH" ] && break

if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
echo "::warning::Attempt ${ATTEMPT}/${MAX_ATTEMPTS} produced no hash for ${SYSTEM}; retrying in $((ATTEMPT * 10))s"
sleep $((ATTEMPT * 10))
fi
done

if [ -z "$HASH" ]; then
echo "::error::Failed to compute hash for ${SYSTEM} after ${MAX_ATTEMPTS} attempts"
cat "$BUILD_LOG"
exit 1
fi

echo "$HASH" > hash.txt
echo "Computed hash for ${SYSTEM}: $HASH"

- name: Upload hash
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: hash-${{ matrix.system }}
path: hash.txt
retention-days: 1

# Assembles nix/hashes.json from the per-system hashes and uploads it as the only input publish accepts.
compute:
needs: hash
runs-on: ubuntu-24.04
outputs:
changed: ${{ steps.update.outputs.changed }}

steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false

- name: Download hash artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: ${{ runner.temp }}/hashes
pattern: hash-*

- name: Update hashes.json
id: update
env:
HASHES: ${{ runner.temp }}/hashes
run: |
set -euo pipefail

HASH_FILE="nix/hashes.json"

[ -f "$HASH_FILE" ] || echo '{"nodeModules":{}}' > "$HASH_FILE"

for SYSTEM in x86_64-linux aarch64-linux aarch64-darwin; do
HASH="$(tr -d '[:space:]' < "$HASHES/hash-${SYSTEM}/hash.txt")"
echo "${SYSTEM}: ${HASH}"
jq --arg sys "$SYSTEM" --arg h "$HASH" '.nodeModules[$sys] = $h' "$HASH_FILE" > tmp.json
mv tmp.json "$HASH_FILE"
done

cat "$HASH_FILE"
bash .github/nix-hashes/validate.sh "$HASH_FILE"

if git diff --quiet -- "$HASH_FILE"; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "### Nix hashes" >> "$GITHUB_STEP_SUMMARY"
echo "Status: no changes" >> "$GITHUB_STEP_SUMMARY"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "### Nix hashes" >> "$GITHUB_STEP_SUMMARY"
echo "Status: changed" >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload hashes.json
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: nix-hashes
path: nix/hashes.json
if-no-files-found: error
retention-days: 1

# Runs even when compute found no change, so an open bot pull request with stale hashes is closed, not merged.
publish:
needs: compute
if: github.repository == 'anomalyco/opencode' && github.ref == 'refs/heads/v1'
runs-on: ubuntu-24.04
concurrency:
group: bot-nix-hashes-publish-v1
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
actions: write

steps:
- name: Check out publish scripts
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.sha }}
path: trusted
sparse-checkout: /.github/nix-hashes/
sparse-checkout-cone-mode: false
persist-credentials: false

# Holds the push credential; only nix/hashes.json is checked out and nothing in it runs.
- name: Check out v1
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: v1
path: repo
sparse-checkout: /nix/hashes.json
sparse-checkout-cone-mode: false

- name: Download hashes.json
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: nix-hashes
path: ${{ runner.temp }}/nix-hashes

- name: Publish
working-directory: repo
run: bash ../trusted/.github/nix-hashes/publish.sh "$RUNNER_TEMP/nix-hashes/hashes.json"
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
Loading
Loading