name: Nix Lockfile Fix on: push: branches: [main] paths: - 'ui-tui/package-lock.json' - 'ui-tui/package.json' - 'web/package-lock.json' - 'web/package.json' workflow_dispatch: inputs: pr_number: description: 'PR number to fix (leave empty to run on the selected branch)' required: false type: string issue_comment: types: [edited] permissions: contents: write pull-requests: write concurrency: group: nix-lockfile-fix-${{ github.event.issue.number || github.event.inputs.pr_number || github.ref }} cancel-in-progress: false jobs: # ── Auto-fix on main ─────────────────────────────────────────────── # Fires when a push to main touches package.json or package-lock.json # in ui-tui/ or web/. Runs fix-lockfiles --apply and pushes the hash # update commit directly to main so Nix builds never stay broken. # # Safety invariants: # 1. The fix commit only touches nix/*.nix files, which are NOT in # the paths filter above, so this cannot re-trigger itself. # 2. An explicit file-whitelist check before commit aborts if # fix-lockfiles ever modifies unexpected files. # 3. Job-level concurrency with cancel-in-progress: true ensures # back-to-back pushes collapse to the newest; ref: main checkout # always operates on the latest branch state. # 4. Uses a GitHub App token (not GITHUB_TOKEN) so the fix commit # triggers downstream nix.yml verification. auto-fix-main: if: github.event_name == 'push' runs-on: ubuntu-latest timeout-minutes: 25 concurrency: group: auto-fix-main cancel-in-progress: true steps: - name: Generate GitHub App token id: app-token uses: actions/create-github-app-token@7bfa3a4717ef143a604ee0a99d859b8886a96d00 # v1.9.3 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: ref: main token: ${{ steps.app-token.outputs.token }} - uses: ./.github/actions/nix-setup - name: Apply lockfile hashes id: apply run: nix run .#fix-lockfiles -- --apply - name: Commit & push if: steps.apply.outputs.changed == 'true' shell: bash run: | set -euo pipefail # Ensure only nix files were modified — prevents accidental # self-triggering if fix-lockfiles ever touches package files. unexpected="$(git diff --name-only | grep -Ev '^nix/(tui|web)\.nix$' || true)" if [ -n "$unexpected" ]; then echo "::error::Unexpected modified files: $unexpected" exit 1 fi # Record the base SHA before committing — used to detect package # file changes if we need to rebase after a non-fast-forward push. BASE_SHA="$(git rev-parse HEAD)" git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' git add nix/tui.nix nix/web.nix git commit -m "fix(nix): auto-refresh npm lockfile hashes" \ -m "Source: $GITHUB_SHA" \ -m "Run: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" # Retry push with rebase in case main advanced with an unrelated # commit during the nix build. Without this, a non-fast-forward # rejection silently loses the fix. If package files changed during # the rebase, abort — a fresh auto-fix run will handle the new state. for attempt in 1 2 3; do if git push origin HEAD:main; then exit 0 fi echo "::warning::Push attempt $attempt failed (non-fast-forward?), rebasing…" git fetch origin main # If package files changed between our base and the new main, # our computed hashes are stale. Abort and let the next triggered # run recompute from the correct package-lock state. pkg_changed="$(git diff --name-only "$BASE_SHA"..origin/main -- \ 'ui-tui/package-lock.json' 'ui-tui/package.json' \ 'web/package-lock.json' 'web/package.json' || true)" if [ -n "$pkg_changed" ]; then echo "::warning::Package files changed since hash computation — aborting; a fresh run will recompute" exit 0 fi git rebase origin/main done echo "::error::Failed to push after 3 rebase attempts" exit 1 # ── PR fix (manual / checkbox) ───────────────────────────────────── # Existing behavior: run on manual dispatch OR when a task-list # checkbox in the sticky lockfile-check comment flips from [ ] to [x]. fix: if: | github.event_name == 'workflow_dispatch' || (github.event_name == 'issue_comment' && github.event.issue.pull_request != null && contains(github.event.comment.body, '[x] **Apply lockfile fix**') && !contains(github.event.changes.body.from, '[x] **Apply lockfile fix**')) runs-on: ubuntu-latest timeout-minutes: 25 steps: - name: Authorize & resolve PR id: resolve uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | // 1. Verify the actor has write access — applies to both checkbox // clicks and manual dispatch. const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ owner: context.repo.owner, repo: context.repo.repo, username: context.actor, }); if (!['admin', 'write', 'maintain'].includes(perm.permission)) { core.setFailed( `${context.actor} lacks write access (has: ${perm.permission})` ); return; } // 2. Resolve which ref to check out. let prNumber = ''; if (context.eventName === 'issue_comment') { prNumber = String(context.payload.issue.number); } else if (context.eventName === 'workflow_dispatch') { prNumber = context.payload.inputs.pr_number || ''; } if (!prNumber) { core.setOutput('ref', context.ref.replace(/^refs\/heads\//, '')); core.setOutput('repo', context.repo.repo); core.setOutput('owner', context.repo.owner); core.setOutput('pr', ''); return; } const { data: pr } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: Number(prNumber), }); core.setOutput('ref', pr.head.ref); core.setOutput('repo', pr.head.repo.name); core.setOutput('owner', pr.head.repo.owner.login); core.setOutput('pr', String(pr.number)); # Wipe the sticky lockfile-check comment to a "running" state as soon # as the job is authorized, so the user sees their click was picked up # before the ~minute of nix build work. - name: Mark sticky as running if: steps.resolve.outputs.pr != '' uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 with: header: nix-lockfile-check number: ${{ steps.resolve.outputs.pr }} message: | ### 🔄 Applying lockfile fix… Triggered by @${{ github.actor }} — [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: repository: ${{ steps.resolve.outputs.owner }}/${{ steps.resolve.outputs.repo }} ref: ${{ steps.resolve.outputs.ref }} token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 - uses: ./.github/actions/nix-setup - name: Apply lockfile hashes id: apply run: nix run .#fix-lockfiles -- --apply - name: Commit & push if: steps.apply.outputs.changed == 'true' shell: bash run: | set -euo pipefail git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' git add nix/tui.nix nix/web.nix git commit -m "fix(nix): refresh npm lockfile hashes" git push - name: Update sticky (applied) if: steps.apply.outputs.changed == 'true' && steps.resolve.outputs.pr != '' uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 with: header: nix-lockfile-check number: ${{ steps.resolve.outputs.pr }} message: | ### ✅ Lockfile fix applied Pushed a commit refreshing the npm lockfile hashes — [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). - name: Update sticky (already current) if: steps.apply.outputs.changed == 'false' && steps.resolve.outputs.pr != '' uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 with: header: nix-lockfile-check number: ${{ steps.resolve.outputs.pr }} message: | ### ✅ Lockfile hashes already current Nothing to commit — [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). - name: Update sticky (failed) if: failure() && steps.resolve.outputs.pr != '' uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 with: header: nix-lockfile-check number: ${{ steps.resolve.outputs.pr }} message: | ### ❌ Lockfile fix failed See the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for logs.