A tag is a mutable pointer. `actions/checkout@v7` is whatever the
publisher last moved v7 to, so using one is not trusting the version that
was reviewed -- it is trusting every future version, including whatever
is pushed by whoever compromises the publisher's account. That is the
shape of the tj-actions/changed-files compromise: no repository changed a
line, the tags moved underneath them, and the action began dumping runner
memory to the logs.
Each `uses:` now carries the full 40-character SHA with its release in a
trailing comment. Read the comment for the version; the SHA is what runs.
Dependabot already covers github-actions weekly and updates both halves
together, so keeping current costs nothing.
The dataaxiom cleanup action was already pinned -- it is handed
`packages: write` and deletes things, so it was worth doing early -- and
only picks up the trailing-version convention here. Its comment loses the
"rather than a moving major tag" framing, which is no longer what makes
it different from its neighbors now that they are all pinned too.
The two `uses: ./.github/workflows/...` entries are local paths, not
actions: they always resolve within the commit already running and there
is no SHA to pin.
Three pins and a types package all described Node 22, and moving any one
of them alone puts the build somewhere the others are not: @types/node
on its own would typecheck against APIs the runtime does not have, and
the base image on its own would ship a major CI never exercised. So
ci.yml, publish.yml, release.yml, both Dockerfile stages and
@types/node move in one change.
Worth knowing before this is deployed: 26 is Current, not LTS. node:26-
alpine reports lts=none, where 24-alpine is Krypton and the 22-alpine we
are leaving is Jod. 26 is due to become Active LTS in October. Nothing
here needs 26 over 24 -- the pins are a single number if the LTS line is
preferred.
engines stays at >=20.19, which is the floor for running ihasmail rather
than the version we build it on; the README's recommendation follows CI
to 26.
Checked on the runtime, not just in CI: the image builds on 26-alpine,
starts, and answers /api/health, and the login, SSE and body-carrying
POST checks from the node-server upgrade pass against a server on
26.8.1.
CI triggers on a push to main and on pull requests, and on nothing else.
That left no way to put a check on 7a1e5ee -- the commit production is
running -- after GitHub's Actions outage on 2026-08-26 orphaned every run
created during it.
Those runs are not merely slow. GitHub accepted them, allocated zero
jobs, and left them in a state its own API contradicts itself about:
gh run rerun -> "cannot be rerun; This workflow is already running"
gh run cancel -> "Cannot cancel a workflow run that is completed"
gh api -> status=queued, conclusion=null, jobs=0
Neither recoverable nor clearable, and with no manual trigger the only
remaining option would have been an empty commit pushed to main to move
the ref -- which is both a junk commit and against how changes land here.
workflow_dispatch also covers the ordinary case of wanting a check on a
commit that predates a CI change.