Skip to content

Commit d273cb4

Browse files
c1-squire-dev[bot]gsilvestrinclaude
authored
feat(record-release): forward the Tauri updater signature as asset metadata (#105)
Part of the multipass-desktop in-app auto-update feature. Design: ductone/multipass#553; app: ductone/multipass#554. `record-release` reads `manifest.json` and POSTs a RecordRelease request to the registry ingest endpoint; this forwards the updater bundle's minisign signature so it reaches the registry as per-asset metadata. ## Changes - `proto/artifacts/v1/manifest.proto`: `string updater_signature = 10;` on `Asset` (JSON `updaterSignature`). Regenerated bindings. - `cmd/record-release/main.go`: map `updaterSignature` → the request's `assets[].metadata["updater.signature"]` (only for the asset that carries it). - Tests added. Also confirmed `cmd/merge-manifests` round-trips assets through the proto with `DiscardUnknown`, so the proto field is what preserves the signature through the merge → record-release chain (no change needed there). ## Codegen `buf generate proto` with the plugin version referenced in the proto header (protocolbuffers/go v1.36.10); reverted an unrelated `buf format` reordering to keep the diff minimal. ## Verified `go build`/`vet`/`test ./...` all pass. The registry ingest maps JSON `assets[].metadata` → `registry.v1.Asset.metadata` server-side (the paired `metadata` field in ductone/connector-registry-api). Pairs with generate-dist-manifest (multipass-workflows) and the mirror allowlist. --------- Co-authored-by: Giancarlo Silvestrin <giancarlo.silvestrin@conductorone.com> Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b01c222 commit d273cb4

4 files changed

Lines changed: 148 additions & 33 deletions

File tree

cmd/record-release/main.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,18 @@ type ReleaseAsset struct {
5050
CertificateURL string `json:"certificateUrl,omitempty"`
5151
SbomURL string `json:"sbomUrl,omitempty"`
5252
Attestations []*ReleaseAttestation `json:"attestations,omitempty"`
53+
// Metadata carries free-form, per-asset key/value pairs that the registry
54+
// stores on registry.v1.Asset.metadata. Currently used to forward the Tauri
55+
// updater bundle's minisign signature under updaterSignatureMetadataKey.
56+
// Left nil (and omitted from JSON) for assets without any metadata.
57+
Metadata map[string]string `json:"metadata,omitempty"`
5358
}
5459

60+
// updaterSignatureMetadataKey is the well-known registry metadata key under
61+
// which a Tauri auto-update bundle's base64 minisign signature is stored
62+
// (registry.v1.Asset.metadata["updater.signature"]).
63+
const updaterSignatureMetadataKey = "updater.signature"
64+
5565
// ReleaseImage is the transformed image for the registry API.
5666
type ReleaseImage struct {
5767
Ref string `json:"ref"`
@@ -316,7 +326,7 @@ func main() {
316326
func transformAssets(manifest *pb.Manifest) map[string]*ReleaseAsset {
317327
assets := make(map[string]*ReleaseAsset)
318328
for platform, asset := range manifest.GetAssets() {
319-
assets[platform] = &ReleaseAsset{
329+
ra := &ReleaseAsset{
320330
Platform: platform,
321331
Filename: asset.GetFilename(),
322332
MediaType: asset.GetMediaType(),
@@ -328,6 +338,13 @@ func transformAssets(manifest *pb.Manifest) map[string]*ReleaseAsset {
328338
SbomURL: asset.GetSbomHref(),
329339
Attestations: transformAttestations(asset.GetAttestations()),
330340
}
341+
// The Tauri updater bundle carries a base64 minisign signature that must
342+
// reach the registry as per-asset metadata. Only the updater asset sets
343+
// updaterSignature; every other asset leaves Metadata nil/omitted.
344+
if sig := asset.GetUpdaterSignature(); sig != "" {
345+
ra.Metadata = map[string]string{updaterSignatureMetadataKey: sig}
346+
}
347+
assets[platform] = ra
331348
}
332349

333350
return assets

cmd/record-release/main_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"encoding/json"
55
"reflect"
6+
"strings"
67
"testing"
78

89
pb "github.com/ConductorOne/github-workflows/pb/artifacts/v1"
@@ -43,6 +44,54 @@ func TestTransformAssetsPreservesAssetAttestations(t *testing.T) {
4344
}
4445
}
4546

47+
func TestTransformAssetsMapsUpdaterSignatureToMetadata(t *testing.T) {
48+
manifest := pb.Manifest_builder{
49+
Assets: map[string]*pb.Asset{
50+
// The macOS updater bundle carries the minisign signature.
51+
"darwin-universal-updater": pb.Asset_builder{
52+
Filename: strPtr("baton-example-v1.2.3-darwin-universal.app.tar.gz"),
53+
MediaType: strPtr("application/gzip"),
54+
Href: strPtr("https://dist.example.com/updater.app.tar.gz"),
55+
UpdaterSignature: strPtr("dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduCg=="),
56+
}.Build(),
57+
// A regular asset must not gain any metadata.
58+
"linux-amd64": pb.Asset_builder{
59+
Filename: strPtr("baton-example-v1.2.3-linux-amd64.tar.gz"),
60+
MediaType: strPtr("application/gzip"),
61+
Href: strPtr("https://dist.example.com/asset.tar.gz"),
62+
}.Build(),
63+
},
64+
}.Build()
65+
66+
assets := transformAssets(manifest)
67+
68+
updater := assets["darwin-universal-updater"]
69+
wantMeta := map[string]string{"updater.signature": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduCg=="}
70+
if !reflect.DeepEqual(updater.Metadata, wantMeta) {
71+
t.Fatalf("updater metadata = %#v, want %#v", updater.Metadata, wantMeta)
72+
}
73+
74+
if plain := assets["linux-amd64"]; plain.Metadata != nil {
75+
t.Fatalf("non-updater asset metadata = %#v, want nil", plain.Metadata)
76+
}
77+
78+
// Metadata is omitted from JSON when nil, present when set.
79+
plainBody, err := json.Marshal(assets["linux-amd64"])
80+
if err != nil {
81+
t.Fatalf("marshal plain asset: %v", err)
82+
}
83+
if strings.Contains(string(plainBody), "\"metadata\"") {
84+
t.Fatalf("plain asset JSON unexpectedly contains metadata: %s", plainBody)
85+
}
86+
updaterBody, err := json.Marshal(updater)
87+
if err != nil {
88+
t.Fatalf("marshal updater asset: %v", err)
89+
}
90+
if !strings.Contains(string(updaterBody), "\"updater.signature\"") {
91+
t.Fatalf("updater asset JSON missing metadata signature: %s", updaterBody)
92+
}
93+
}
94+
4695
func TestTransformAttestationsSkipsIncompleteAssetEntries(t *testing.T) {
4796
got := transformAttestations([]*pb.AttestationDescriptor{
4897
attestation(slsaProvenance, "https://dist.example.com/provenance.sigstore.json"),

pb/artifacts/v1/manifest.pb.go

Lines changed: 73 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/artifacts/v1/manifest.proto

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ message Asset {
8787
// For your customer flow (verify one OS artifact at a time), this is the recommended place to link
8888
// per-artifact provenance and/or SBOM attestations stored in S3.
8989
repeated AttestationDescriptor attestations = 9;
90+
91+
// updater_signature is the base64-encoded minisign signature for a Tauri
92+
// in-app auto-update bundle. Only set on updater-bundle assets
93+
// ({platform}-updater); empty for all other assets. The value is forwarded to
94+
// the connector registry as per-asset metadata under the well-known key
95+
// "updater.signature".
96+
// JSON key: "updaterSignature".
97+
string updater_signature = 10;
9098
}
9199

92100
// Image represents metadata for a container image (digest-first).

0 commit comments

Comments
 (0)