Skip to content

feat(setup-semstat): extract the semstat installer into a reusable action - #234

Merged
sydorovdmytro merged 10 commits into
mainfrom
devops-1369/setup-semstat-action
Aug 25, 2026
Merged

feat(setup-semstat): extract the semstat installer into a reusable action#234
sydorovdmytro merged 10 commits into
mainfrom
devops-1369/setup-semstat-action

Conversation

@sydorovdmytro

@sydorovdmytro sydorovdmytro commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Closes DEVOPS-1369

Stacked on #231 — the installer it moves only exists on that branch. Base flips to main once #231 merges.

Moves install-semstat.sh and its bats suite out of semver-validation into a new setup-semstat action, so the four migrations queued behind it (DEVOPS-1365, DEVOPS-1366, DEVOPS-1367, and an unfiled publish-helm-chart one) reuse it instead of each carrying a copy and its own renovate pin. semver-validation becomes the first caller, outputs unchanged.

Two additions over the moved copy:

  • the install directory is appended to $GITHUB_PATH, since most queued consumers call semstat from inside a bash function or a while read loop where a step output is not in scope. The absolute path stays an output too, and SEMSTAT_SKIP_PATH turns the append off for a caller that only ever names the binary. semver-validation sets it, so it leaves the caller's PATH alone.
  • verify-signature, default true, has cosign check checksums.txt against …/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 a RUNNER_TEMP marker so the skip never adopts a cosign the runner image shipped.

semstat_version is gone from semver-validation: the pin and its renovate comment live in setup-semstat now, 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 how cve-scan and govulncheck already reference ci-test-notify.

verify-signature ships on, against the acceptance criteria

DEVOPS-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.txt is 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.
  • 15 principals have push to loft-sh/semstat, plus every workflow there and every third-party action those call.
  • GitHub's release immutability is not retroactive, so v0.0.2, which DEFAULT_VERSION pins, stays replaceable permanently. Tracked in DEVOPS-1386, along with the goreleaser draft-flow change that makes future releases immutable.

Neither setup-semstat/v1 nor semver-validation/v4 is 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 with verify-signature: false for 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, not v2

This corrects inherited guidance in #231's README, and it is the part most worth a second opinion.

semver-validation/v1, v2 and v3 all already exist on origin, and all three are still the node action:

6b5890f  refs/tags/semver-validation/v1   using: node24
964b5ff  refs/tags/semver-validation/v2   using: node24
b52efbd  refs/tags/semver-validation/v3   using: node24

Live callers, by what they pin:

Pin Callers
semver-validation/v1 (floating) infrastructure, loft-enterprise, vcluster-candy
semver-validation/v3 (floating) hosted-platform, salesforce-actions-server
6b5890f (v1's commit) vcluster-pro ×4 workflows
b52efbd (v3's commit) vcluster-observability-gateway

Advancing v1 — which DEVOPS-1369's acceptance criteria say to do — or v2, or v3, 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 gets v4. The five SHA-pinned callers are insulated either way.

Also blocking merge

setup-semstat/v1 does not exist yet, so the pin comment in the README is aspirational until the tag is cut from the merge commit. semver-validation reaches the installer through github.action_path rather 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 the SEMSTAT_SKIP_PATH opt-out, the cosign path, the absent-bundle failure, the RUNNER_TEMP guard, work-directory cleanup on both exit paths, and the verified-marker state machine in both directions
  • make test-semver-validation — 35 tests, unchanged
  • make lint — actionlint + zizmor clean
  • make check-bats-jobs — passes; test-setup-semstat.yaml runs its suite
  • make check-docs — clean, regenerated READMEs committed
  • shellcheck -x on install-semstat.sh — clean

On CI (13/13 green):

  • test-setup-semstat.yaml: semstat called by name from a shell function, called through the path output, a second install proven to reuse the first by inode identity, real cosign against the real v0.0.2 bundle, and a non-boolean verify-signature failing the step
  • new Verify by default job: calls the action with no verify-signature input 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 green
  • the two cosign-skip assertions now delete the semstat install between calls while keeping the cosign marker. They previously proved nothing: the reuse fast path exits before both the command -v cosign gate and verify-blob, so they stayed green even if the skip had left no cosign at all. Log from this run shows two real installs and reverify.__sigstore_cosign-installer outcome=skipped
  • test-semver-validation.yaml: a job environment that would repoint the install or force the PATH append back on, asserted to lose on all three names; exactly one install under RUNNER_TEMP; semstat asserted absent from the job's PATH

Cross-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 the github.action_path hop works when the action is consumed from elsewhere:

/home/runner/work/_actions/loft-sh/github-actions/devops-1369/setup-semstat-action/
  .github/actions/semver-validation/../setup-semstat/src/install-semstat.sh

It also pins the default and the opt-out as resolved values rather than inference (verify-signature: trueInstall cosign outcome=success; falseoutcome=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.

@sydorovdmytro
sydorovdmytro requested a review from a team as a code owner August 19, 2026 21:07
@sydorovdmytro
sydorovdmytro force-pushed the devops-1369/setup-semstat-action branch from de5bd88 to 9d63339 Compare August 20, 2026 21:43
Base automatically changed from devops-1335/semstat-wrapper to main August 25, 2026 14:22
…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
sydorovdmytro force-pushed the devops-1369/setup-semstat-action branch from d94336f to 043817c Compare August 25, 2026 14:32
@sydorovdmytro
sydorovdmytro merged commit c8c7c44 into main Aug 25, 2026
12 checks passed
@sydorovdmytro
sydorovdmytro deleted the devops-1369/setup-semstat-action branch August 25, 2026 14:33
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