From d03d34576e7c82e632a22eebc0155c0d4f466e4c Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Tue, 25 Aug 2026 14:24:23 +0400 Subject: [PATCH] ci(nix): recover a stale vendorHash automatically --- .github/workflows/nix.yaml | 138 ++++++++++++++++++++++++++++++++- .github/workflows/release.yaml | 3 + Taskfile.yaml | 5 ++ nix/package.nix | 1 - 4 files changed, 145 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nix.yaml b/.github/workflows/nix.yaml index c4d03b9..ec1194d 100644 --- a/.github/workflows/nix.yaml +++ b/.github/workflows/nix.yaml @@ -21,16 +21,152 @@ on: permissions: contents: read +concurrency: + group: nix-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} + # Keyed on the PR author, which is stable across the fix-up push, so the run + # started by that push cannot cancel the run that made it. + cancel-in-progress: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login != 'dependabot[bot]' }} + +env: + SELF_HOSTED_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} + IS_DEPENDABOT: ${{ github.actor == 'dependabot[bot]' }} + jobs: build: name: build runs-on: ubuntu-latest + permissions: + # The only write is git push, which authenticates with the PAT that + # checkout persists, not with GITHUB_TOKEN. + contents: read + pull-requests: write steps: - uses: actions/checkout@v7 with: - ref: ${{ inputs.ref || github.ref }} + # head.sha, not the merge ref: the hash is computed for the tree that + # gets pushed, and the run that verifies it must build that same tree. + ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }} + token: ${{ (env.IS_DEPENDABOT == 'true' && secrets.DEPENDABOT) || secrets.GITHUB_TOKEN }} + - uses: cachix/install-nix-action@v31 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} + - name: nix build + id: build + continue-on-error: ${{ env.SELF_HOSTED_PR == 'true' }} run: nix build .#default --no-link --print-build-logs + + - name: Recompute vendorHash + id: hash + if: steps.build.outcome == 'failure' && env.SELF_HOSTED_PR == 'true' + run: | + nix run nixpkgs#nix-update -- --flake --version=skip --no-src default + + # An unchanged hash means the build broke for some other reason. Say + # nothing and let the job fail on the real error. + if git diff --quiet -- nix/package.nix; then + echo "stale=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "stale=true" >> "$GITHUB_OUTPUT" + sed -n 's/^[[:space:]]*vendorHash = "\([^"]*\)".*/hash=\1/p' nix/package.nix >> "$GITHUB_OUTPUT" + + - name: Push vendorHash fix + if: steps.hash.outputs.stale == 'true' && env.IS_DEPENDABOT == 'true' + env: + HEAD_REF: ${{ github.head_ref }} + PUSH_TOKEN: ${{ secrets.DEPENDABOT }} + run: | + if [ -z "$PUSH_TOKEN" ]; then + echo "::error::the DEPENDABOT push token is missing from the Dependabot secret store" + exit 1 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add nix/package.nix + git commit -m "fix(nix): update vendorHash [dependabot skip]" + git push origin "HEAD:refs/heads/$HEAD_REF" + echo "::notice::vendorHash updated and pushed; the run on the new commit decides this PR" + + - name: Comment stale vendorHash + if: steps.hash.outputs.stale == 'true' && env.IS_DEPENDABOT != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.number }} + HASH: ${{ steps.hash.outputs.hash }} + run: | + marker='' + body=$(cat </dev/null + else + gh pr comment "$PR" --body "$body" + fi + + # Once the hash is fixed the build passes and the step above stops running, + # so without this the comment keeps claiming the branch is broken under a + # green check. + - name: Resolve stale vendorHash comment + # Gated on the hash rather than the build: a later push can fix the hash + # while the build still fails for an unrelated reason, and the comment + # must stop blaming the vendor derivation either way. Skipped when the + # recompute itself errored, since then we do not know. + if: ${{ !cancelled() && env.SELF_HOSTED_PR == 'true' && steps.hash.outputs.stale != 'true' && steps.hash.outcome != 'failure' }} + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.number }} + BUILD: ${{ steps.build.outcome }} + run: | + marker='' + if ! matches=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR/comments?per_page=100" \ + --jq ".[] | select(.user.login == \"github-actions[bot]\" and ((.body // \"\") | contains(\"$marker\"))) | .id"); then + echo "::error::could not list the pull request comments" + exit 1 + fi + id=$(printf '%s\n' "$matches" | head -n 1) + [ -n "$id" ] || exit 0 + + if [ "$BUILD" = "success" ]; then + detail='`nix build` passes on this branch.' + else + detail='The build is failing for another reason; see the workflow log.' + fi + body=$(cat </dev/null + + - name: Report build failure + if: ${{ !cancelled() && steps.build.outcome == 'failure' && !(env.IS_DEPENDABOT == 'true' && steps.hash.outputs.stale == 'true') }} + run: | + echo "::error file=nix/package.nix::nix build failed; see the log above" + exit 1 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9157295..75f4a03 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -24,6 +24,9 @@ jobs: nix: name: Verify Nix package uses: ./.github/workflows/nix.yaml + permissions: + contents: read + pull-requests: write with: ref: ${{ inputs.tag || github.ref }} diff --git a/Taskfile.yaml b/Taskfile.yaml index 5060719..bab6af7 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -39,6 +39,11 @@ tasks: cmds: - "{{.GOLANGCI}} run" + nix-hash: + desc: Recompute nix/package.nix vendorHash after a dependency change. + cmds: + - nix run nixpkgs#nix-update -- --flake --version=skip --no-src default + test: desc: Run Go tests. cmds: diff --git a/nix/package.nix b/nix/package.nix index 368ffcc..4b2a6c9 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -29,7 +29,6 @@ ldflags+=("-X 'go.aykhans.me/sarin/internal/version.GoVersion=$(go version)'") ''; - # cmd/cli produces a binary named "cli"; rename it to "sarin". postInstall = '' mv $out/bin/cli $out/bin/sarin '';