From 1b3cadd777aefe59525771e0b6e6614d8a685087 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Mon, 3 Aug 2026 16:16:41 -0400 Subject: [PATCH 1/5] fix(renovate): fixes SHA-pin drift at the source and adds a self-heal commit path Removes extractVersionTemplate truncation from the .jinja customManagers added in #20 -- it assumed the native github-actions manager always writes major-only version comments, which is false whenever a patch/minor release lands on a digest the native manager already tracks (confirmed via actions/checkout's real releases: v7.0.1 exists as a formal release, so the untruncated customManager now resolves the same value the native manager writes). Truncating only one side of the same comment guaranteed drift between rendered .yaml files and their .jinja sources on every non-major action bump. Adds a postUpgradeTasks entry that re-renders root files from template/includes before Renovate's own commit, folding any remaining drift into that commit directly instead of depending on the separate, currently-broken bot-token push in render-template.yaml (which requires workflows:write and has been silently failing due to continue-on-error masking it). --- .github/renovate.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index acb014b..cecca50 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,21 +1,23 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "postUpgradeTasks": { + "commands": ["bash .github/scripts/renovate-render.sh"], + "executionMode": "branch" + }, "customManagers": [ { "customType": "regex", "description": "Update SHA-pinned actions in .jinja template files", "managerFilePatterns": ["/template/.*\\.ya?ml\\.jinja$/", "/includes/.*\\.jinja$/"], "matchStrings": ["uses:\\s+(?[\\w-]+/[\\w-]+)@(?[a-f0-9]+)\\s+#\\s+(?v[\\S]+)"], - "datasourceTemplate": "github-releases", - "extractVersionTemplate": "^(?v\\d+)" + "datasourceTemplate": "github-releases" }, { "customType": "regex", "description": "Update SHA-pinned Nix flake inputs in template files", "managerFilePatterns": ["/template/flake\\.nix\\.jinja$/", "/includes/flake-extra-inputs\\.jinja$/"], "matchStrings": ["github:(?[\\w-]+/[\\w-]+)/(?[a-f0-9]+).*#\\s*(?v[\\S]+)"], - "datasourceTemplate": "github-releases", - "extractVersionTemplate": "^(?v\\d+)" + "datasourceTemplate": "github-releases" }, { "customType": "regex", From df493895536b8a2979292f47b6ad806ba908dcbc Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 4 Aug 2026 09:42:41 -0400 Subject: [PATCH 2/5] fix(renovate): adds the render script referenced by postUpgradeTasks Missed in 1b3cadd -- the postUpgradeTasks entry referenced this script but it was never actually staged/committed. Also expands its header comment to carry the full why (moved out of config.js's allowedCommands entry, which now just links here). --- .github/scripts/renovate-render.sh | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100755 .github/scripts/renovate-render.sh diff --git a/.github/scripts/renovate-render.sh b/.github/scripts/renovate-render.sh new file mode 100755 index 0000000..e6df7aa --- /dev/null +++ b/.github/scripts/renovate-render.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Renovate postUpgradeTask, run before Renovate's own commit is made. +# +# Renovate's native github-actions manager and this repo's .jinja customManagers +# (.github/renovate.json) can each resolve a different version string for the +# same SHA-pinned action/flake input -- e.g. the native manager writes the exact +# newest release tag (v7.0.1) into root .yaml files, while the .jinja source +# resolves independently for the same dependency. Left alone, this makes the +# `consistency` job in pr-checks.yaml fail on every non-major bump, since it +# re-renders from the .jinja sources and diffs against the checked-in files. +# +# Re-rendering here, before Renovate commits, folds any resulting diff into +# Renovate's own commit -- no separate push/credential needed. Mirrors +# `just render`, minus the Nix devshell (unavailable in Renovate's runner); +# copier is installed via pipx in khepri-deps/renovate's workflow instead. +set -euo pipefail + +export PATH="$HOME/.local/bin:$PATH" +command -v copier >/dev/null 2>&1 || pip install --quiet --user copier + +find . -maxdepth 1 \ + ! -name '.' ! -name '.git' ! -name '.venv' ! -name '.direnv' \ + ! -name '.serena' \ + ! -name 'template' ! -name 'includes' ! -name 'copier.yaml' \ + ! -name 'hack' ! -name 'tests' ! -name 'pytest.ini' \ + ! -name 'flake.lock' ! -name 'CHANGELOG.md' \ + -exec rm -rf {} + + +copier copy --vcs-ref=HEAD --trust --defaults \ + --data-file includes/copier-answers-sample.yml -f . . + +git show HEAD:lib/nix/project.nix > lib/nix/project.nix 2>/dev/null || true +git show HEAD:justfile > justfile 2>/dev/null || true +git show HEAD:.gitignore > .gitignore 2>/dev/null || true +git show HEAD:.github/workflows/render-template.yaml > .github/workflows/render-template.yaml 2>/dev/null || true +git checkout -- .copier-answers.yaml 2>/dev/null || true From 6ecae9c0a76c1b275fd6cffc20d3e7da5e17d6b1 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 4 Aug 2026 14:24:26 -0400 Subject: [PATCH 3/5] fix(renovate): fixes the actual template source, not just its rendered copy .github/renovate.json is itself generated from includes/renovate-template.jinja + template/.github/renovate.json.jinja -- the previous commits here only edited the rendered output, leaving the real source unfixed. That's exactly the class of bug this whole PR exists to prevent, and it broke both CI jobs on this PR (checks: editorconfig-checker flagged tabs in the new script; consistency: re-rendering from the still-broken source reverted both fixes). Also corrects two problems found while re-verifying against a real copier run: - copier.yaml requires the jinja2-git-dir Jinja extension, which is not a stock copier feature -- copier-flake bundles it, but a bare pip/pipx install of copier does not. The script now runs via `uvx --with jinja2-git-dir==0.5.0 copier==9.13.1`. - The previous script restored justfile/.gitignore/render-template.yaml/ project.nix via `git show HEAD:`, copied from just render's CI usage. That's correct post-commit (HEAD includes the round's changes by the time render-template.yaml runs), but wrong pre-commit: postUpgradeTasks run before Renovate's commit, so HEAD is stale by one round. Concretely, render-template.yaml has SHA-pinned actions/checkout and actions/create-github-app-token, both live targets of the native github-actions manager -- the old script would have silently reverted any same-round bump to either pin. Fixed by dropping the destructive find+rm-rf/full-regenerate step entirely (not needed for this script's narrower reconcile-only purpose) and snapshotting justfile/.gitignore from the working tree via cp before the copier run, not from git history. lib/nix/project.nix needs no handling (copier.yaml's own _skip_if_exists already protects it); render-template.yaml is never touched since it has no template counterpart. Verified end-to-end against a disposable clone: the script now renders .github/renovate.json byte-identical to the fixed sources, is idempotent on a second run, and leaves an uncommitted same-round bump to render-template.yaml's actions/checkout pin untouched. --- .github/scripts/renovate-render.sh | 49 ++++++++++++++++++---------- includes/renovate-template.jinja | 6 ++-- template/.github/renovate.json.jinja | 4 +++ 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/.github/scripts/renovate-render.sh b/.github/scripts/renovate-render.sh index e6df7aa..06ccaf8 100755 --- a/.github/scripts/renovate-render.sh +++ b/.github/scripts/renovate-render.sh @@ -10,27 +10,40 @@ # re-renders from the .jinja sources and diffs against the checked-in files. # # Re-rendering here, before Renovate commits, folds any resulting diff into -# Renovate's own commit -- no separate push/credential needed. Mirrors -# `just render`, minus the Nix devshell (unavailable in Renovate's runner); -# copier is installed via pipx in khepri-deps/renovate's workflow instead. +# Renovate's own commit -- no separate push/credential needed. +# +# Deliberately narrower than `just render`: no full wipe-and-regenerate, since +# this only needs to reconcile already-templated files, not audit for +# orphaned/removed template files (that's what the human-reviewed `consistency` +# job and `just render` are for). copier itself only touches files it manages +# (or creates none for files with no template source), so files with no +# .jinja counterpart -- like render-template.yaml -- are never touched here +# and don't need restoring. `justfile`/`.gitignore` DO have generic template +# counterparts that would clobber this repo's self-only customizations, so +# those are snapshotted from the working tree (not git HEAD -- Renovate's own +# edits this round aren't committed yet, so HEAD would be stale) and restored +# after the copy. +# +# Runs via uvx instead of the Nix devshell (unavailable in Renovate's runner). +# copier.yaml requires the jinja2-git-dir extension, which isn't a stock +# copier feature -- inject it explicitly, pinned to what copier-flake bundles. set -euo pipefail -export PATH="$HOME/.local/bin:$PATH" -command -v copier >/dev/null 2>&1 || pip install --quiet --user copier +PRESERVE=(justfile .gitignore) +SNAPSHOT_DIR=$(mktemp -d) +trap 'rm -rf "$SNAPSHOT_DIR"' EXIT + +for f in "${PRESERVE[@]}"; do + [[ -f "$f" ]] && cp "$f" "$SNAPSHOT_DIR/$(basename "$f")" +done -find . -maxdepth 1 \ - ! -name '.' ! -name '.git' ! -name '.venv' ! -name '.direnv' \ - ! -name '.serena' \ - ! -name 'template' ! -name 'includes' ! -name 'copier.yaml' \ - ! -name 'hack' ! -name 'tests' ! -name 'pytest.ini' \ - ! -name 'flake.lock' ! -name 'CHANGELOG.md' \ - -exec rm -rf {} + +uvx --with jinja2-git-dir==0.5.0 copier==9.13.1 copy --vcs-ref=HEAD --trust --defaults \ + --data-file includes/copier-answers-sample.yml -f . . -copier copy --vcs-ref=HEAD --trust --defaults \ - --data-file includes/copier-answers-sample.yml -f . . +for f in "${PRESERVE[@]}"; do + [[ -f "$SNAPSHOT_DIR/$(basename "$f")" ]] && cp "$SNAPSHOT_DIR/$(basename "$f")" "$f" +done -git show HEAD:lib/nix/project.nix > lib/nix/project.nix 2>/dev/null || true -git show HEAD:justfile > justfile 2>/dev/null || true -git show HEAD:.gitignore > .gitignore 2>/dev/null || true -git show HEAD:.github/workflows/render-template.yaml > .github/workflows/render-template.yaml 2>/dev/null || true +# copier always restamps _commit/created_on; not something Renovate ever +# legitimately touches, so reverting to the last commit is safe. git checkout -- .copier-answers.yaml 2>/dev/null || true diff --git a/includes/renovate-template.jinja b/includes/renovate-template.jinja index a04df26..7f6181f 100644 --- a/includes/renovate-template.jinja +++ b/includes/renovate-template.jinja @@ -4,16 +4,14 @@ "description": "Update SHA-pinned actions in .jinja template files", "managerFilePatterns": ["/template/.*\\.ya?ml\\.jinja$/", "/includes/.*\\.jinja$/"], "matchStrings": ["uses:\\s+(?[\\w-]+/[\\w-]+)@(?[a-f0-9]+)\\s+#\\s+(?v[\\S]+)"], - "datasourceTemplate": "github-releases", - "extractVersionTemplate": "^(?v\\d+)" + "datasourceTemplate": "github-releases" }, { "customType": "regex", "description": "Update SHA-pinned Nix flake inputs in template files", "managerFilePatterns": ["/template/flake\\.nix\\.jinja$/", "/includes/flake-extra-inputs\\.jinja$/"], "matchStrings": ["github:(?[\\w-]+/[\\w-]+)/(?[a-f0-9]+).*#\\s*(?v[\\S]+)"], - "datasourceTemplate": "github-releases", - "extractVersionTemplate": "^(?v\\d+)" + "datasourceTemplate": "github-releases" }, { "customType": "regex", diff --git a/template/.github/renovate.json.jinja b/template/.github/renovate.json.jinja index 47c39f1..b492130 100644 --- a/template/.github/renovate.json.jinja +++ b/template/.github/renovate.json.jinja @@ -1,5 +1,9 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json"{% if _is_template %}, + "postUpgradeTasks": { + "commands": ["bash .github/scripts/renovate-render.sh"], + "executionMode": "branch" + }, "customManagers": [ {% include pathjoin("includes", "renovate-template.jinja") ignore missing %} ] From 488600199c1602aa676b49cd90abd4a21fd1c981 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 4 Aug 2026 14:52:37 -0400 Subject: [PATCH 4/5] fix(just): restores the render script after the destructive render wipe .github/scripts/renovate-render.sh has no template counterpart (same category as render-template.yaml), so the render recipe's find+rm-rf step deletes it and copier never recreates it -- confirmed live on PR #22 (both checks and consistency failed the same way this fix already addressed for other self-only files). mkdir -p is required before the restore: unlike the other restored paths, .github/scripts/ has no other template-managed sibling, so copier never creates the directory at all and a bare 'git show HEAD:... > path' redirect fails silently with no such directory. Verified against a disposable clone by running the exact destructive wipe + copier copy + restore sequence the consistency job runs, then confirming git status is clean modulo .copier-answers.yaml (already handled by the existing render-template.yaml/consistency steps). --- justfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/justfile b/justfile index 815e173..51ed3f7 100644 --- a/justfile +++ b/justfile @@ -25,6 +25,9 @@ render: git show HEAD:justfile > justfile 2>/dev/null || true git show HEAD:.gitignore > .gitignore 2>/dev/null || true git show HEAD:.github/workflows/render-template.yaml > .github/workflows/render-template.yaml 2>/dev/null || true + mkdir -p .github/scripts + git show HEAD:.github/scripts/renovate-render.sh > .github/scripts/renovate-render.sh 2>/dev/null || true + chmod +x .github/scripts/renovate-render.sh 2>/dev/null || true # Run unit tests test: From 958919090561bace74c70d2d70dfe6a563567dcc Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 4 Aug 2026 16:03:28 -0400 Subject: [PATCH 5/5] fix(renovate): switches to github-tags, drops postUpgradeTask reconciliation Root-caused the actual native-manager resolution path (not just observed behavior): for a SHA pin with a version-looking comment, Renovate's github-actions extractor explicitly skips GithubDigestDatasource and falls through to GithubTagsDatasource -- not github-releases. The .jinja customManagers were pointed at github-releases, which is a strict subset of tags (every Release implies an underlying tag, not every tag has a Release). Switching to github-tags makes both resolution paths structurally identical -- same datasource, same (default, unset) versioning, same untransformed currentValue -- rather than merely observed to agree for this one dependency. With that in place, the postUpgradeTask reconciliation step added earlier is no longer solving a real problem: it existed specifically to paper over the two managers disagreeing, and they no longer can by construction. Reverts it entirely (script, postUpgradeTasks config, justfile restore-list entry) rather than keeping unneeded complexity -- it depended on uvx/jinja2-git-dir/copier version pins that would need maintaining, and a justfile restore-list that's already proven easy to forget to update (this is the second time a self-only file needed adding to it in this same PR). If the datasource alignment ever proves insufficient for some edge case, the existing consistency check plus a manual "just render" fix-up (as already happened once, in PR #19) is a sufficient, simpler safety net. Verified against a disposable clone running the exact destructive wipe + copier copy + restore sequence the consistency job runs. --- .github/renovate.json | 8 ++--- .github/scripts/renovate-render.sh | 49 ---------------------------- includes/renovate-template.jinja | 4 +-- justfile | 3 -- template/.github/renovate.json.jinja | 4 --- 5 files changed, 4 insertions(+), 64 deletions(-) delete mode 100755 .github/scripts/renovate-render.sh diff --git a/.github/renovate.json b/.github/renovate.json index cecca50..70bbcbc 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,23 +1,19 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "postUpgradeTasks": { - "commands": ["bash .github/scripts/renovate-render.sh"], - "executionMode": "branch" - }, "customManagers": [ { "customType": "regex", "description": "Update SHA-pinned actions in .jinja template files", "managerFilePatterns": ["/template/.*\\.ya?ml\\.jinja$/", "/includes/.*\\.jinja$/"], "matchStrings": ["uses:\\s+(?[\\w-]+/[\\w-]+)@(?[a-f0-9]+)\\s+#\\s+(?v[\\S]+)"], - "datasourceTemplate": "github-releases" + "datasourceTemplate": "github-tags" }, { "customType": "regex", "description": "Update SHA-pinned Nix flake inputs in template files", "managerFilePatterns": ["/template/flake\\.nix\\.jinja$/", "/includes/flake-extra-inputs\\.jinja$/"], "matchStrings": ["github:(?[\\w-]+/[\\w-]+)/(?[a-f0-9]+).*#\\s*(?v[\\S]+)"], - "datasourceTemplate": "github-releases" + "datasourceTemplate": "github-tags" }, { "customType": "regex", diff --git a/.github/scripts/renovate-render.sh b/.github/scripts/renovate-render.sh deleted file mode 100755 index 06ccaf8..0000000 --- a/.github/scripts/renovate-render.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# Renovate postUpgradeTask, run before Renovate's own commit is made. -# -# Renovate's native github-actions manager and this repo's .jinja customManagers -# (.github/renovate.json) can each resolve a different version string for the -# same SHA-pinned action/flake input -- e.g. the native manager writes the exact -# newest release tag (v7.0.1) into root .yaml files, while the .jinja source -# resolves independently for the same dependency. Left alone, this makes the -# `consistency` job in pr-checks.yaml fail on every non-major bump, since it -# re-renders from the .jinja sources and diffs against the checked-in files. -# -# Re-rendering here, before Renovate commits, folds any resulting diff into -# Renovate's own commit -- no separate push/credential needed. -# -# Deliberately narrower than `just render`: no full wipe-and-regenerate, since -# this only needs to reconcile already-templated files, not audit for -# orphaned/removed template files (that's what the human-reviewed `consistency` -# job and `just render` are for). copier itself only touches files it manages -# (or creates none for files with no template source), so files with no -# .jinja counterpart -- like render-template.yaml -- are never touched here -# and don't need restoring. `justfile`/`.gitignore` DO have generic template -# counterparts that would clobber this repo's self-only customizations, so -# those are snapshotted from the working tree (not git HEAD -- Renovate's own -# edits this round aren't committed yet, so HEAD would be stale) and restored -# after the copy. -# -# Runs via uvx instead of the Nix devshell (unavailable in Renovate's runner). -# copier.yaml requires the jinja2-git-dir extension, which isn't a stock -# copier feature -- inject it explicitly, pinned to what copier-flake bundles. -set -euo pipefail - -PRESERVE=(justfile .gitignore) -SNAPSHOT_DIR=$(mktemp -d) -trap 'rm -rf "$SNAPSHOT_DIR"' EXIT - -for f in "${PRESERVE[@]}"; do - [[ -f "$f" ]] && cp "$f" "$SNAPSHOT_DIR/$(basename "$f")" -done - -uvx --with jinja2-git-dir==0.5.0 copier==9.13.1 copy --vcs-ref=HEAD --trust --defaults \ - --data-file includes/copier-answers-sample.yml -f . . - -for f in "${PRESERVE[@]}"; do - [[ -f "$SNAPSHOT_DIR/$(basename "$f")" ]] && cp "$SNAPSHOT_DIR/$(basename "$f")" "$f" -done - -# copier always restamps _commit/created_on; not something Renovate ever -# legitimately touches, so reverting to the last commit is safe. -git checkout -- .copier-answers.yaml 2>/dev/null || true diff --git a/includes/renovate-template.jinja b/includes/renovate-template.jinja index 7f6181f..b65e43d 100644 --- a/includes/renovate-template.jinja +++ b/includes/renovate-template.jinja @@ -4,14 +4,14 @@ "description": "Update SHA-pinned actions in .jinja template files", "managerFilePatterns": ["/template/.*\\.ya?ml\\.jinja$/", "/includes/.*\\.jinja$/"], "matchStrings": ["uses:\\s+(?[\\w-]+/[\\w-]+)@(?[a-f0-9]+)\\s+#\\s+(?v[\\S]+)"], - "datasourceTemplate": "github-releases" + "datasourceTemplate": "github-tags" }, { "customType": "regex", "description": "Update SHA-pinned Nix flake inputs in template files", "managerFilePatterns": ["/template/flake\\.nix\\.jinja$/", "/includes/flake-extra-inputs\\.jinja$/"], "matchStrings": ["github:(?[\\w-]+/[\\w-]+)/(?[a-f0-9]+).*#\\s*(?v[\\S]+)"], - "datasourceTemplate": "github-releases" + "datasourceTemplate": "github-tags" }, { "customType": "regex", diff --git a/justfile b/justfile index 51ed3f7..815e173 100644 --- a/justfile +++ b/justfile @@ -25,9 +25,6 @@ render: git show HEAD:justfile > justfile 2>/dev/null || true git show HEAD:.gitignore > .gitignore 2>/dev/null || true git show HEAD:.github/workflows/render-template.yaml > .github/workflows/render-template.yaml 2>/dev/null || true - mkdir -p .github/scripts - git show HEAD:.github/scripts/renovate-render.sh > .github/scripts/renovate-render.sh 2>/dev/null || true - chmod +x .github/scripts/renovate-render.sh 2>/dev/null || true # Run unit tests test: diff --git a/template/.github/renovate.json.jinja b/template/.github/renovate.json.jinja index b492130..47c39f1 100644 --- a/template/.github/renovate.json.jinja +++ b/template/.github/renovate.json.jinja @@ -1,9 +1,5 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json"{% if _is_template %}, - "postUpgradeTasks": { - "commands": ["bash .github/scripts/renovate-render.sh"], - "executionMode": "branch" - }, "customManagers": [ {% include pathjoin("includes", "renovate-template.jinja") ignore missing %} ]