From 514d30e27358752df0b7b3937c90a06b4d3b1313 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 26 Jul 2026 14:58:57 +0300 Subject: [PATCH 1/5] ci: add a scheduled updater for the template's action pins Dependabot cannot reach the pins under project_name/.github/workflows/. Its github-actions ecosystem only parses *.yml/*.yaml, and three of the four template workflows are .jinja. Renaming them does not help either, because Jinja control blocks inside a `steps:` sequence are not parseable YAML (dependabot-core#5970 tracks the general gap). Until now those pins only moved when someone bumped them by hand, which is how they drifted. pinact rewrites line by line rather than parsing YAML, so the templating is irrelevant to it, including the file named "{% if in_pypi %}build-and-publish.yml{% endif %}.jinja". Scoped to project_name/ on purpose: the root workflows stay with Dependabot, since two updaters over the same lines would produce competing PRs. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JP6NCazpaMz2Xfejds3mxq --- .github/workflows/update-template-pins.yml | 82 ++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .github/workflows/update-template-pins.yml diff --git a/.github/workflows/update-template-pins.yml b/.github/workflows/update-template-pins.yml new file mode 100644 index 0000000..873c1d8 --- /dev/null +++ b/.github/workflows/update-template-pins.yml @@ -0,0 +1,82 @@ +name: Update template action pins + +# Dependabot cannot see the pins in project_name/: the github-actions ecosystem +# only parses *.yml/*.yaml, and three of the four template workflows are .jinja. +# Renaming them is not an option either, since Jinja control blocks inside a +# `steps:` sequence are not parseable YAML. pinact rewrites line by line, so the +# templating is irrelevant to it. +# +# The root workflows are deliberately left to Dependabot. Running both updaters +# over the same files would produce competing PRs against the same lines. + +on: + schedule: + # quarterly, matching the dependabot cadence in .github/dependabot.yml + - cron: '0 6 1 1,4,7,10 *' + workflow_dispatch: + +permissions: + contents: read + +env: + PINACT_VERSION: v4.1.0 + +jobs: + update-pins: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install pinact + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release download "$PINACT_VERSION" \ + --repo suzuki-shunsuke/pinact \ + --pattern pinact_linux_amd64.tar.gz \ + --pattern 'pinact_*_checksums.txt' \ + --dir "$RUNNER_TEMP/pinact" + cd "$RUNNER_TEMP/pinact" + sha256sum --ignore-missing --check pinact_*_checksums.txt + tar -xzf pinact_linux_amd64.tar.gz + echo "$RUNNER_TEMP/pinact" >> "$GITHUB_PATH" + + - name: Update template action pins + env: + GITHUB_TOKEN: ${{ github.token }} + # Given no file arguments pinact scans .github/workflows only, which + # would silently retarget this step at the root workflows, hence -r to + # fail closed if the glob ever stops matching. -z/-0 is required because + # one template is named "{% if in_pypi %}build-and-publish.yml{% endif %}.jinja". + # + # Twice, because a pin whose comment is a moving tag rather than a + # version (`# release/v1`) takes one pass to resolve the comment to the + # version the SHA actually is, and only then can the next pass see that + # a newer release exists. The second pass is a no-op once converged. + run: | + for _ in 1 2; do + git ls-files -z 'project_name/.github/workflows/*' \ + | xargs -0 -r pinact run --update + done + + - name: Create pull request + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "ci: update action pins in the template" + title: "⬆️ Update action pins in the template" + body: | + Automated update of the SHA-pinned actions under `project_name/.github/workflows/`, + which Dependabot cannot reach. Each pin is moved to the latest release + and its `# vX.Y.Z` comment is rewritten to match. + + Review the diff as you would a Dependabot PR: check the release notes + for anything behavioural, especially on major bumps. + branch: ci/template-action-pins + delete-branch: true + labels: dependencies From 23e8c856e565fcc93ca2d195fa96111068a7afe4 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 26 Jul 2026 15:13:42 +0300 Subject: [PATCH 2/5] ci: run the template pin updater monthly instead of quarterly The original cadence just mirrored dependabot's. That matching is not worth much here: this updater has no cooldown equivalent to dependabot's cooldown.default-days, so the tradeoff is different. A monthly run keeps each PR down to a handful of pins, which is the difference between reviewing a bump and rubber-stamping one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JP6NCazpaMz2Xfejds3mxq --- .github/workflows/update-template-pins.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-template-pins.yml b/.github/workflows/update-template-pins.yml index 873c1d8..abc8156 100644 --- a/.github/workflows/update-template-pins.yml +++ b/.github/workflows/update-template-pins.yml @@ -11,8 +11,8 @@ name: Update template action pins on: schedule: - # quarterly, matching the dependabot cadence in .github/dependabot.yml - - cron: '0 6 1 1,4,7,10 *' + # monthly + - cron: '0 6 1 * *' workflow_dispatch: permissions: From b4268ac7e907b7e5ee8dbfaa475725fcd9156172 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 26 Jul 2026 15:22:00 +0300 Subject: [PATCH 3/5] ci: gate template pin updates behind a 7-day min-age Mirrors cooldown.default-days in .github/dependabot.yml, which the updater otherwise had no equivalent of. --min-age carries a second behaviour worth keeping rather than suppressing (config `min_age.always: false` would drop it): the run fails outright when a pin already in the tree is younger than the threshold. The updater itself can never introduce one, since it will not move a pin onto a release that young, so a pin that fresh arrived by hand. That is the case worth stopping for. It fails right now for exactly that reason: setup-uv v9.0.0 was pinned in 80fe49b while its commit was 5 days old. It crosses the line on 2026-07-28, before the next scheduled run on 2026-08-01. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JP6NCazpaMz2Xfejds3mxq --- .github/workflows/update-template-pins.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/update-template-pins.yml b/.github/workflows/update-template-pins.yml index abc8156..3ef6106 100644 --- a/.github/workflows/update-template-pins.yml +++ b/.github/workflows/update-template-pins.yml @@ -49,10 +49,18 @@ jobs: - name: Update template action pins env: GITHUB_TOKEN: ${{ github.token }} - # Given no file arguments pinact scans .github/workflows only, which - # would silently retarget this step at the root workflows, hence -r to - # fail closed if the glob ever stops matching. -z/-0 is required because - # one template is named "{% if in_pypi %}build-and-publish.yml{% endif %}.jinja". + # Given no file arguments pinact scans .github/workflows instead, so -r + # stops it from ever retargeting the root workflows should the glob stop + # matching (it no-ops rather than falling back). -z/-0 is required + # because one template is named + # "{% if in_pypi %}build-and-publish.yml{% endif %}.jinja". + # + # --min-age does two separate things. It refuses to move a pin onto a + # release whose commit is younger than 7 days, which is the cooldown + # equivalent of cooldown.default-days in .github/dependabot.yml. It also + # fails the run when a pin already in the tree is that young. The second + # half is wanted: the updater itself can never introduce one, so a pin + # that fresh got there by hand and is worth stopping for. # # Twice, because a pin whose comment is a moving tag rather than a # version (`# release/v1`) takes one pass to resolve the comment to the @@ -61,7 +69,7 @@ jobs: run: | for _ in 1 2; do git ls-files -z 'project_name/.github/workflows/*' \ - | xargs -0 -r pinact run --update + | xargs -0 -r pinact run --update --min-age 7 done - name: Create pull request From 0e304e049e87e7ba63c3e3774db8cb2581d80681 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 26 Jul 2026 15:26:09 +0300 Subject: [PATCH 4/5] prune comments --- .github/workflows/update-template-pins.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/update-template-pins.yml b/.github/workflows/update-template-pins.yml index 3ef6106..bfeadac 100644 --- a/.github/workflows/update-template-pins.yml +++ b/.github/workflows/update-template-pins.yml @@ -58,9 +58,9 @@ jobs: # --min-age does two separate things. It refuses to move a pin onto a # release whose commit is younger than 7 days, which is the cooldown # equivalent of cooldown.default-days in .github/dependabot.yml. It also - # fails the run when a pin already in the tree is that young. The second - # half is wanted: the updater itself can never introduce one, so a pin - # that fresh got there by hand and is worth stopping for. + # fails the run when a pin already in the tree is that young, which is + # deliberate: the updater cannot introduce a pin that fresh, so one that + # appears got there by hand. # # Twice, because a pin whose comment is a moving tag rather than a # version (`# release/v1`) takes one pass to resolve the comment to the From ed6c1bc3bf9d59a3ddb46a8c98b9ca365a2329cd Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 26 Jul 2026 15:59:44 +0300 Subject: [PATCH 5/5] comment cleanup --- .github/workflows/update-template-pins.yml | 35 ++++------------------ 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/.github/workflows/update-template-pins.yml b/.github/workflows/update-template-pins.yml index bfeadac..6ab6ac8 100644 --- a/.github/workflows/update-template-pins.yml +++ b/.github/workflows/update-template-pins.yml @@ -1,18 +1,12 @@ name: Update template action pins -# Dependabot cannot see the pins in project_name/: the github-actions ecosystem -# only parses *.yml/*.yaml, and three of the four template workflows are .jinja. -# Renaming them is not an option either, since Jinja control blocks inside a -# `steps:` sequence are not parseable YAML. pinact rewrites line by line, so the -# templating is irrelevant to it. -# -# The root workflows are deliberately left to Dependabot. Running both updaters -# over the same files would produce competing PRs against the same lines. +# Dependabot cannot see the pins under project_name/, +# since the github-actions ecosystem only parses *.yml/*.yaml. +# We use pinact instead. on: schedule: - # monthly - - cron: '0 6 1 * *' + - cron: '0 6 1 * *' # monthly workflow_dispatch: permissions: @@ -49,27 +43,10 @@ jobs: - name: Update template action pins env: GITHUB_TOKEN: ${{ github.token }} - # Given no file arguments pinact scans .github/workflows instead, so -r - # stops it from ever retargeting the root workflows should the glob stop - # matching (it no-ops rather than falling back). -z/-0 is required - # because one template is named - # "{% if in_pypi %}build-and-publish.yml{% endif %}.jinja". - # - # --min-age does two separate things. It refuses to move a pin onto a - # release whose commit is younger than 7 days, which is the cooldown - # equivalent of cooldown.default-days in .github/dependabot.yml. It also - # fails the run when a pin already in the tree is that young, which is - # deliberate: the updater cannot introduce a pin that fresh, so one that - # appears got there by hand. - # - # Twice, because a pin whose comment is a moving tag rather than a - # version (`# release/v1`) takes one pass to resolve the comment to the - # version the SHA actually is, and only then can the next pass see that - # a newer release exists. The second pass is a no-op once converged. + # 2 passes: moving tag (`# release/v1`) -> SHA version -> newer release run: | for _ in 1 2; do - git ls-files -z 'project_name/.github/workflows/*' \ - | xargs -0 -r pinact run --update --min-age 7 + pinact run --update --min-age 7 project_name/.github/workflows/* done - name: Create pull request