fix(release): repair four gates that reported success without checking - #338
Open
loispostula wants to merge 1 commit into
Open
fix(release): repair four gates that reported success without checking#338loispostula wants to merge 1 commit into
loispostula wants to merge 1 commit into
Conversation
Review of the release command family surfaced four defects that each let a failure through silently, plus the supporting should-fixes. All sit in the #336/#337 stack and none change the published object shapes. bundle-linux could never have succeeded. cargo-deb's default output is `target_dir_base.join("debian")` with no target triple (config.rs `default_deb_output_dir`; the triple is appended only by `target_dependent_path`, which the deb output does not go through), so `--target` does not move the artifact and reading a tripled path was an unconditional ENOENT. `-o` is now passed explicitly at the per-triple directory, which is cleared first and from which exactly one .deb must emerge: the previous take-the-first-by-sort-order would ship a stale build. The production check-run gate was satisfied by the job it runs inside. `conclusion` is null until `status` reaches `completed`, so a queued or running check read as green, and this command's own check run is attached to the tagged commit, making the count-based no-checks refusal unreachable. The gate now takes named `--required-checks` and demands completed-and-success for each. BREAKING: `--required-checks` is mandatory unless `--skip-check-runs` is passed. fsl_libs supplies it via `vars.RELEASE_REQUIRED_CHECKS`, defaulting to `test`. Failure is considered only for the required names. A commit on main routinely carries unrelated bad check runs: measured on fsl_libs main, 20 check runs including a failed docker_build and several cancelled by concurrency. Refusing on any bad run anywhere would block every release. The same commit carries two runs named `test`, one failed and one successful, because the postsubmit and the nightly both attach to main's SHA, so the best outcome per name wins. resolve built the manifest URL from `channels.manifest_base`. channels.json is written by the promotion credential, whose stated invariant is that it cannot write artifacts; sourcing the manifest location from inside it hands over exactly that power, and a `kind: authenticode` entry then skips client signature verification on every platform. Resolution now uses the client's own production base, promote re-asserts the field on every write rather than only at creation, and the docs state it is informational. probe-store could not fail on the property it gates. Its only If-Match coverage asserted that a successful CAS update succeeded, which a store ignoring the precondition also does. A stale-ETag write is now attempted and must be refused. Note this may legitimately FAIL against the deployed MinIO, which is the point: publication must not be enabled until it passes. Also: publish verifies detached signatures, which are immutable once written but carry no manifest digest and so were never read back; artifacts stay compared against the digest the MANIFEST records rather than one recomputed from the bytes just sent, which would be self-consistent by construction. key_from_url takes the last bucket segment, since a base URL containing the bucket name yielded a doubled prefix that surfaced only after the artifacts were immutable. assert_monotonic parses the candidate version before the comparison loop, which an empty published set skipped entirely, permanently bricking future publishes. healthcheck distinguishes a definite 404 from a transport failure via a typed status error rather than matching on message text, and treats only 404 as absent: 403 against an anonymously readable bucket is a broken read policy, and calling it healthy let the workflow close its own tracking issue during an outage. license_macos is omitted when absent rather than serialized as null, which jq -r renders as the string "null". sign-linux feeds the passphrase over --passphrase-fd 0 instead of the argv, never echoes gpg's arguments into an error, and writes stdin concurrently with draining stdout so a full pipe buffer cannot deadlock the signing job. 15 new tests, 339 passing. The check-run gate is extracted to a pure function so the still-running, satisfied-by-itself and unrelated-failure cases are covered directly. Signed-off-by: Loïs Postula <admin@conceptions.postu.la>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
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.
Stacks on #337. Review of the release command family found four gates that
report success without checking the thing they exist to check, plus the
supporting fixes. No published object shape changes.
The four
1.
bundle-linuxhas never succeeded. cargo-deb's default output istarget_dir_base.join("debian")with no target triple (config.rsdefault_deb_output_dir; the triple is appended only bytarget_dependent_path, which the deb output does not go through). So--targetdoes not move the artifact, and reading a tripled path is anunconditional ENOENT.
bundle-linuxexits nonzero,sign-linuxandpublishskip. This means the end-to-end Linux run described in #336 cannot have
happened — worth re-checking what else that run was thought to cover.
Fixed by passing
-oexplicitly. The per-triple directory is kept (cargo-deb'sdefault is shared by every package in the workspace), cleared first, and
exactly one
.debmust emerge rather than taking the first by sort order.2. The production check-run gate is satisfied by its own job.
conclusionis null untilstatusreachescompleted, and only four explicitfailure conclusions counted as bad, so a queued or running check read as green.
This command runs inside
facts, whose own check run is attached to the taggedcommit, so the count was never zero and the no-checks refusal was unreachable.
Net effect: tag any ancestor of main whose CI never ran and it publishes.
3.
manifest_baseredirection.resolvebuilt the manifest URL from afield inside channels.json, which is written by the promotion credential —
whose documented invariant is that it cannot write artifacts. Point it
anywhere and every digest check downstream is satisfied against the attacker's
own manifest; a
"kind": "authenticode"entry skips client signatureverification entirely. Resolution now uses the client's own production base;
promotere-asserts the field on every write instead of only at creation.4.
probe-storecannot fail on the property it gates. Its only If-Matchcoverage asserted that a successful CAS update succeeded — which a store that
ignores the precondition also does. A stale-ETag write is now attempted and
must be refused.
point: if If-Match is not enforced, publication must not be enabled. Nobody has
run this against the real endpoint yet.
--required-checksis now mandatory unless--skip-check-runsis passed.fslabs/fsl_libs#4230's companion branch supplies it via
vars.RELEASE_REQUIRED_CHECKS, defaulting totest.Failure is deliberately considered only for the named checks. Measured on
fsl_libs main: 20 check runs on one commit, including a failed
docker_buildand several
cancelledby concurrency. Refusing on any bad run anywhere wouldblock every release. That same commit carries two runs named
test— onefailed, one successful — because the postsubmit and the nightly both attach to
main's SHA, so the best outcome per name wins.
Also
publishreads back the detached signatures, which are immutable oncewritten but carry no manifest digest and so were never verified. Artifacts
stay compared against the digest the manifest records, not one recomputed
from the bytes just sent, which would be self-consistent by construction.
key_from_urltakes the last bucket segment: a base URL containing thebucket name yielded a doubled prefix, surfacing only after every artifact was
already immutable.
assert_monotonicparses the candidate version before the comparison loop.An empty published set skipped validation entirely, so a bad first version
committed once and then bricked every future production publish permanently.
healthcheckdistinguishes a definite 404 from a transport failure via atyped status error rather than matching on message text, and treats only
404 as absent. A 403 against an anonymously readable bucket is a broken read
policy; calling it healthy let the workflow close its own tracking issue
during an outage.
license_macosis omitted when absent rather than serialized asnull,which
jq -rrenders as the literal string"null".sign-linuxfeeds the passphrase over--passphrase-fd 0instead of theargv (readable in
/proc), never echoes gpg's arguments into an error, andwrites stdin concurrently with draining stdout so a full pipe buffer cannot
deadlock the signing job.
Testing
cargo test: 339 passed (324 baseline + 15).cargo fmt --checkandcargo clippy --all-targetsclean.The check-run gate is extracted to a pure function so the still-running,
satisfied-by-itself and unrelated-failure cases are covered directly, including
a regression test built from the measured shape of fsl_libs main.
Not exercised: the
bundle-linuxfix needs a real cargo-deb run, andprobe-storeneeds the deployed store.