feat(setup-semstat): extract the semstat installer into a reusable action - #234
Merged
Conversation
sydorovdmytro
force-pushed
the
devops-1369/setup-semstat-action
branch
from
August 20, 2026 21:43
de5bd88 to
9d63339
Compare
vcauesantos
approved these changes
Aug 25, 2026
…tion The installer, its suite and the release pin move out of semver-validation so that anything needing semstat installs it the same way instead of carrying a copy. The script keeps its own bats suite, which serves a release from disk through a stubbed curl and cosign, so the download, the checksum verification and the version cross-check all run for real. Two things arrive with the move. The install directory goes on GITHUB_PATH, because every consumer so far calls semstat from inside a bash function or a `while read` loop where a step output is not in scope, and the path output is kept for callers that would rather name the binary. And verify-signature cosign-verifies checksums.txt against semstat's release workflow at the exact tag before reading it, off by default because it costs a cosign install on the job. The two markers answer different questions and both are kept: the digest says the cached binary is the one that passed, the signature marker says the release it came out of was checked. A step asking to verify is not served by whatever an earlier unverified step left behind. The stale signature claim is dropped before this run's binary lands, so a run killed mid-install leaves a binary with no claim rather than a claim with no basis. DEFAULT_VERSION in the script is the pin, read by a renovate customManager, because the script is an entry point in its own right: an action in this repository runs it directly rather than pinning this action by SHA. Two defaults would be two pins that can disagree. The jq gate does not come along. jq is what semver-validation reads semstat's output with, not something installing semstat needs, and report.sh already names it with the same error.
…ning it A sibling in the same repository cannot be pinned correctly: the commit that adds the dependency is always newer than any commit available to pin, so the pin names a commit that predates the change needing it and keeps running it while fixes to the installer land next to it. github.action_path is inside a full checkout of this repository, so running the script from there ships both actions from one commit and removes the drift class rather than guarding against it. A relative `uses: ./...` still does not work, which is why a pin was the alternative at all. There is no semstat_version input any more; the pin lives with the installer, so Renovate opens one bump rather than one per action that runs semstat. Nothing pins semver-validation/v4 yet, so the input goes rather than being deprecated. verify-signature is a passthrough. The installer documents verification as the thing to turn on for jobs that publish, and this action is what those jobs actually call, so the policy was otherwise unreachable.
The release section taught `git tag -f` plus `--force` as the house idiom with no counterpart, which is the mistake this stack nearly shipped: advancing a floating tag reaches every caller pinned to it on their next run, with no version for them to notice. That is what you want for a fix and not for a change that adds a runner requirement, network egress, a token, a permission, or a write to the job environment. Records semver-validation as the worked example, and the gh search that answers who pins a tag before it is touched.
…ry failure Dropping the semstat_version input left SEMSTAT_VERSION unbound in semver-validation's install step. Composite steps inherit the job's environment, so a caller setting that name at workflow or job level, or an earlier step writing it to GITHUB_ENV, silently chose which semstat release the action installed. That is the same hole the SEMSTAT_BASE_URL clearing beside it exists to close, and it is now closed the same way, in both actions and in the recipe the README gives for running the script from a sibling. The bundle fetch reported every curl failure as a release that publishes no bundle, so a runner whose proxy allows github.com but not the objects.githubusercontent.com that release assets redirect to was told to re-dispatch semstat's release workflow, which repairs nothing it has. Only curl's HTTP-error status says the release answered and lacks the asset; anything else now names the transport. That download also had no timeouts, so a stalled connection hung the step until the job timed out rather than failing in 120s. The signature marker was the one write in the script with no ::error:: behind it, and a claim that does not land sends every later verifying step back to the network for the rest of the job. Both new paths are covered by tests that fail without the fix. The e2e jobs now run under a job environment that would repoint the installer if a binding were ever dropped again, which is the part bats cannot reach.
… job github.action_path reaches outside semver-validation's own directory, and a sparse single-action checkout died there on bash's bare complaint. The jq check the shared installer dropped moves to semver-validation, which is what still needs it. cosign-installer runs once per job rather than once per call, and the installer takes a PATH opt-out for callers that only ever name the binary.
…sign Both workflows claimed a second verified install proved the skipped cosign-installer left cosign reachable. It proved nothing: the reuse fast path exits before the `command -v cosign` gate and before verify-blob, so the assertion held even if the skip had left no cosign at all. Drop the semstat install between the two calls, keep the cosign marker, and the second call re-downloads and verifies with cosign-installer skipped. Also corrects the recorded reason for cutting semver-validation/v4: it binds SEMSTAT_SKIP_PATH and leaves the caller's PATH alone, so "puts semstat on the caller's PATH" was never one of the reasons.
The absent-bundle error told the reader a re-dispatch of semstat's release workflow repairs the release. That is semstat's repair, not something a consumer of this action can reach, and it stops working outright once a release is cut under immutability. Name a release that carries a bundle instead, which is advice the reader can act on either way.
…flow semstat now cuts its release as a draft and publishes it only after signing succeeds, so an outage leaves a draft rather than a published release with no provenance, and every published release carries its bundle. The absent -bundle branch therefore no longer describes an incomplete publish; it describes an asset missing from a release still mutable enough to lose one.
checksums.txt is fetched from the same release as the archive it vouches for, so anyone able to replace one replaces the other in the same call. It proves the download arrived whole and nothing about who published it. The pinned release predates GitHub's release immutability, which is not retroactive, so its assets stay replaceable for good. The signature is the only link in the chain that does not come from the thing it is checking. Both actions are unreleased, so no caller changes behaviour under them. The cost is a cosign install per job and a dependency on Sigstore being reachable: verify-signature: false buys both back for a job that neither publishes nor gates a publish. The installer keeps defaulting to false when the variable is unset, because the actions are what install cosign and a direct run of the script has no such step.
The second install in the end-to-end job inherited the flipped default, so it would have re-downloaded to verify, moved a new binary into place, and failed the inode reuse assertion for a reason that is not a reuse bug. Also gives every job in this workflow the timeout its sibling workflow already sets, which matters more now that a reachable Sigstore is on the default path.
sydorovdmytro
force-pushed
the
devops-1369/setup-semstat-action
branch
from
August 25, 2026 14:32
d94336f to
043817c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes DEVOPS-1369
Stacked on #231 — the installer it moves only exists on that branch. Base flips to
mainonce #231 merges.Moves
install-semstat.shand its bats suite out ofsemver-validationinto a newsetup-semstataction, so the four migrations queued behind it (DEVOPS-1365, DEVOPS-1366, DEVOPS-1367, and an unfiledpublish-helm-chartone) reuse it instead of each carrying a copy and its own renovate pin.semver-validationbecomes the first caller, outputs unchanged.Two additions over the moved copy:
$GITHUB_PATH, since most queued consumers call semstat from inside a bash function or awhile readloop where a step output is not in scope. The absolute path stays an output too, andSEMSTAT_SKIP_PATHturns the append off for a caller that only ever names the binary.semver-validationsets it, so it leaves the caller'sPATHalone.verify-signature, default true, has cosign checkchecksums.txtagainst…/release.yaml@refs/tags/<version>before any integrity claim is read off the manifest. cosign is installed once per job rather than once per call, tracked by aRUNNER_TEMPmarker so the skip never adopts a cosign the runner image shipped.semstat_versionis gone fromsemver-validation: the pin and its renovate comment live insetup-semstatnow, which is the point of the extraction, and that input never shipped in a tagged release.Distribution choice (full reasoning and the rejected options are on the Linear issue): remote
uses:pinned by SHA with the tag in a comment, matching howcve-scanandgovulncheckalready referenceci-test-notify.verify-signatureships on, against the acceptance criteriaDEVOPS-1369 says default false, on the grounds that verifying a first-party binary out of a first-party release is ceremony. That does not survive what the release actually looks like:
checksums.txtis fetched from the same release as the archive it vouches for, so whoever can replace one replaces the other in the same call. It proves the download arrived whole and nothing about who published it.loft-sh/semstat, plus every workflow there and every third-party action those call.v0.0.2, whichDEFAULT_VERSIONpins, stays replaceable permanently. Tracked in DEVOPS-1386, along with the goreleaser draft-flow change that makes future releases immutable.Neither
setup-semstat/v1norsemver-validation/v4is tagged yet, so no caller changes behaviour under the flip, and this is the only free moment to pick the default. The cost is a cosign install per job and a dependency on Sigstore being reachable, bought back withverify-signature: falsefor a job that neither publishes nor gates one. Both READMEs now list the two Sigstore hosts as required by default, which is the part most likely to bite a runner behind an egress allowlist.The rewrite must ship as
semver-validation/v4, notv2This corrects inherited guidance in #231's README, and it is the part most worth a second opinion.
semver-validation/v1,v2andv3all already exist on origin, and all three are still the node action:Live callers, by what they pin:
semver-validation/v1(floating)infrastructure,loft-enterprise,vcluster-candysemver-validation/v3(floating)hosted-platform,salesforce-actions-server6b5890f(v1's commit)vcluster-pro×4 workflowsb52efbd(v3's commit)vcluster-observability-gatewayAdvancing
v1— which DEVOPS-1369's acceptance criteria say to do — orv2, orv3, hands five live workflows in five repos a network-egress requirement (Sigstore included, now that verification is on by default) and four new tool requirements, with no version change for anyone to notice. So no existing tag moves; the rewrite getsv4. The five SHA-pinned callers are insulated either way.Also blocking merge
setup-semstat/v1does not exist yet, so the pin comment in the README is aspirational until the tag is cut from the merge commit.semver-validationreaches the installer throughgithub.action_pathrather than a SHA pin, so nothing on this branch needs re-pinning after the rebase, and a fix to the installer cannot strand behind a sibling pin.Test plan
Local:
make test-setup-semstat— 56 tests, covering PATH and theSEMSTAT_SKIP_PATHopt-out, the cosign path, the absent-bundle failure, theRUNNER_TEMPguard, work-directory cleanup on both exit paths, and the verified-marker state machine in both directionsmake test-semver-validation— 35 tests, unchangedmake lint— actionlint + zizmor cleanmake check-bats-jobs— passes;test-setup-semstat.yamlruns its suitemake check-docs— clean, regenerated READMEs committedshellcheck -xoninstall-semstat.sh— cleanOn CI (13/13 green):
test-setup-semstat.yaml: semstat called by name from a shell function, called through thepathoutput, a second install proven to reuse the first by inode identity, real cosign against the real v0.0.2 bundle, and a non-booleanverify-signaturefailing the stepVerify by defaultjob: calls the action with noverify-signatureinput and asserts the cosign marker exists. Every other job names the value it wants, so without this the default could be flipped back with the suite still greencommand -v cosigngate andverify-blob, so they stayed green even if the skip had left no cosign at all. Log from this run shows two real installs andreverify.__sigstore_cosign-installer outcome=skippedtest-semver-validation.yaml: a job environment that would repoint the install or force thePATHappend back on, asserted to lose on all three names; exactly one install underRUNNER_TEMP;semstatasserted absent from the job'sPATHCross-repo, in
vClusterLabs-Experiments/devops-1369-setup-semstat-e2e(private throwaway, delete on merge), 4/4 green. In-repo tests call both actions by relative path, where the sibling installer is trivially present because the repo is the workspace; nothing here could otherwise prove thegithub.action_pathhop works when the action is consumed from elsewhere:It also pins the default and the opt-out as resolved values rather than inference (
verify-signature: true→Install cosign outcome=success;false→outcome=skipped), and shows that mixing both actions in one job installs cosign once, same inode — the only test anywhere of the shared marker path the two duplicated gates coordinate through.