From 22d869979309a53551813576b1a7dfe4fc8e3917 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Fri, 24 Jul 2026 14:59:39 -0400 Subject: [PATCH 1/7] fix(renovate): add customManager for prek.toml.jinja pins --- .github/renovate.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/renovate.json b/.github/renovate.json index d38851f..f44dcab 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -14,6 +14,13 @@ "managerFilePatterns": ["/template/flake\\.nix\\.jinja$/", "/includes/flake-extra-inputs\\.jinja$/"], "matchStrings": ["github:(?[\\w-]+/[\\w-]+)/(?[a-f0-9]+).*#\\s*(?v[\\S]+)"], "datasourceTemplate": "github-releases" + }, + { + "customType": "regex", + "description": "Update Git hook dependency pins in prek.toml.jinja (mirrors the native prek.toml manager, which can't parse this Jinja-templated file)", + "managerFilePatterns": ["/template/prek\\.toml\\.jinja$/"], + "matchStrings": ["repo = \"https://github\\.com/(?[\\w.-]+/[\\w.-]+)\"\\nrev = \"(?[^\"]+)\""], + "datasourceTemplate": "github-tags" } ] } From 2c732c24be1fdd3a31d96b2ebbb097275d2366ac Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Fri, 24 Jul 2026 14:59:46 -0400 Subject: [PATCH 2/7] fix(ci): soft-fail render-template when RENOVATE_TOKEN is unset or stale --- .github/workflows/render-template.yaml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/render-template.yaml b/.github/workflows/render-template.yaml index 35abb96..e4b3f84 100644 --- a/.github/workflows/render-template.yaml +++ b/.github/workflows/render-template.yaml @@ -16,35 +16,50 @@ permissions: jobs: render: runs-on: ubuntu-latest + # Soft-fail: an unconfigured or stale RENOVATE_TOKEN (e.g. on a fork, or if the + # token expires) shouldn't red-X every Renovate PR touching includes/**|template/**. + continue-on-error: true permissions: contents: write timeout-minutes: 15 + env: + # secrets can't be referenced directly in `if:` conditions; mirror into env first. + RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Checkout + id: checkout + if: ${{ env.RENOVATE_TOKEN != '' }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ github.ref }} token: ${{ secrets.RENOVATE_TOKEN }} - uses: ./.github/actions/nix-setup + if: ${{ steps.checkout.outcome == 'success' }} - name: Set up Git + if: ${{ steps.checkout.outcome == 'success' }} run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Re-render from template + if: ${{ steps.checkout.outcome == 'success' }} run: nix develop -c just render - name: Update flake lock if inputs changed + if: ${{ steps.checkout.outcome == 'success' }} run: | if ! git diff --quiet flake.nix; then nix flake lock fi - name: Restore copier answers + if: ${{ steps.checkout.outcome == 'success' }} run: git restore .copier-answers.yaml - name: Commit and push if changed + if: ${{ steps.checkout.outcome == 'success' }} run: | if [ -n "$(git status --porcelain)" ]; then git add -A From 5b268bdc72451091bb81b89e921c2e713663769d Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sat, 25 Jul 2026 14:39:52 -0400 Subject: [PATCH 3/7] fix(renovate): move prek.toml.jinja customManager to its jinja source .github/renovate.json is generated from template/.github/renovate.json.jinja via an include of includes/renovate-template.jinja, not hand-edited directly. The previous commit added the customManager to the generated file only, so the consistency check's re-render reverted it right back out. --- includes/renovate-template.jinja | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/includes/renovate-template.jinja b/includes/renovate-template.jinja index 5b4cc67..7f6181f 100644 --- a/includes/renovate-template.jinja +++ b/includes/renovate-template.jinja @@ -12,5 +12,12 @@ "managerFilePatterns": ["/template/flake\\.nix\\.jinja$/", "/includes/flake-extra-inputs\\.jinja$/"], "matchStrings": ["github:(?[\\w-]+/[\\w-]+)/(?[a-f0-9]+).*#\\s*(?v[\\S]+)"], "datasourceTemplate": "github-releases" + }, + { + "customType": "regex", + "description": "Update Git hook dependency pins in prek.toml.jinja (mirrors the native prek.toml manager, which can't parse this Jinja-templated file)", + "managerFilePatterns": ["/template/prek\\.toml\\.jinja$/"], + "matchStrings": ["repo = \"https://github\\.com/(?[\\w.-]+/[\\w.-]+)\"\\nrev = \"(?[^\"]+)\""], + "datasourceTemplate": "github-tags" } {% endif %} From a90b48529d64869a3fd2a2f0889a4008e1655964 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Sun, 26 Jul 2026 11:55:32 -0400 Subject: [PATCH 4/7] fix(ci): scope continue-on-error to the checkout step only Job-level continue-on-error masked any failure in the job, not just an auth problem -- a genuine bug in the render/push steps would have soft- passed too. Scoping it to just the checkout step preserves hard-fail behavior for everything downstream while still tolerating an unset or stale RENOVATE_TOKEN. --- .github/workflows/render-template.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/render-template.yaml b/.github/workflows/render-template.yaml index e4b3f84..ee08718 100644 --- a/.github/workflows/render-template.yaml +++ b/.github/workflows/render-template.yaml @@ -16,9 +16,6 @@ permissions: jobs: render: runs-on: ubuntu-latest - # Soft-fail: an unconfigured or stale RENOVATE_TOKEN (e.g. on a fork, or if the - # token expires) shouldn't red-X every Renovate PR touching includes/**|template/**. - continue-on-error: true permissions: contents: write timeout-minutes: 15 @@ -26,9 +23,14 @@ jobs: # secrets can't be referenced directly in `if:` conditions; mirror into env first. RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} steps: + # Skipped entirely when unconfigured (e.g. a fork with no RENOVATE_TOKEN secret). + # continue-on-error scoped to just this step: an invalid/expired token should + # soft-fail here rather than red-X the whole job, but any REAL failure in the + # steps below (a broken render, a bad push, etc.) must still fail loudly. - name: Checkout id: checkout if: ${{ env.RENOVATE_TOKEN != '' }} + continue-on-error: true uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ github.ref }} From d2eaf2cc7fc9cfcdfe9e727cf6a4200ed16ae9c0 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Mon, 27 Jul 2026 09:51:11 -0400 Subject: [PATCH 5/7] fix(ci): scope RENOVATE_TOKEN requirement to only the push step Checkout only needs read access to a public repo -- GITHUB_TOKEN handles that fine. Gating checkout on the token (and cascading skips to every step after it) meant nix-setup, the actual render, and the flake-lock check never ran when the token was unset or stale, even though none of them need any special credential. Only the final `git push` genuinely needs RENOVATE_TOKEN: GITHUB_TOKEN-authored pushes don't retrigger downstream workflow runs, and pushing a workflow-file change needs `workflows: write`, which GITHUB_TOKEN lacks here. --- .github/workflows/render-template.yaml | 27 +++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/render-template.yaml b/.github/workflows/render-template.yaml index ee08718..e3fc268 100644 --- a/.github/workflows/render-template.yaml +++ b/.github/workflows/render-template.yaml @@ -23,47 +23,46 @@ jobs: # secrets can't be referenced directly in `if:` conditions; mirror into env first. RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} steps: - # Skipped entirely when unconfigured (e.g. a fork with no RENOVATE_TOKEN secret). - # continue-on-error scoped to just this step: an invalid/expired token should - # soft-fail here rather than red-X the whole job, but any REAL failure in the - # steps below (a broken render, a bad push, etc.) must still fail loudly. + # Default GITHUB_TOKEN is fine here: this is a public repo and checkout only + # needs read access. No token gating needed on this step (or anything below, + # up to the push) -- they should always run so template bugs still get caught + # even when RENOVATE_TOKEN is unset or stale. - name: Checkout - id: checkout - if: ${{ env.RENOVATE_TOKEN != '' }} - continue-on-error: true uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ github.ref }} - token: ${{ secrets.RENOVATE_TOKEN }} - uses: ./.github/actions/nix-setup - if: ${{ steps.checkout.outcome == 'success' }} - name: Set up Git - if: ${{ steps.checkout.outcome == 'success' }} run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Re-render from template - if: ${{ steps.checkout.outcome == 'success' }} run: nix develop -c just render - name: Update flake lock if inputs changed - if: ${{ steps.checkout.outcome == 'success' }} run: | if ! git diff --quiet flake.nix; then nix flake lock fi - name: Restore copier answers - if: ${{ steps.checkout.outcome == 'success' }} run: git restore .copier-answers.yaml + # The only step that actually needs a non-default token: GITHUB_TOKEN-authored + # pushes don't trigger downstream workflow runs (pr-checks.yaml wouldn't re-run + # on the pushed commit), and pushing a change to a workflow file needs + # `workflows: write`, which GITHUB_TOKEN doesn't have here. Skipped entirely + # when unconfigured (e.g. a fork); soft-fails (rather than red-X'ing the PR) + # when the token is set but invalid/expired. - name: Commit and push if changed - if: ${{ steps.checkout.outcome == 'success' }} + if: ${{ env.RENOVATE_TOKEN != '' }} + continue-on-error: true run: | if [ -n "$(git status --porcelain)" ]; then + git remote set-url origin "https://x-access-token:${RENOVATE_TOKEN}@github.com/${{ github.repository }}.git" git add -A git commit -m "chore: re-render root files from updated template" git push From b9d1be0b00dca0a31d226438f399a354908e258a Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Mon, 27 Jul 2026 14:16:54 -0400 Subject: [PATCH 6/7] fix(ci): mint a khepri-bot installation token instead of a static PAT Replaces the stale, misleadingly-named RENOVATE_TOKEN secret (a leftover from this repo's pre-migration self-hosted Renovate setup, unrelated to what this workflow actually does) with a fresh installation token minted at runtime from khepri-bot's App credentials (KHEPRI_BOT_APP_ID/KHEPRI_BOT_PRIVATE_KEY, org-level secrets on gordon-code scoped to this repo). No more manual rotation or silent expiry -- the only way this now breaks is a deliberate key rotation, not decay. --- .github/workflows/render-template.yaml | 29 ++++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/render-template.yaml b/.github/workflows/render-template.yaml index e3fc268..1e5a1f6 100644 --- a/.github/workflows/render-template.yaml +++ b/.github/workflows/render-template.yaml @@ -19,14 +19,11 @@ jobs: permissions: contents: write timeout-minutes: 15 - env: - # secrets can't be referenced directly in `if:` conditions; mirror into env first. - RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} steps: # Default GITHUB_TOKEN is fine here: this is a public repo and checkout only # needs read access. No token gating needed on this step (or anything below, # up to the push) -- they should always run so template bugs still get caught - # even when RENOVATE_TOKEN is unset or stale. + # even when the bot token below is unset or invalid. - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: @@ -51,18 +48,32 @@ jobs: - name: Restore copier answers run: git restore .copier-answers.yaml + # Mints a short-lived khepri-bot installation token instead of a static PAT -- + # no manual rotation, no silent expiry. `vars` (unlike `secrets`) can be used + # directly in `if:`, so this cleanly no-ops on forks without KHEPRI_BOT_APP_ID + # configured. continue-on-error: an invalid/revoked key soft-fails here rather + # than red-X'ing the whole job. + - name: Generate bot token + id: bot-token + if: ${{ vars.KHEPRI_BOT_APP_ID != '' }} + continue-on-error: true + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ vars.KHEPRI_BOT_APP_ID }} + private-key: ${{ secrets.KHEPRI_BOT_PRIVATE_KEY }} + # The only step that actually needs a non-default token: GITHUB_TOKEN-authored # pushes don't trigger downstream workflow runs (pr-checks.yaml wouldn't re-run # on the pushed commit), and pushing a change to a workflow file needs - # `workflows: write`, which GITHUB_TOKEN doesn't have here. Skipped entirely - # when unconfigured (e.g. a fork); soft-fails (rather than red-X'ing the PR) - # when the token is set but invalid/expired. + # `workflows: write`, which GITHUB_TOKEN doesn't have here. - name: Commit and push if changed - if: ${{ env.RENOVATE_TOKEN != '' }} + if: ${{ steps.bot-token.outcome == 'success' }} continue-on-error: true + env: + GH_TOKEN: ${{ steps.bot-token.outputs.token }} run: | if [ -n "$(git status --porcelain)" ]; then - git remote set-url origin "https://x-access-token:${RENOVATE_TOKEN}@github.com/${{ github.repository }}.git" + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" git add -A git commit -m "chore: re-render root files from updated template" git push From 8ce8b8b6c3f8f9dcbc2c5479e46434156cfa8cbf Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 28 Jul 2026 09:12:33 -0400 Subject: [PATCH 7/7] fix(ci): use client-id instead of deprecated app-id input --- .github/workflows/render-template.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/render-template.yaml b/.github/workflows/render-template.yaml index 1e5a1f6..f3cb0d4 100644 --- a/.github/workflows/render-template.yaml +++ b/.github/workflows/render-template.yaml @@ -50,16 +50,16 @@ jobs: # Mints a short-lived khepri-bot installation token instead of a static PAT -- # no manual rotation, no silent expiry. `vars` (unlike `secrets`) can be used - # directly in `if:`, so this cleanly no-ops on forks without KHEPRI_BOT_APP_ID + # directly in `if:`, so this cleanly no-ops on forks without KHEPRI_BOT_CLIENT_ID # configured. continue-on-error: an invalid/revoked key soft-fails here rather # than red-X'ing the whole job. - name: Generate bot token id: bot-token - if: ${{ vars.KHEPRI_BOT_APP_ID != '' }} + if: ${{ vars.KHEPRI_BOT_CLIENT_ID != '' }} continue-on-error: true uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ vars.KHEPRI_BOT_APP_ID }} + client-id: ${{ vars.KHEPRI_BOT_CLIENT_ID }} private-key: ${{ secrets.KHEPRI_BOT_PRIVATE_KEY }} # The only step that actually needs a non-default token: GITHUB_TOKEN-authored