diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 53a56cc..0fd96e4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -35,7 +35,7 @@ jobs: - name: Validate distribution contract run: | node scripts/validate-distribution-contract.mjs - node --test scripts/distribution-contract.test.mjs + node --test scripts/distribution-contract.test.mjs scripts/aqua-registry.test.mjs - name: Install Aqua for registry validation uses: aquaproj/aqua-installer@96a9bc20066c5bf5e275b41019cfc165b25f4e2e # v4.0.5 with: @@ -46,8 +46,8 @@ jobs: AQUA_CONFIG: ${{ github.workspace }}/tests/aqua/aqua.yaml AQUA_POLICY_CONFIG: ${{ github.workspace }}/tests/aqua/aqua-policy.yaml run: | - aqua update-checksum aqua update + aqua update-checksum git diff --exit-code -- tests/aqua/aqua-checksums.json tests/aqua/aqua.yaml for environment in darwin/amd64 darwin/arm64 linux/amd64 linux/arm64; do root="$RUNNER_TEMP/aqua-${environment//\//-}" @@ -158,6 +158,7 @@ jobs: test -s catalogs/azure.json test -s catalogs/simple-icons.json test -s scripts/generate-provider-catalogs.mjs + test -s scripts/aqua-registry.test.mjs test -s scripts/sync-example-templates.mjs test -s scripts/validate-provider-catalogs.mjs test -s scripts/validate-distribution-contract.mjs diff --git a/README.md b/README.md index 5c13132..af409de 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ `stack-sh/cli` is the open-source native Rust `stack` command for Stack architecture diagrams. -The repository contains native validation, formatting, and rendering commands. [Stack CLI 0.3.0](https://github.com/stack-sh/cli/releases/tag/v0.3.0) is the first supported native binary release for macOS 13 or newer and glibc-based Linux 2.31 or newer, on arm64 and x86_64. GitHub Releases and Homebrew are available; Cargo, Aqua, and self-update are still planned. The target matrix, artifact names, verification material, channel ownership, and rollback rules are defined by the [distribution contract](./docs/distribution.md), with signing and verification procedures in the [supply-chain guide](./docs/supply-chain.md). +The repository contains native validation, formatting, and rendering commands. [Stack CLI 0.3.0](https://github.com/stack-sh/cli/releases/tag/v0.3.0) is the first supported native binary release for macOS 13 or newer and glibc-based Linux 2.31 or newer, on arm64 and x86_64. GitHub Releases, Homebrew, and the owner-maintained Aqua registry are available; Cargo and self-update are still planned. The target matrix, artifact names, verification material, channel ownership, and rollback rules are defined by the [distribution contract](./docs/distribution.md), with signing and verification procedures in the [supply-chain guide](./docs/supply-chain.md). ## Install @@ -12,6 +12,15 @@ On supported Homebrew hosts, install the owner-maintained formula: brew install stack-sh/tap/stack ``` +For Aqua, copy the pinned configuration and policy from the [Aqua installation section](./docs/distribution.md#aqua-installation), review them, allow the policy, and install the checksum-locked package: + +```sh +aqua policy allow +aqua update-checksum +aqua install +stack --version +``` + For a direct installation, download the archive for your target and its verification material from [GitHub Releases](https://github.com/stack-sh/cli/releases/tag/v0.3.0). Verify the checksum signature and both attestations by following the [supply-chain guide](./docs/supply-chain.md), then follow the [direct installation steps](./docs/distribution.md#direct-installation). The macOS artifacts are reproducibly ad-hoc signed, not Apple-notarized; Sigstore and GitHub attestations provide the publisher-identity check. ## Commands diff --git a/aqua/README.md b/aqua/README.md index b77a654..32d2d58 100644 --- a/aqua/README.md +++ b/aqua/README.md @@ -9,8 +9,8 @@ The registry supports only the release contract's tier-1 environments: macOS and Install the pinned Aqua version used by CI, then test every supported mapping without executing a foreign-architecture binary: ```sh -aqua update-checksum aqua update +aqua update-checksum for environment in darwin/amd64 darwin/arm64 linux/amd64 linux/arm64; do AQUA_CONFIG=tests/aqua/aqua.yaml \ AQUA_POLICY_CONFIG=tests/aqua/aqua-policy.yaml \ @@ -20,7 +20,7 @@ for environment in darwin/amd64 darwin/arm64 linux/amd64 linux/arm64; do done ``` -`aqua update-checksum` must reproduce `tests/aqua/aqua-checksums.json` exactly. The file locks all four release archives to the SHA-256 values obtained from the release checksum asset after Aqua verifies its Sigstore bundle. `aqua update` must leave the pinned fixture unchanged until a newer stable release exists. +`aqua update` must leave the pinned fixture unchanged until a newer stable release exists. `aqua update-checksum` must then reproduce `tests/aqua/aqua-checksums.json` exactly. The file locks all four release archives to the SHA-256 values obtained from the release checksum asset after Aqua verifies its Sigstore bundle. On the native host, repeat without `--test` in an isolated `AQUA_ROOT_DIR`, then run `stack --version`, `stack init`, `stack check`, and `stack render`. diff --git a/distribution/distribution-contract.json b/distribution/distribution-contract.json index 9d79e7a..f089144 100644 --- a/distribution/distribution-contract.json +++ b/distribution/distribution-contract.json @@ -11,7 +11,7 @@ }, "availability": { "state": "available", - "message": "Stack CLI 0.3.0 is available from GitHub Releases and Homebrew. Cargo, Aqua, and self-update remain planned." + "message": "Stack CLI 0.3.0 is available from GitHub Releases, Homebrew, and the owner-maintained Aqua registry. Cargo and self-update remain planned." }, "versioning": { "scheme": "Semantic Versioning", @@ -144,14 +144,14 @@ }, { "id": "aqua", - "state": "planned", + "state": "available", "targets": [ "aarch64-apple-darwin", "x86_64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu" ], - "owns": "registry metadata and version pinning mapped to GitHub archives and SHA-256 values", + "owns": "owner registry metadata, immutable registry revision, version pinning, and SHA-256 lock mapped to canonical GitHub archives", "source": "github-release", "updatePolicy": "Aqua owns upgrades; stack self-update must refuse replacement" }, diff --git a/docs/distribution.md b/docs/distribution.md index 6c686f5..1cb33eb 100644 --- a/docs/distribution.md +++ b/docs/distribution.md @@ -2,7 +2,7 @@ This document defines the shared release contract for the Stack CLI. It is normative for GitHub Releases, Homebrew, Cargo, Aqua, and `stack` self-update implementations. The machine-readable source is [`distribution/distribution-contract.json`](../distribution/distribution-contract.json). -[Stack CLI 0.3.0](https://github.com/stack-sh/cli/releases/tag/v0.3.0) is available as a supported GitHub Release for every target below and through the owner-maintained Homebrew tap for the hosts marked below. Cargo, Aqua, and self-update remain **planned** and have no supported install command yet. +[Stack CLI 0.3.0](https://github.com/stack-sh/cli/releases/tag/v0.3.0) is available as a supported GitHub Release for every target below, through the owner-maintained Homebrew tap for the hosts marked below, and through the checksum-locked owner Aqua registry. Cargo and self-update remain **planned** and have no supported install command yet. ## Supported platform matrix @@ -10,10 +10,10 @@ The first supported binary matrix is intentionally narrow: | Rust target | OS | Architecture | Runtime floor | Direct | Homebrew | Cargo | Aqua | Self-update | | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `aarch64-apple-darwin` | macOS | arm64 | macOS 13 | available | available | planned | planned | planned | -| `x86_64-apple-darwin` | macOS | x86_64 | macOS 13 | available | — | planned | planned | planned | -| `aarch64-unknown-linux-gnu` | Linux | arm64 | glibc 2.31 | available | available | planned | planned | planned | -| `x86_64-unknown-linux-gnu` | Linux | x86_64 | glibc 2.31 | available | available | planned | planned | planned | +| `aarch64-apple-darwin` | macOS | arm64 | macOS 13 | available | available | planned | available | planned | +| `x86_64-apple-darwin` | macOS | x86_64 | macOS 13 | available | — | planned | available | planned | +| `aarch64-unknown-linux-gnu` | Linux | arm64 | glibc 2.31 | available | available | planned | available | planned | +| `x86_64-unknown-linux-gnu` | Linux | x86_64 | glibc 2.31 | available | available | planned | available | planned | Windows, musl-based Linux distributions such as Alpine, BSD, and 32-bit architectures are not supported release targets. A source build may happen to work elsewhere, but it is best-effort and does not block a release. Cargo installs on supported targets require Rust 1.85 or newer. Homebrew availability additionally follows [Homebrew's current tier-1 host requirements](https://docs.brew.sh/Support-Tiers); Stack does not label a host as supported when the package manager itself classifies it below tier 1. @@ -74,6 +74,57 @@ The formula does not remove or replace Stack configuration and icon stores durin Homebrew was activated after the immutable `v0.3.0` release assets were published. The release manifest therefore remains the publication-time record, while this contract and the tap CI record the later channel activation; release assets are not replaced to retrofit that state. +## Aqua installation + +The owner registry is the [`aqua/registry.yaml`](../aqua/registry.yaml) file pinned to immutable commit `42702cda91a4156901b9a601bd143c43dcf05766`. Aqua maps `darwin/amd64`, `darwin/arm64`, `linux/amd64`, and `linux/arm64` to the four canonical GitHub Release archives, reads their SHA-256 values from the signed checksum asset, and verifies the checksum bundle against the tagged `release.yaml` workflow identity. + +Add the following `aqua.yaml` to a Git repository: + +```yaml +checksum: + enabled: true + require_checksum: true +registries: + - name: stack-sh + type: github_content + repo_owner: stack-sh + repo_name: cli + ref: 42702cda91a4156901b9a601bd143c43dcf05766 + path: aqua/registry.yaml +packages: + - name: stack-sh/cli@v0.3.0 + registry: stack-sh +``` + +Because Aqua denies non-standard registries by default, add and review this narrow `aqua-policy.yaml` rather than disabling policy: + +```yaml +registries: + - name: stack-sh + type: github_content + repo_owner: stack-sh + repo_name: cli + ref: 'Version == "42702cda91a4156901b9a601bd143c43dcf05766"' + path: aqua/registry.yaml +packages: + - name: stack-sh/cli + registry: stack-sh + version: semver(">= 0.3.0") +``` + +Allow the reviewed policy once, generate the checksum lock, and install: + +```sh +aqua policy allow +aqua update-checksum +aqua install +stack --version +``` + +Commit `aqua-checksums.json` with the configuration. To upgrade after a new stable Stack release, run `aqua update`, review the version change, then run `aqua update-checksum` and `aqua install`. Aqua owns the replacement; `stack` self-update must refuse to overwrite it. The registry maintainer procedure and four-target test command are in [`aqua/README.md`](../aqua/README.md). + +Aqua was activated after the immutable `v0.3.0` release assets were published. The release manifest remains the publication-time record; the pinned registry commit, generated checksum lock, contract, and CI runs are the later activation evidence. No release asset is replaced. + ## Direct installation Download [Stack CLI 0.3.0](https://github.com/stack-sh/cli/releases/tag/v0.3.0), select the archive whose target matches the supported platform table, and obtain all matching verification material. Complete the [supply-chain verification](./supply-chain.md), then extract and install the verified binary. Replace `{target}` with the exact release target: diff --git a/docs/releases/v0.3.0.md b/docs/releases/v0.3.0.md index 5f2dd98..9cc51e6 100644 --- a/docs/releases/v0.3.0.md +++ b/docs/releases/v0.3.0.md @@ -10,7 +10,7 @@ Highlights: - Install on macOS 13 or newer and glibc-based Linux 2.31 or newer, on arm64 or x86_64. - Verify every archive with SHA-256, keyless Sigstore, SLSA provenance, and an SPDX 2.3 SBOM attestation. -Download the archive for your target and all matching verification materials from this release. Follow the [release verification guide](https://github.com/stack-sh/cli/blob/v0.3.0/docs/supply-chain.md) before installing the `stack` binary. Homebrew, Cargo, Aqua, and self-update channels are not available in this release. +Download the archive for your target and all matching verification materials from this release. Follow the [release verification guide](https://github.com/stack-sh/cli/blob/v0.3.0/docs/supply-chain.md) before installing the `stack` binary. At publication time, Homebrew, Cargo, Aqua, and self-update were not available. Homebrew and Aqua were activated later without replacing these immutable assets; see the [current distribution contract](../distribution.md) for supported install commands. The macOS binaries have a reproducible ad-hoc signature and are not Apple-notarized. Use the Sigstore verification flow above as the publisher-identity check. After verification, replace `{target}` with the downloaded target and install the binary: diff --git a/scripts/aqua-registry.test.mjs b/scripts/aqua-registry.test.mjs new file mode 100644 index 0000000..88169f5 --- /dev/null +++ b/scripts/aqua-registry.test.mjs @@ -0,0 +1,63 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const configuration = fs.readFileSync(path.join(root, "tests/aqua/aqua.yaml"), "utf8"); +const policy = fs.readFileSync(path.join(root, "tests/aqua/aqua-policy.yaml"), "utf8"); +const registry = fs.readFileSync(path.join(root, "aqua/registry.yaml"), "utf8"); +const distribution = fs.readFileSync(path.join(root, "docs/distribution.md"), "utf8"); +const checksums = JSON.parse( + fs.readFileSync(path.join(root, "tests/aqua/aqua-checksums.json"), "utf8"), +); + +const targets = [ + "aarch64-apple-darwin", + "aarch64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", +]; + +test("the owner registry is pinned to one immutable revision", () => { + const revision = configuration.match(/^\s+ref: ([0-9a-f]{40})$/m)?.[1]; + + assert.match(revision ?? "", /^[0-9a-f]{40}$/); + assert.ok(policy.includes(`ref: 'Version == "${revision}"'`)); + assert.ok(distribution.includes(`immutable commit \`${revision}\``)); + assert.ok(distribution.includes(`ref: ${revision}`)); + assert.ok(distribution.includes(`ref: 'Version == "${revision}"'`)); + assert.ok(!configuration.includes("ref: main")); +}); + +test("the registry maps exactly the four supported release targets", () => { + assert.ok(registry.includes("asset: stack-{{.Version}}-{{.Arch}}-{{.OS}}.{{.Format}}")); + assert.ok(registry.includes("asset: stack-{{.Version}}-checksums.txt")); + assert.ok(registry.includes("asset: stack-{{.Version}}-checksums.txt.sigstore.json")); + assert.ok(registry.includes("src: \"{{.AssetWithoutExt}}/stack\"")); + for (const environment of ["darwin/amd64", "darwin/arm64", "linux/amd64", "linux/arm64"]) { + assert.ok(registry.includes(`- ${environment}`)); + } + assert.ok(!registry.includes("windows/")); +}); + +test("the checksum lock covers every archive and the registry revision", () => { + const revision = configuration.match(/^\s+ref: ([0-9a-f]{40})$/m)?.[1]; + const expectedIds = targets.map( + (target) => + `github_release/github.com/stack-sh/cli/v0.3.0/stack-v0.3.0-${target}.tar.gz`, + ); + expectedIds.push( + `registries/github_content/github.com/stack-sh/cli/${revision}/aqua/registry.yaml`, + ); + + assert.deepEqual( + checksums.checksums.map(({ id }) => id).sort(), + expectedIds.sort(), + ); + for (const entry of checksums.checksums) { + assert.match(entry.checksum, /^[A-F0-9]+$/); + assert.equal(entry.algorithm, entry.id.startsWith("registries/") ? "sha512" : "sha256"); + } +}); diff --git a/scripts/distribution-contract.test.mjs b/scripts/distribution-contract.test.mjs index 7f55d2b..de466dd 100644 --- a/scripts/distribution-contract.test.mjs +++ b/scripts/distribution-contract.test.mjs @@ -73,6 +73,13 @@ test("the activated Homebrew channel cannot regress to planned", () => { assert.throws(() => validateDistributionContract(candidate, cargoToml), /homebrew state must be available/); }); +test("the activated Aqua channel cannot regress to planned", () => { + const candidate = changed((value) => { + value.channels.find(({ id }) => id === "aqua").state = "planned"; + }); + assert.throws(() => validateDistributionContract(candidate, cargoToml), /aqua state must be available/); +}); + test("an unactivated package-manager channel cannot become available", () => { const candidate = changed((value) => { value.channels.find(({ id }) => id === "cargo").state = "available"; diff --git a/scripts/validate-distribution-contract.mjs b/scripts/validate-distribution-contract.mjs index 40f503c..f8cc86d 100644 --- a/scripts/validate-distribution-contract.mjs +++ b/scripts/validate-distribution-contract.mjs @@ -12,7 +12,7 @@ const expectedTargets = [ "x86_64-unknown-linux-gnu", ]; const expectedChannels = ["aqua", "cargo", "github-release", "homebrew", "self-update"]; -const availableChannels = new Set(["github-release", "homebrew"]); +const availableChannels = new Set(["aqua", "github-release", "homebrew"]); const requiredArchiveEntries = ["LICENSE", "NOTICE", "THIRD_PARTY_LICENSES.md", "stack"]; const requiredUnsupportedTerms = ["32-bit", "BSD", "Windows", "musl"]; const requiredActivationTerms = ["Cargo package version", "SBOMs", "provenance", "stack --version"]; @@ -57,8 +57,9 @@ export function validateDistributionContract(contract, cargoToml) { invariant( contract.availability?.message?.includes("Stack CLI 0.3.0") && contract.availability.message.includes("GitHub Releases") && - contract.availability.message.includes("Homebrew"), - "availability message must identify the verified stable GitHub release and Homebrew channel", + contract.availability.message.includes("Homebrew") && + contract.availability.message.includes("Aqua"), + "availability message must identify the verified stable GitHub release, Homebrew, and Aqua channels", ); invariant( contract.product.currentSourceVersion === cargoValue(cargoToml, "version"), diff --git a/tests/aqua/aqua-checksums.json b/tests/aqua/aqua-checksums.json index a12e7d7..93385f6 100644 --- a/tests/aqua/aqua-checksums.json +++ b/tests/aqua/aqua-checksums.json @@ -19,6 +19,11 @@ "id": "github_release/github.com/stack-sh/cli/v0.3.0/stack-v0.3.0-x86_64-unknown-linux-gnu.tar.gz", "checksum": "6C9AB21D96FC6FB0A12A6FF4F7F56103D8DD8513AC85C8D30AB6E072C5C6A7D9", "algorithm": "sha256" + }, + { + "id": "registries/github_content/github.com/stack-sh/cli/42702cda91a4156901b9a601bd143c43dcf05766/aqua/registry.yaml", + "checksum": "FCF882176F20C6C85A07B848935633EC45F19B0A04A7010E14D051A71EAD5AAC1A6BE2F60B4F730D72821DFB293CF3CEFCE92A680059A3259D8FD85A32A3D4BF", + "algorithm": "sha512" } ] } diff --git a/tests/aqua/aqua-policy.yaml b/tests/aqua/aqua-policy.yaml index 79b9ab5..ea347b6 100644 --- a/tests/aqua/aqua-policy.yaml +++ b/tests/aqua/aqua-policy.yaml @@ -2,8 +2,11 @@ registries: - name: stack-sh - type: local - path: ../../aqua/registry.yaml + type: github_content + repo_owner: stack-sh + repo_name: cli + ref: 'Version == "42702cda91a4156901b9a601bd143c43dcf05766"' + path: aqua/registry.yaml packages: - name: stack-sh/cli registry: stack-sh diff --git a/tests/aqua/aqua.yaml b/tests/aqua/aqua.yaml index eb64f79..7ae3393 100644 --- a/tests/aqua/aqua.yaml +++ b/tests/aqua/aqua.yaml @@ -7,8 +7,11 @@ checksum: - all registries: - name: stack-sh - type: local - path: ../../aqua/registry.yaml + type: github_content + repo_owner: stack-sh + repo_name: cli + ref: 42702cda91a4156901b9a601bd143c43dcf05766 + path: aqua/registry.yaml packages: - name: stack-sh/cli@v0.3.0 registry: stack-sh