feat(release-tool): add drt — Daedalus release CLI
Adds the drt (Daedalus Release Tool) Rust CLI, replacing the old
proposal-ui Haskell tool.
Commands:
drt fetch-installers — download unsigned installers from a Hydra eval
drt sign — GPG + macOS/Windows code signing via SSH hosts
drt release — hash, sign, upload to S3, push version JSON
drt serve — local HTTP mirror for end-to-end tester workflows
(generates newsfeed + verification files on the fly)
Nix:
- Add crane + fenix flake inputs for Rust builds
- Add perSystem/release-cli.nix (drt package + devShell + checks)
- Add ops/ direnv environment for signing credentials
fix(drt/serve): bind IPv6 loopback alongside IPv4
Also bind [::1]:<port> so that `localhost` resolving to ::1 (common on
modern Linux/macOS) works without changing NEWS_URL to 127.0.0.1.
IPv6 bind is best-effort — silently skipped if unavailable.
refactor(drt): remove signing from release, add next-step hints
drt release no longer runs GPG signing — signing is a separate step
done via drt sign. Existing .asc files are still picked up and uploaded.
drt fetch-installers now hints: run drt sign
drt sign now hints: run drt release --bucket <bucket> --bucket-url <url>
fix(drt): skip -unsigned. installer files in all commands
fix(drt): split Darwin platform into DarwinArm/DarwinX86
Platform::Darwin was used for all .pkg files, causing aarch64-darwin
and x86_64-darwin installers to collide in HashMaps (last writer wins).
Split into DarwinArm (aarch64) and DarwinX86 (x86_64), with keys
darwin-arm and darwin in both version JSON and newsfeed output.
Platform detection now parses the full filename rather than just the
extension.
fix(drt/release): split bucket arg into bucket name + key prefix
Passing --bucket dripdropz-production/daedalus/7.4 used the full string
as the S3 bucket name, which contains slashes that are invalid in bucket
names. The AWS SDK then built a malformed canonical request, causing
SignatureDoesNotMatch errors.
Split --bucket on the first '/' into the actual bucket name and an
optional key prefix. All object keys (hash, filename, version JSON,
signatures) are stored under the prefix when one is present.
--bucket my-bucket → bucket=my-bucket, prefix=
--bucket my-bucket/path/to/dir → bucket=my-bucket, prefix=path/to/dir
feat(drt/release): add --test flag to upload newsfeed stub to S3
Production newsfeeds are managed via GitHub; drt release should not
touch them. Add --test to opt in to uploading a generated newsfeed
stub and its SHA-256 verification file alongside the installers:
newsfeed/newsfeed_<env>.json
newsfeed-verification/<env>/<timestamp>.txt
Refactor serve::generate_local_newsfeed into a pub build_newsfeed
function that returns bytes + sha256 without touching the filesystem,
shared by both drt serve and drt release --test.
Add s3::upload_bytes for generic public-read byte uploads.
feat(drt/s3): skip upload if object already exists
Before uploading each installer (hash key + filename copy) or signature,
issue a HeadObject to check if the key is already present. If so, skip
the upload silently.
For hash-keyed objects the key IS the content hash, so existence implies
correct content — no re-download needed to verify.
This makes drt release idempotent: safe to re-run after a partial failure
without re-uploading multi-hundred-MB installer files.
feat(drt/fetch): retry downloads and skip already-verified files
Two improvements for unreliable Hydra connections:
- Retry: wraps each download in up to 5 attempts with exponential
backoff (3 s, 6 s, 12 s, 24 s). Prints the error and delay before
each retry so progress is visible.
- Resume: before downloading, checks if the destination file already
exists and its SHA-256 matches the expected hash. If so, skips the
download entirely. Re-running after a partial failure continues from
where it left off rather than restarting from the beginning.
Also removes the unused human_size placeholder.
fix(drt/sign): skip already-signed files and resume partial signing
Add `unsigned_path()` and `is_already_code_signed()` to `Installer`.
In the signing loop, if the -unsigned companion exists and the signed
file is also present, skip (idempotent re-run). If the companion exists
but the signed file is missing (network error mid-SCP/pipe), fall
through and retry — macOS already skips the rename conditionally;
Windows now does the same.
fix(drt/release): simplify done output to version, json url, and installer urls
feat(drt/release): show newsfeed urls in done summary when --test is passed
fix(drt/release): remove duplicate path prefix in newsfeed summary urls
fix(drt/release): use exact timestamp and no-store cache-control for newsfeed uploads
Rounding updatedAt to the nearest hour meant re-runs within the same
hour overwrote the same verification file key in S3, but CloudFront
served the cached old hash causing a mismatch. Using actual milliseconds
gives each run a unique key. Add Cache-Control: no-store to both newsfeed
and verification uploads so CDN never serves stale copies.