Skip to content

Sign and publish mswin binary packages to cache.ruby-lang.org - #137

Open
hsbt wants to merge 17 commits into
masterfrom
claude/windows-binary-signing-automation-4ad9ce
Open

Sign and publish mswin binary packages to cache.ruby-lang.org#137
hsbt wants to merge 17 commits into
masterfrom
claude/windows-binary-signing-automation-4ad9ce

Conversation

@hsbt

@hsbt hsbt commented Jul 28, 2026

Copy link
Copy Markdown
Member

The mswin zip packages built here have so far only existed as workflow artifacts. This wires them into the release pipeline.

draft-release.yml builds the zip from the draft tarball, signs the Ruby-built PE files through SignPath under the manually approved signing environment, and stages it as pub/tmp/ruby-<version>-x64-mswin64_140-draft.zip. These jobs never block the source release.

A new publish-binaries.yml, dispatched by ruby/ruby's publish workflow, promotes the staged zip to the append-only pub/ruby/binaries/mswin64/. It also regenerates pub/ruby/binaries/index.json, a pymanager-style feed with ruby-build style tags for rb to resolve installs against.

Daily snapshots are published unsigned under dev/, keeping the newest 30. mswin-release.yml remains as the backfill path for already released tarballs.

Until the SignPath project is provisioned, the signing step is skipped and packages ship unsigned with signed: false in the index. See SIGNING.md for the signing policy.

🤖 Generated with Claude Code

hsbt and others added 17 commits July 29, 2026 08:28
Generates pub/ruby/binaries/index.json from the objects under
mswin64/, following the pymanager (PEP 773) feed shape with
ruby-build style tags. Regenerated from scratch on every run so the
index heals itself from S3 state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wraps SignPath signing behind a zip-in/zip-out contract so the
signing backend stays swappable. Signature verification covers only
the PE files built from the Ruby tree; vcpkg DLLs stay unsigned
under the SignPath Foundation OSS program, as documented in
SIGNING.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mswin-build learns to consume the draft tarball from the Packages
artifact of the same run, skipping the release-index check that the
not-yet-published tarball cannot pass, and exposes the package name
as an output. draft-release builds the zip from it, signs it under
the manually approved signing environment, and stages it as
pub/tmp/ruby-<version>-x64-mswin64_140-draft.zip. The mswin jobs are
kept out of the release job needs so a packaging failure never
blocks the source release. remove-tmp-package cleans up the staged
zip alongside the tarballs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Promotes the staged mswin zip to pub/ruby/binaries/mswin64/ when
ruby/ruby publish dispatches publish-binaries. The destination is
append-only; a fixed package is reissued under a revisioned name
instead of overwriting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mswin-release becomes the backfill path: it signs the zip built from
an already published tarball and uploads it straight to
pub/ruby/binaries/mswin64/ with the same overwrite refusal, then
regenerates the index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dev builds are published unsigned under immutable date+commit names,
pruned to the newest 30, and reflected in the binaries index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A reissued zip like ruby-4.0.5-1-x64-mswin64_140.zip was parsed as a
prerelease, so it never received the 4.0.5 resolution tags while the
broken original kept them. Only the highest revision of a version
now carries the resolution tags. Prereleases get channel:
prerelease, keeping clients that treat the first release entry as
latest stable from picking an rc. Builds are also sorted
version-first instead of by upload date, so a backfilled old release
cannot shadow a newer one for consumers picking the first tag match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SIGNING.md promises revisioned reissues but the workflows could not
produce one. mswin-release now takes a REVISION input and
mswin-build appends it to the package name, which also becomes the
zip root, so the append-only bucket accepts the fixed package under
a fresh name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sorting the whole file name orders lexically by version, which
misorders pruning once 4.9 and 4.10 dev builds coexist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The README still claimed publishing to cache.ruby-lang.org was not
wired up, and described mswin-release as ending at an Actions
artifact. Neither is true once this pipeline lands. The setup that
signing depends on lives in repository settings, so it was invisible:
without a `signing` environment GitHub silently creates an
unprotected one and the approval gate SIGNING.md describes does not
exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A mistyped revision reached S3 unchecked. `9x` produced a name the
index regex rejects, which fails index generation for every workflow
until someone deletes the object by hand, and `abc` parsed as a
prerelease suffix so a reissue silently kept the plain tags on the
package it was meant to replace. `git rev-parse --short=10` is a
lower bound and lengthens an ambiguous prefix, which breaks the dev
name the same way. Both are now caught before the name is used.

Skipped signing also now logs a warning, since publishing unsigned
was otherwise indistinguishable from a normal green run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A reissue left the original with an empty tag list, so nothing could
resolve it, contradicting the documented contract that older
revisions resolve by their revisioned exact name. Numbering the first
publication revision 0 gives it that name.

Sorting compared Gem::Version alone, and its string segments order
"4.1.0.dev" below "4.1.0.preview1" alphabetically, sinking today
snapshot beneath a months-old preview and breaking the newest-first
guarantee. Version, then channel rank, then revision now decide, with
the commit as a tie-breaker so the order is total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A draft tag gets re-cut freely, and signing at draft time spent a
SignPath signing request and a human approval on every re-cut. Worse,
each re-cut silently overwrote a staged zip that had already been
signed and approved, so what publish promoted was not necessarily
what anyone approved.

draft-release now stages the zip unsigned and verifies the build
checksum instead of regenerating it, keeping the build witness intact
until signing. publish-binaries takes the staged zip, verifies it
against that witness, signs it under the signing environment, and
uploads the signed bytes. Index regeneration and staging cleanup move
to a separate job so a transient failure there can be re-run without
tripping the append-only guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
signtool verify /pa only asks whether the chain is trusted, so any
certificate the default policy accepts passed, and /q threw away the
reason when it did not. /tw now enforces the RFC 3161 timestamp that
SIGNING.md promises, an optional thumbprint pins the certificate to
ours once SignPath issues it, and a failed check re-runs verbose so
the log distinguishes a missing signature from an untrusted chain.

mswin-release also gains a precheck job, so dispatching an already
published version fails in seconds instead of after an hour of build
time and a signing approval.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`aws s3 ls` splits on whitespace, so a name containing a space
yielded a truncated key, and any object whose third dash-separated
field was not a date sorted as the oldest and was deleted first. The
listing now goes through list-objects-v2 and candidates must match
the dev name pattern. What gets deleted is recorded in the step
summary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dropping null keys meant a dev entry had `commit` but no `revision`,
a first release had neither, and a reissue had `revision` but no
`commit`, so `schema: 1` guaranteed nothing and consumers had to
probe for each key. Entries now always carry every key.

`date` also meant two different things, a commit date for dev builds
and an upload date for releases. It is now `commit_date` and
`published_at`. Entries gained `name`, the zip root directory, which
was a documented part of the package contract that consumers could
only recover by parsing the URL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant