Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## Unreleased

- Let one update feed carry multiple installables per platform. Artifacts
gain an optional `component` discriminator (`app`, `cli`, or `tool`) that
participates in the manifest's duplicate-identity rule, writers emit the
legacy shape when no component is set, and `updater-config` selects by
component; a config without one keeps matching component-less entries
only. `raco rivet release --update-payload <installer>:<glob>[:<component>]`
(repeatable) expands project-relative globs into component-tagged feed
entries, so products shipping an app and a CLI — or a portable tar.gz
instead of the zip — stop splitting into two feeds. The
`updater-config` struct gains the component field as its last argument.

- Add `fetch-sibling-asset` to `rivet/distribution` and the
`raco rivet sign-manifest` subcommand. The sibling helper checks the HTTP
status before saving, enforces the declared Content-Length against the
Expand Down
22 changes: 14 additions & 8 deletions rivet-cli/main.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
" raco rivet release build signed installer, update manifest, SBOM, and notices\n"
" raco rivet release --development exercise release flow without platform production signing\n"
" raco rivet release --without-updates release without an update manifest or update key\n"
" raco rivet release --update-payload <installer>:<glob>[:<component>]\n"
" add a per-platform update payload to the feed\n"
" raco rivet manifest-compose <inputs...> [--output <file>]\n"
" raco rivet sign-manifest --application-id <id> --version <v> --build <n>\n"
" --channel <stable|beta|dev> --base-url <url>\n"
Expand Down Expand Up @@ -169,18 +171,22 @@
(say "production packaged, signed, and verified ~a~a"
output
(if launch-smoke? "" " (launch smoke skipped)"))]
[(or (list "release")
(list "release" "--development")
(list "release" "--without-updates")
(list "release" "--development" "--without-updates")
(list "release" "--without-updates" "--development"))
[(cons "release" release-args)
(define release-project! (command "release" 'release-project!))
(define production? (not (member "--development" args)))
(define updates? (not (member "--without-updates" args)))
(define production? (not (member "--development" release-args)))
(define updates? (not (member "--without-updates" release-args)))
(define update-payloads
(let loop ([remaining release-args] [payloads '()])
(match remaining
['() (reverse payloads)]
[(list* "--update-payload" spec tail)
(loop tail (cons spec payloads))]
[(cons _ tail) (loop tail payloads)])))
(define-values (installer manifest sbom notices portable-zip)
(release-project! (current-project!)
#:production? production?
#:updates? updates?))
#:updates? updates?
#:update-payloads update-payloads))
(say "release installer: ~a" installer)
(say "portable zip: ~a" portable-zip)
(when (eq? (system-type 'os) 'unix)
Expand Down
66 changes: 61 additions & 5 deletions rivet-cli/release.rkt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#lang racket/base

(require file/zip
(require file/glob
file/zip
racket/file
racket/list
racket/path
racket/string
"../rivet/distribution/crypto.rkt"
Expand All @@ -19,7 +21,8 @@
release-update-environment
portable-zip-path
create-portable-zip!
release-update-artifacts)
release-update-artifacts
parse-update-payload-spec)

(define (required-environment name)
(define value (getenv name))
Expand Down Expand Up @@ -63,7 +66,8 @@
(sha256-file/hex payload)
(file-size payload)
installer
'()))
'()
#f))

(define (portable-update-artifact update-config portable-zip)
(update-artifact-for-file update-config portable-zip
Expand Down Expand Up @@ -91,9 +95,54 @@
;; MSI and hand it to the shared Windows handoff; AppImage installs select the
;; self-replacing image. macOS's host client intentionally consumes the zip —
;; the DMG remains the human installer.
;;
;; Products that publish additional update payloads per platform (a CLI next
;; to the app, a tar.gz instead of the zip) declare them with
;; `--update-payload <installer>:<glob>[:<component>]`, repeatable. Globs
;; expand relative to the project root; each match joins the feed with the
;; component discriminator, and a triple without a component keeps the
;; legacy shape.
(define (parse-update-payload-spec who spec)
(define parts (string-split spec ":"))
(unless (and (>= (length parts) 2) (<= (length parts) 3))
(raise-arguments-error who
"--update-payload expects <installer>:<glob>[:<component>]"
"value" spec))
(define installer (string->symbol (list-ref parts 0)))
(unless (memq installer '(zip msi deb rpm appimage))
(raise-arguments-error who
"update payload installer must be zip, msi, deb, rpm, or appimage"
"value" spec))
(define component
(and (= (length parts) 3)
(let ([name (string->symbol (list-ref parts 2))])
(unless (memq name '(app cli tool))
(raise-arguments-error who
"update payload component must be app, cli, or tool"
"value" spec))
name)))
(values installer (list-ref parts 1) component))

(define (expand-update-payloads update-config project platform architecture specs)
(append*
(for/list ([spec (in-list specs)])
(define-values (installer pattern component)
(parse-update-payload-spec 'release-project! spec))
(define matches
(sort (glob (project-path project pattern)) path<?))
(when (null? matches)
(raise-arguments-error 'release-project!
"update payload pattern matched no files"
"pattern" pattern))
(for/list ([path (in-list matches)])
(define artifact
(update-artifact-for-file update-config path platform architecture installer))
(struct-copy update-artifact artifact [component component])))))

(define (release-update-artifacts update-config project portable-zip installer
#:platform [platform (release-platform)]
#:architecture [architecture (release-architecture)])
#:architecture [architecture (release-architecture)]
#:extra-payloads [extra-payloads '()])
(define portable
(update-artifact-for-file update-config portable-zip
platform architecture 'zip))
Expand All @@ -111,7 +160,11 @@
(update-artifact-for-file update-config payload
platform architecture 'appimage))]
[else #f]))
(if native (list portable native) (list portable)))
(append
(list portable)
(if native (list native) '())
(expand-update-payloads update-config project platform architecture
extra-payloads)))

(define (cleanup-release-stage! package)
(delete-directory/files package #:must-exist? #f))
Expand Down Expand Up @@ -187,4 +240,7 @@
(provide portable-update-artifact
release-update-artifact
release-update-artifacts
parse-update-payload-spec
expand-update-payloads
(struct-out update-environment)
cleanup-release-stage!))
6 changes: 4 additions & 2 deletions rivet-cli/sign-manifest.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
racket/path
racket/string
"../rivet/distribution/crypto.rkt"
"../rivet/distribution/identity.rkt"
"../rivet/distribution/manifest.rkt")

(provide sign-manifest!
Expand Down Expand Up @@ -139,7 +140,7 @@
(update-manifest
application-id version build channel
(or (hash-ref options 'published-at #f)
(number->string (current-seconds)))
(rfc3339-now))
(hash-ref options 'minimum-version "0.0.0")
(hash-ref options 'previous-version #f)
(hash-ref options 'rollback-allowed? #f)
Expand All @@ -157,7 +158,8 @@
(sha256-file/hex path)
(file-size path)
(requested-artifact-installer artifact)
'()))))
'()
#f))))
(make-parent-directory* output)
(call-with-output-file output
#:exists 'truncate/replace
Expand Down
3 changes: 2 additions & 1 deletion rivet/distribution/managed-updater.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@
key
(updater-key-id instance)
(updater-rollout-bucket instance)
(updater-maximum-download-bytes instance)))
(updater-maximum-download-bytes instance)
#f))

(define (available-result instance candidate)
(define manifest (update-candidate-manifest candidate))
Expand Down
49 changes: 38 additions & 11 deletions rivet/distribution/manifest.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
(define manifest-schema-version 1)

(struct update-artifact
(platform architecture url sha256 size installer arguments)
(platform architecture url sha256 size installer arguments component)
#:transparent)

(struct update-manifest
Expand All @@ -32,7 +32,11 @@
'(schema application_id version build channel published_at minimum_version
previous_version rollback_allowed rollout artifacts))
(define artifact-fields
'(platform architecture url sha256 size installer arguments))
'(platform architecture url sha256 size installer arguments component))
;; Components discriminate multiple installables per (platform, architecture,
;; installer): a product may ship an app and a CLI built from one release.
;; Legacy manifests carry no component; those entries are the plain artifact.
(define artifact-components '(app cli tool))
(define wrapper-fields '(schema payload signature))
(define signature-fields '(algorithm key_id value))

Expand Down Expand Up @@ -69,13 +73,18 @@
value)

(define (artifact->jsexpr artifact)
(hasheq 'platform (symbol->string (update-artifact-platform artifact))
'architecture (symbol->string (update-artifact-architecture artifact))
'url (update-artifact-url artifact)
'sha256 (string-downcase (update-artifact-sha256 artifact))
'size (update-artifact-size artifact)
'installer (symbol->string (update-artifact-installer artifact))
'arguments (update-artifact-arguments artifact)))
(define base
(hasheq 'platform (symbol->string (update-artifact-platform artifact))
'architecture (symbol->string (update-artifact-architecture artifact))
'url (update-artifact-url artifact)
'sha256 (string-downcase (update-artifact-sha256 artifact))
'size (update-artifact-size artifact)
'installer (symbol->string (update-artifact-installer artifact))
'arguments (update-artifact-arguments artifact)))
(if (update-artifact-component artifact)
(hash-set base 'component
(symbol->string (update-artifact-component artifact)))
base))

(define (manifest->payload-bytes manifest)
(unless (update-manifest? manifest)
Expand Down Expand Up @@ -106,6 +115,19 @@
(lambda (v) (and (string? v)
(regexp-match? #px"^[0-9A-Fa-f]{64}$" v)))
"64 hexadecimal SHA-256 characters"))
(define component
(cond
[(not (hash-has-key? value 'component)) #f]
[(eq? (hash-ref value 'component) 'null) #f]
[else
(define name (required value 'component string? "string"))
(define symbol-name (string->symbol name))
(unless (memq symbol-name artifact-components)
(raise-arguments-error 'payload-bytes->manifest
"artifact component is not supported"
"component" name
"supported" artifact-components))
symbol-name]))
(update-artifact
(string->symbol (required value 'platform string? "string"))
(string->symbol (required value 'architecture string? "string"))
Expand All @@ -118,7 +140,8 @@
(string->symbol (required value 'installer string? "string"))
(required value 'arguments
(lambda (v) (and (list? v) (andmap string? v)))
"array of strings")))
"array of strings")
component))

(define (payload-bytes->manifest payload)
(define value
Expand Down Expand Up @@ -183,9 +206,13 @@
(define duplicate-identity
(check-duplicates
(map (lambda (artifact)
;; The component discriminator is part of the identity: one feed
;; may carry an app and a CLI built from the same release, while
;; component-less legacy entries stay unique per triple.
(list (update-artifact-platform artifact)
(update-artifact-architecture artifact)
(update-artifact-installer artifact)))
(update-artifact-installer artifact)
(update-artifact-component artifact)))
artifacts)
equal?))
(when duplicate-identity
Expand Down
5 changes: 4 additions & 1 deletion rivet/distribution/updater.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@

(struct updater-config
(application-id current-version channel platform architecture
public-key expected-key-id rollout-bucket maximum-download-bytes)
public-key expected-key-id rollout-bucket maximum-download-bytes
component)
#:transparent)

(struct update-candidate (manifest artifact) #:transparent)
Expand Down Expand Up @@ -289,12 +290,14 @@
[(>= (updater-config-rollout-bucket config)
(update-manifest-rollout manifest)) #f]
[else
(define component (updater-config-component config))
(define artifact
(for/first ([item (in-list (update-manifest-artifacts manifest))]
#:when (and (eq? (update-artifact-platform item)
(updater-config-platform config))
(eq? (update-artifact-architecture item)
(updater-config-architecture config))
(eq? (update-artifact-component item) component)
(artifact-matches-install-kind? item install-kind)))
item))
(and artifact (update-candidate manifest artifact))]))
Expand Down
14 changes: 7 additions & 7 deletions rivet/tests/distribution.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,21 @@

(define sample-artifact
(update-artifact 'windows 'x64 "https://updates.example/app.msi"
(make-string 64 #\a) 4 'msi '("/quiet")))
(make-string 64 #\a) 4 'msi '("/quiet") #f))
(define sample-manifest
(update-manifest "dev.rivet.test" "1.2.0" 12 'stable
"2026-09-28T00:00:00Z" "1.0.0" "1.1.0"
#t 100 (list sample-artifact)))

(define linux-config
(updater-config "dev.rivet.test" "1.1.0" 'stable 'linux 'x64
#f #f 0 (* 1024 1024)))
#f #f 0 (* 1024 1024) #f))
(define linux-zip
(update-artifact 'linux 'x64 "https://updates.example/app.zip"
(make-string 64 #\b) 4 'zip '()))
(make-string 64 #\b) 4 'zip '() #f))
(define linux-appimage
(update-artifact 'linux 'x64 "https://updates.example/app.AppImage"
(make-string 64 #\c) 4 'appimage '()))
(make-string 64 #\c) 4 'appimage '() #f))
(define linux-manifest
(struct-copy update-manifest sample-manifest
[artifacts (list linux-zip linux-appimage)]))
Expand All @@ -90,10 +90,10 @@

(define windows-config
(updater-config "dev.rivet.test" "1.1.0" 'stable 'windows 'x64
#f #f 0 (* 1024 1024)))
#f #f 0 (* 1024 1024) #f))
(define windows-zip
(update-artifact 'windows 'x64 "https://updates.example/app.zip"
(make-string 64 #\d) 4 'zip '()))
(make-string 64 #\d) 4 'zip '() #f))
(define windows-manifest
(struct-copy update-manifest sample-manifest
[artifacts (list windows-zip sample-artifact)]))
Expand Down Expand Up @@ -220,7 +220,7 @@

(define config
(updater-config "dev.rivet.test" "1.1.0" 'stable 'windows 'x64
public-key "release-2026" 0 (* 1024 1024)))
public-key "release-2026" 0 (* 1024 1024) #f))
(check-true (update-candidate? (select-update config sample-manifest)))
(check-false
(select-update (struct-copy updater-config config [current-version "1.2.0"])
Expand Down
2 changes: 1 addition & 1 deletion rivet/tests/managed-updater.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

(define artifact
(update-artifact 'windows 'x64 "https://updates.example/app.zip"
(make-string 64 #\a) 100 'zip '()))
(make-string 64 #\a) 100 'zip '() #f))
(define manifest
(update-manifest "dev.rivet.managed-test" "2.0.0" 2 'stable
"2026-10-13T00:00:00Z" "0.0.0" "1.0.0"
Expand Down
2 changes: 1 addition & 1 deletion rivet/tests/manifest-compose.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
(define temp-root (make-temporary-file "rivet-manifest-compose-~a" 'directory))

(define (artifact platform architecture url)
(update-artifact platform architecture url (make-string 64 #\a) 1 'msi '()))
(update-artifact platform architecture url (make-string 64 #\a) 1 'msi '() #f))
(define (manifest #:artifacts artifacts #:version [version "1.4.0"])
(update-manifest "dev.rivet.smoke" version 7 'stable
"2026-10-09T10:00:00Z" "0.0.0" #f #t 100 artifacts))
Expand Down
2 changes: 1 addition & 1 deletion rivet/tests/platform-update-adapter.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
(define (candidate installer [version "2.0.0"])
(define artifact
(update-artifact platform 'x64 "https://updates.example/application.zip"
(make-string 64 #\a) 1 installer '()))
(make-string 64 #\a) 1 installer '() #f))
(update-candidate
(update-manifest "dev.rivet.adapter-test" version 2 'stable
"2026-10-10T00:00:00Z" "1.0.0" "1.0.0"
Expand Down
Loading
Loading