Reachable-from-main alone accepts a tag moved to any other main commit.
The release object records its creation-time target_commitish, so require
tag_commit == release.targetCommitish: a moved tag now fails closed, and
the SHA emitted for checkouts is provably the released commit.
- release-binaries and web-sync now resolve the release tag and check out
the verified commit SHA, closing the tag-move race on the job that
produces the release assets, not just the jobs that consume them.
- homebrew-bump resolves the tag, downloads the tarballs by captured asset
id, and computes sha256 locally instead of trusting the mutable
*.tar.gz.sha256 sidecar assets.
- The step pin now covers the lay-in, formula, and release-tag checkout
blocks so edits to the trust boundary force a deliberate fixture update.
The provenance checks ran at resolve time but the consumers re-resolved
mutable names afterward, leaving a move-the-tag or replace-the-asset race:
- Checkouts now use the commit SHA the resolver emitted (tag_commit),
falling back to the qualified tag ref.
- The resolver captures the release's name/id asset pairs once, checks all
uploaders against them, and the lay-in step downloads by asset id. An
asset deleted and reuploaded between resolve and download gets a new id
and fails closed.
- The npm-publish comment now names the third trusted-publisher component
(environment), which is what binds the OIDC token to the gate.
The npm backfill republishes GitHub release assets, which are mutable for
anyone with contents: write. Require every asset on the release to have
been uploaded by github-actions[bot], matching the release-author check.
Not a hard bound, but it removes the direct edit path and leaves a review
signal when a hand-uploaded asset blocks a backfill.
GitHub's latest-release pointer is operator-mutable and is not a bound on
what :latest serves. List the package's versions on ghcr and refuse to
move :X.Y or :latest below the newest X.Y.Z tag there. A 404 means the
package does not exist yet, so the first publish applies all tags; any
other lookup failure aborts. The version filter is grouped with || true so
an empty list does not trip pipefail.
Third adversarial pass:
- Provenance now also requires the release to be authored by the release
automation account, so a hand-created release on a valid tag does not
qualify for backfill.
- The npm dist-tag decision is per package. A partial publish can leave
platform packages ahead of the wrapper, and a single lookup on
@gitlawb/gl would then downgrade an ahead package's latest tag.
- The environment gate test now emits one line per job and marks jobs with
no declaration MISSING, so a job added without environment: release can
no longer pass silently.
Second adversarial pass found two residuals:
- The provenance check compared main...<tag> with an unqualified refname,
the same branch-shadows-tag ambiguity the refs/tags/ checkout prefix was
added to avoid. Resolve the tag through commits/refs/tags/<tag> to a
commit SHA and compare that instead.
- npm view ... || true mapped every lookup failure to "no latest", so a
registry outage during an older backfill would publish under --tag
latest and move the dist-tag backward. E404 still means first publish;
any other failure now aborts before a dist-tag is chosen.
Two residuals from review of the dispatch-publish surface:
- environment: release only covered the three registry-publish jobs. Declaring
it on all eight jobs means a dispatch of unmodified YAML on a non-main ref
is denied by the environment's deployment rules before any step runs.
- The resolver shape-checked vX.Y.Z but did not prove the tag was produced by
the release flow. A write-access user could plant a v99.99.99 tag on
unreviewed content and dispatch a backfill against it. In CI (GH_TOKEN
present) the resolver now requires a GitHub release for the tag and a
main...tag compare status of identical or behind, so the tag's commit must
already be reachable from main.
Closes#271, closes#238.
actions/checkout resolves an unqualified ref as a branch before a tag, so
a branch named v0.7.1 shadows the tag and the release builds unreviewed
content. Qualify all four release-tag checkouts as refs/tags/..., which
takes checkout's unconditional tag path.
docker-manifest applied :latest and :X.Y unconditionally, so a backfill
of an older tag repointed them at older code. The manifest now always
publishes the immutable :X.Y.Z and applies the moving tags only when the
resolved version is not older than the registry's latest release. The
same shape existed on the npm side: publish moves the latest dist-tag to
whatever it publishes, so a backfilled older version would have stolen
it; older backfills now publish under the backfill dist-tag instead.
The manifest also asserted nothing about the digest set it stitches, so
a wrong-arity or misnamed artifact would ship under :latest. Assert two
digests of 64 lowercase hex chars before imagetools create.
Extend the resolver test to pin both publish steps byte-for-byte and to
reject any unqualified tag-checkout ref, so these guards cannot regress
silently.
Closes#234. workflow_dispatch runs the selected ref's YAML and npm trusted
publishing matches on the workflow filename, not the ref, so a dispatched
non-main ref could reach the OIDC publish with no approval. Declare
environment: release on all three publish jobs (npm-publish, docker,
docker-manifest) so the environment's deployment-branch rule gates every
publish surface, not just the OIDC one.
Also close the two residuals the issue carries: the release-tag case glob
was right-unanchored (accepted v1.2.3.4.5, v1_x, v1-any-branch-name, bare
v1), now an extglob v+([0-9]).+([0-9]).+([0-9]); and the OIDC publish ran
npm@^11.5.1, a floating range, now pinned to [email protected].1.
The environment itself must exist with a deployment-branch rule limited to
main (and required reviewers if desired) for the gate to bite; npmjs.com
can also scope each package's trusted publisher to environment "release"
for a registry-side check that survives a workflow edit.
The coverage gate matched `# x-release-please-version` anywhere in a manifest,
so an annotated `version` under any other table satisfied it. A crate could
leave the version cargo actually reads unannotated, pass the gate, and still
freeze at release time, with release-please rewriting the decoy line instead:
the exact failure this gate exists to catch.
Scope the match to the [package] table.
icaptcha-client became a workspace member in #138, after #130 last wrote
release-please-config.json, and nothing updated extra-files. release-please
rewrites a version line only when the line carries the annotation AND the
manifest is listed, so the crate was skipped every release and sat at 0.4.0
while the other five walked to 0.7.1.
Nothing broke: every intra-workspace path dep is bare, so no version
requirement could go unsatisfied; no crate is published to a registry; and
the crate reads no CARGO_PKG_VERSION of its own. The cost was the workspace
Cargo.toml comment asserting a lockstep rule the config did not keep, which
is what the next person adding a crate would read and copy.
Add the missing entry, and gate the rule so it cannot silently lapse again.
The gate derives its required set from `cargo metadata`, so a new crate reds
CI whether or not its author knows the gate exists, and it covers all three
release-silent shapes: a member absent from extra-files, a member whose
version line lost the annotation, and an extra-files path that is no longer
a member.
Next release moves icaptcha-client 0.4.0 to the current workspace version in
one step. Nothing depends on that version, so the jump is cosmetic.
sync-release-lock ran cargo update --workspace on whatever Rust the
ubuntu-latest image shipped, so the lock it wrote for the release PR came
off an undeclared input. Install the same stable toolchain the other
cargo jobs use. This does not make resolution identical (stable moves,
and the MSRV gate checks the same lock on 1.91), it just removes the
runner image from the equation.
The release-binaries build, both Dockerfiles and the darwin path of
build-bins.sh also resolved without --locked, so a lockfile disagreeing
with the manifests was caught on the PR gates but not where artifacts
are actually produced. Add --locked to all five.
Two of those five cannot reach resolution today and are hardened for
when they can: the Dockerfile dep-cache layer at line 24 copies four of
the workspace's six member manifests, so cargo dies at workspace load and
|| true swallows it, and Dockerfile.bins pins rust:1.85 against a 1.91
MSRV. Both are pre-existing and left alone here. The live coverage comes
from release.yml, Dockerfile:37 and build-bins.sh.
The gitlawb-core dependency-purity gate still resolves without --locked
on purpose, but its comment justified that by saying the build and test
jobs do the same, which stopped being true when those jobs gained
--locked. Restate the real reason: a stale lock would red that gate for
something unrelated to the dependency closure it checks.
gitlawb-core is embedded by gl, git-remote-gitlawb, and the node daemon,
so it must stay lean. Add a CI gate that fails if its normal (non-dev,
non-build) transitive dependency tree gains any crate not on an explicit
allowlist.
The allowlist is exhaustive-by-construction: a new heavy dependency reds
CI whether or not anyone thought to ban it, which a denylist cannot do.
The checker resolves like the build/test jobs (no --locked, since the
committed lockfile can lag the manifests) and snapshots/restores
Cargo.lock so a local run leaves no working-tree side effects.
- ci/gitlawb-core-allowed-deps.txt: the 83 current normal deps, plus the
regeneration command and rationale in the header
- scripts/check-gitlawb-core-deps.sh: recompute and diff; hard-fail on a
non-allowlisted crate, note-only on a stale (removed) allowlist entry
- pr-checks.yml: new hard-fail core-deps-purity job, matching the pinned
action SHAs and style of the sibling jobs
- Menu bar icon with colored status dot (green/yellow/red)
- Start/Stop node via Docker Compose
- Settings UI: ports, public URL, postgres password, auto-sync, operator config
- View Logs window with live docker compose logs
- Auto-start on launch and login (SMAppService)
- Docker detection with install prompt
- Repo root detection via compile-time path for local builds
- Port variables wired through docker-compose.yml for custom port support
- GITLAWB_AUTO_SYNC passthrough for peer replication toggle
- Build script: scripts/build-macos-app.sh (codesign + DMG)
Co-authored-by: Rimas Mocevicius <[email protected]>