Skip to content

ci: run update_pins.py weekly and open a PR - #32

Merged
sbp-bvanb merged 2 commits into
mainfrom
ci/pins-updater
Aug 29, 2026
Merged

ci: run update_pins.py weekly and open a PR#32
sbp-bvanb merged 2 commits into
mainfrom
ci/pins-updater

Conversation

@sbp-bvanb

Copy link
Copy Markdown
Collaborator

Adds .github/workflows/pins-updater.yml: every Monday (and on demand via workflow_dispatch, with optional --soak / --block-major-bumps overrides) it runs uv run update_pins.py and, if any pin moved, force-pushes the bump/pins branch and opens or refreshes a single PR carrying the script's full report.

One long-lived branch rather than a dated branch per run: a stale pins PR proposes versions the next refresh has already superseded, so the newest run should replace the open PR instead of stacking another one beside it.

uv is bootstrapped from pins/uv.env — the same URL and sha256 the Dockerfile installs, verified with sha256sum -c — instead of a third-party setup action, so the workflow leans on the repo's own pinned artifact and there is one less action to keep pinned.

The PR is opened with secrets.PINS_UPDATER_TOKEN when that secret exists, falling back to GITHUB_TOKEN. The fallback works but GitHub does not trigger pull_request workflows for PRs opened with GITHUB_TOKEN, so the docker build and smoke matrix that actually validate a pin bump stay idle until someone pushes an empty commit or reopens the PR. Both paths are documented in the workflow and in README.

Not run live yet. YAML parses, yamllint is clean apart from the SHA-comment spacing warning every existing workflow here shares, and all four run blocks pass bash -n. shellcheck was unavailable in the authoring environment; CI's shellcheck job scans files on disk, not YAML run blocks, so it would not cover them either.

@sbp-bvanb
sbp-bvanb force-pushed the ci/pins-updater branch 2 times, most recently from 7c989a6 to 307c8b3 Compare August 24, 2026 21:30
@sbp-bvanb
sbp-bvanb requested a review from dtump August 24, 2026 21:30

@dtump dtump left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NEEDS WORK · 0 block, 4 fix, 1 later

I agree with the functionality and with most of the design calls (single long-lived branch, bootstrapping from pins/uv.env, dispatch inputs through env). I walked the run path step by step rather than just reading it — notes on what I verified are at the bottom. Four things to fix before merge.

Fix before merge

1. The OpenSpec change is missing, and this repo pre-registered it

openspec/changes/automate-version-pins/tasks.md:51, under "## 8. Deferred to dedicated follow-up changes":

  • 8.2 Weekly automated refresh (CI/cron) opening a PR, so freshness doesn't depend on someone remembering to run the script — the fragments + script were built to support it

That is this PR, verbatim. It ships with no change directory, no delta, and the box still unticked. CONTRIBUTING.md:11 asks for a change proposal for non-trivial behaviour changes, and the plan for this specific work named a dedicated follow-up change.

I did consider whether CI plumbing is exempt — none of the eleven prior commits touching .github/workflows/ carried OpenSpec artifacts, and that precedent is fine for linting and action bumps. This is different: it adds a second, unattended actor to the version-pin-refresh capability plus a new operator-configured secret, and every requirement in that capability is currently phrased "the operator runs the refresh script".

What I'd like: one ADDED Requirement: Scheduled unattended refresh with scenarios for the changed-pins and unchanged-pins paths, and 8.2 ticked pointing at it. Sequencing note — openspec/specs/version-pin-refresh/ does not exist yet (automate-version-pins is 26/28 tasks and unarchived), so a standalone change has no synced capability to delta against. Either archive automate-version-pins first and add the follow-up change on top, or fold the requirement into that change and tick 8.2 there. Your call which, but the requirement needs to exist somewhere before the archive/sync happens, otherwise the synced spec will describe pin refresh as a manual, operator-only flow on the day the automation goes live.

2. The fallback token path is the one that will actually run, and it produces an unmergeable PR

.github/workflows/pins-updater.yml:111 and README.md:280 describe the GITHUB_TOKEN fallback as leaving CI idle. Two things sharpen that:

  • GET /repos/schubergphilis/claude-docker/actions/secrets returns {"total_count":0}PINS_UPDATER_TOKEN does not exist, so the fallback is what ships on merge.
  • The main protection ruleset requires the Validate and Docker build (validate, no push) contexts. On a PR opened with GITHUB_TOKEN those never report at all, so the merge button is blocked on "Expected — waiting for status to be reported".

So the weekly PR is not merely uninformative, it is unmergeable until a human pushes an empty commit or closes and reopens it. Please create the secret before merging (a fine-grained PAT scoped to this repo with contents: write + pull_requests: write, rather than a classic PAT with full repo), and reword the README note from "won't run" to "can't be merged until".

3. The link in the generated PR body 404s

.github/workflows/pins-updater.yml:132 emits a repo-relative markdown link. GitHub only rewrites relative links in files rendered from the repo, not in issue or PR bodies — POST /markdown with mode=gfm and context set leaves it untouched:

see <a href=".github/workflows/pins-updater.yml">x</a> here

On a PR page the browser resolves that against /<org>/<repo>/pull/, giving https://github.com/schubergphilis/claude-docker/pull/.github/workflows/pins-updater.yml — a 404 in every weekly PR body. Use ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/main/.github/workflows/pins-updater.yml. (The README.md:278 link is fine — that one is rendered from a repo file and does get rewritten.)

4. The uv bootstrap step buys nothing

.github/workflows/pins-updater.yml:53-66 re-implements Dockerfile:159-171 — curl, sha256sum -c, tar, install. The comment justifies it against a third-party setup action, but that isn't the alternative on the table: update_pins.py declares requires-python = ">=3.11" and dependencies = [], the runner ships python3.12, and ci.yml:36 and ci.yml:105 already invoke the very same script as python3 update_pins.py. Dropping the step removes ten lines, removes the only step that can fail before any work happens, and removes a hard-coded uv-x86_64-unknown-linux-gnu/ archive path that now has to be kept in step with the Dockerfile's $(uname -m)-derived one.

If you want the uv bootstrap anyway for parity with the documented operator command, say that in the comment instead — the current rationale argues against an option nobody was proposing.

Later, not blocking

.github/workflows/pins-updater.yml:127 — the force-push is unconditional, so a week in which nothing new clears the soak still produces a fresh commit SHA over a byte-identical tree. The main protection ruleset sets dismiss_stale_reviews_on_push: true, so an approved-but-unmerged pins PR silently loses its approval every Monday and re-runs the required build. Cheap guard if you ever want it: fetch the remote branch and skip push + edit when HEAD^{tree} matches. Not blocking — most weeks the tree genuinely differs (claude-code alone moves often enough), and when it does, dismissing the review is the correct behaviour.

Strengths

Things I checked that hold up, so they don't get "fixed" later by someone who didn't:

  • install into /usr/local/bin without sudo works. I went looking for this one expecting a failure. The runner image build runs sudo chmod -R 777 /usr/local/bin in install-nodejs.sh, so the step is fine as written.
  • Dispatch inputs go through env and are never interpolated into a script body — and inputs.soak || '7' correctly covers both the schedule trigger (no inputs) and a dispatcher who clears the field. zizmor is clean, and it would have caught the other spelling.
  • persist-credentials: false plus an explicit push URL is the right pairing given the token may be a PAT rather than the job token, and the job-level permissions block is scoped rather than inherited.
  • The automated PR passes this repo's own PR gates. The commit body is 117 characters, which would trip body-max-line-length under stock config-conventional — mcvs-general-action's commitlint.config.mjs disables that rule, so it passes. lint-git's behind-main check passes because the branch is rebuilt from main every run, and the generated body is non-empty for mcvs-pr-validation-action.
  • Failure modes are non-catastrophic. update_pins.py returns non-zero only on real resolve/download/integrity failures, so pipefail means a bad refresh can never reach the commit step; print_reminders, ubuntu_current_digest and go_latest_stable are all best-effort and cannot fail the job; every request carries timeout=30; and passing GITHUB_TOKEN to the script is safe because its redirect handler strips Authorization when a redirect crosses hosts.
  • The rationale comments are worth keeping. The concurrency note, the single-branch justification, and the tee-for-both-log-and-body note all answer the question a reader would actually have.

@dtump

dtump commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Correction to my review: openspec/specs/version-pin-refresh/ does exist on main#30 synced it about fifteen minutes before I posted, and I was reading a stale checkout. So the sequencing caveat in Fix 1 falls away: a standalone follow-up change can delta against the synced capability directly, no need to archive automate-version-pins first. The rest of that finding stands — the change is still open with 8.2 unticked.

@sbp-bvanb
sbp-bvanb force-pushed the ci/pins-updater branch 2 times, most recently from 1597a78 to 2b336fb Compare August 25, 2026 09:48
@sbp-bvanb
sbp-bvanb requested a review from dtump August 25, 2026 11:19
Drop the uv bootstrap step: update_pins.py is stdlib-only with an empty
dependency set and the runner's python3.12 already satisfies its
requires-python, so uv had nothing to resolve. ci.yml already invokes the
script as python3. Removes the only step that could fail before any work
started, plus a hard-coded archive path that had to track the Dockerfile's
$(uname -m)-derived one.

Build the workflow link in the generated PR body from GITHUB_SERVER_URL and
GITHUB_REPOSITORY. GitHub rewrites repo-relative markdown links only in files
rendered from the repo, so the relative one resolved against the pull request
URL and 404'd in every weekly body.

State the real cost of the GITHUB_TOKEN fallback in both the workflow comment
and README: main's ruleset requires the Validate and Docker build contexts,
which never report on a PR opened with GITHUB_TOKEN, so the PR is unmergeable
rather than merely unvalidated until a human intervenes. Name the fine-grained
PAT scope PINS_UPDATER_TOKEN should carry.

Add the OpenSpec change automate-version-pins deferred as task 8.2, and tick
it. version-pin-refresh gains a Scheduled unattended refresh requirement
covering the changed-pins and unchanged-pins paths, PR replacement, on-demand
overrides, and that the run never rewrites a manual pin — without it the
capability would sync describing pin refresh as operator-only on the day the
automation goes live.
@sbp-bvanb

Copy link
Copy Markdown
Collaborator Author

Thanks — all four addressed in c8c069f, and the branch is rebased onto current main (it was based on 93a2d70, i.e. behind, which is also why lint-git had nothing useful to say before).

1. OpenSpec change. Taking the standalone route your correction unblocked: openspec/changes/schedule-pin-refresh/ deltas straight against the synced openspec/specs/version-pin-refresh/spec.md, no archive ordering needed. One ADDED Requirement: Scheduled unattended refresh with the two scenarios you asked for plus four more that fell out of writing it down — PR replacement (the single-branch decision is a requirement, not an implementation detail), on-demand overrides, failed-run-proposes-nothing, and manual-pins-reported-never-rewritten. The requirement also states that the PR must be opened such that the repo's own checks run on it, which is where finding 2 stops being a docs nit and becomes a spec obligation. automate-version-pins 8.2 is ticked and names the change. openspec validate schedule-pin-refresh --strict passes.

I left it unarchived deliberately. Tasks 4.5–4.7 are the secret, a live dispatch run, and the unchanged-pins path — none of which can honestly be ticked yet, and archiving would sync a requirement whose verification hasn't happened. It sits alongside automate-version-pins until then; both deltas are additive to the same capability, so whichever archives first is fine.

2. Token. Reworded in both places from "CI won't run" to the actual consequence — the required Validate and Docker build (validate, no push) contexts never report, so the PR is unmergeable, not merely unvalidated, until someone pushes an empty commit or reopens it. Both now name the scope: fine-grained PAT, this repo, contents: write + pull requests: write, rather than a classic PAT with full repo.

The secret itself I can't create — flagging it as a merge prerequisite rather than a follow-up. proposal.md lists it under Impact as an operational prerequisite and tasks.md 4.5 tracks it unticked. Merging before it exists gets you a weekly PR nobody can merge.

3. Link. Now ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/main/… via a workflow_url variable, with a comment recording why relative fails specifically in a PR body. Left the README.md link relative — as you noted, that one is rendered from a repo file and does get rewritten.

4. uv bootstrap. Dropped, and you're right that the comment was arguing against an option nobody proposed. python3 update_pins.py, same as ci.yml:62 and ci.yml:105. That removes the step, the hard-coded uv-x86_64-unknown-linux-gnu/ path, and the only failure mode that could hit before any work started — the job now has no install step at all. The commit body and PR body said uv run too; both updated.

Later item. Left out, per your own reasoning — but written down in design.md under the single-branch decision as a known gap with the HEAD^{tree} guard as the fix, so the next reader finds a deliberate choice rather than an oversight.

One drift I picked up while in there: the generated PR body listed the manual pins as "nodejs, go, ubuntu base". task joined that set when task-pin-reminder archived, so it's in the list now.

On the strengths section — thanks for writing down what you checked, particularly the /usr/local/bin chmod in install-nodejs.sh and the body-max-line-length exemption. Both are exactly the kind of thing that gets "fixed" later by someone who re-derives the concern and not the answer.

Still not run live. Verified offline: yamllint clean apart from the SHA-comment spacing warning every workflow here shares, all three run: blocks pass bash -n, and the full check suite including zizmor is green on c8c069f.

@sbp-bvanb
sbp-bvanb merged commit 25b47e7 into main Aug 29, 2026
8 checks passed
@sbp-bvanb
sbp-bvanb deleted the ci/pins-updater branch August 29, 2026 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants