From 6e33232784de7ac1694b503bbf84a378a8bbd0af Mon Sep 17 00:00:00 2001 From: konojunya Date: Sun, 6 Sep 2026 05:24:35 +0900 Subject: [PATCH] Activate verified CLI 0.5.1 channels and prepare trusted Cargo publishing --- .github/workflows/cargo-publish.yaml | 95 ++++++++++++++++++++++ .github/workflows/ci.yaml | 2 +- README.md | 19 +++-- distribution/distribution-contract.json | 10 +-- docs/cargo-releasing.md | 8 ++ docs/completions.md | 4 +- docs/distribution.md | 47 +++++++---- docs/publication-audit.md | 2 +- docs/self-update.md | 2 +- scripts/cargo-publish-context.mjs | 44 ++++++++++ scripts/cargo-publish-context.test.mjs | 40 +++++++++ scripts/distribution-contract.test.mjs | 8 +- scripts/validate-distribution-contract.mjs | 4 +- skills/docs-source.json | 4 +- skills/stack-diagrams/SKILL.md | 4 +- tests/aqua/aqua-checksums.json | 16 ++-- tests/aqua/aqua.yaml | 2 +- 17 files changed, 262 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/cargo-publish.yaml create mode 100644 scripts/cargo-publish-context.mjs create mode 100644 scripts/cargo-publish-context.test.mjs diff --git a/.github/workflows/cargo-publish.yaml b/.github/workflows/cargo-publish.yaml new file mode 100644 index 0000000..e8173d8 --- /dev/null +++ b/.github/workflows/cargo-publish.yaml @@ -0,0 +1,95 @@ +name: Cargo trusted publishing + +on: + workflow_dispatch: + inputs: + expected_sha: + description: Exact main commit whose CI has succeeded + required: true + type: string + version: + description: Exact package version in the selected source + required: true + type: string + publish: + description: Publish a new version (false only verifies packaging and OIDC) + required: true + default: false + type: boolean + +permissions: + contents: read + actions: read + +concurrency: + group: cargo-trusted-publishing + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + publish: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + contents: read + actions: read + id-token: write + env: + EXPECTED_SHA: ${{ inputs.expected_sha }} + EXPECTED_VERSION: ${{ inputs.version }} + PACKAGE_NAME: stack-diagram-cli + PUBLISH: ${{ inputs.publish }} + steps: + - name: Reject unexpected dispatch context + run: | + test "$GITHUB_REPOSITORY" = stack-sh/cli + test "$GITHUB_REF" = refs/heads/main + [[ "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]] + test "$GITHUB_SHA" = "$EXPECTED_SHA" + [[ "$EXPECTED_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] + case "$PACKAGE_NAME" in stack-diagram-cli) ;; *) exit 1 ;; esac + case "$PUBLISH" in true|false) ;; *) exit 1 ;; esac + + - name: Check out exact main source without persisted credentials + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Install minimum supported Rust + run: rustup toolchain install 1.85.0 --profile minimal + + - name: Verify package, successful CI, and immutable registry state + env: + GH_TOKEN: ${{ github.token }} + run: | + cargo +1.85.0 metadata --no-deps --locked --format-version 1 > "$RUNNER_TEMP/package.json" + gh run list --repo stack-sh/cli --workflow ci.yaml --event push --branch main --commit "$EXPECTED_SHA" --limit 1 --json status,conclusion,headSha > "$RUNNER_TEMP/ci.json" + node scripts/cargo-publish-context.mjs "$RUNNER_TEMP/package.json" "$RUNNER_TEMP/ci.json" + + - name: Verify package without registry credentials + run: cargo +1.85.0 publish --package "$PACKAGE_NAME" --registry crates-io --locked --dry-run + + - name: Exchange GitHub OIDC identity for a short-lived registry token + id: auth + uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 + + - name: Verify OIDC exchange without publishing + if: inputs.publish == false + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + run: | + test -n "$CARGO_REGISTRY_TOKEN" + echo "OIDC exchange verified; no crate was published." + + - name: Publish the previously verified new version + if: inputs.publish == true + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + run: | + test -n "$CARGO_REGISTRY_TOKEN" + cargo +1.85.0 publish --package "$PACKAGE_NAME" --registry crates-io --locked diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 23b34d5..4e5bb55 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: - name: Check out repository uses: actions/checkout@v7 - name: Validate initial Cargo publication guards - run: node --test scripts/initial-publish-context.test.mjs + run: node --test scripts/initial-publish-context.test.mjs scripts/cargo-publish-context.test.mjs - name: Read supported specification revision id: specification diff --git a/README.md b/README.md index 271278a..4ccd1ef 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.5.0](https://github.com/stack-sh/cli/releases/tag/v0.5.0) is the 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 remains planned. Self-update was removed in 0.5.0; use the installation owner to upgrade. 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.5.1](https://github.com/stack-sh/cli/releases/tag/v0.5.1) is the 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, the owner-maintained Aqua registry, and Cargo are available. Self-update was removed in 0.5.0; use the installation owner to upgrade. 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 ``` +With Rust 1.85 or newer and a native linker, install from crates.io: + +```sh +cargo install stack-diagram-cli --version 0.5.1 --locked +stack --version +``` + +The package is `stack-diagram-cli`, not the unrelated `stack-cli`; the binary is `stack`. Cargo owns updates and uninstall, and does not automatically install completion or manual files. See [Cargo installation](./docs/distribution.md#cargo-installation). + 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 @@ -21,11 +30,11 @@ 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.5.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. +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.5.1). 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 -The command inventory below is available in the published 0.5.0 binaries, including `stack doctor`, `stack config`, and structured JSON output for automation. +The command inventory below is available in the published 0.5.1 binaries, including `stack doctor`, `stack config`, and structured JSON output for automation. ```text stack help @@ -72,9 +81,9 @@ stack manpage `stack config path` prints the selected `config.yaml` path without creating or reading the file. `stack config get default_icons_path` strictly reads the supported configuration and prints the effective icon-store path. `stack doctor` reports the CLI version, configuration path and source, configuration validity, effective icon-store source, and installed known-provider packs. It is read-only, emits actionable categories instead of configuration contents, exits `0` for healthy and warning-only reports, and exits `2` when it finds an operational problem. See the [configuration discovery and doctor contract](./docs/configuration.md). -`stack update` is removed in 0.5.0. Homebrew, Aqua, and future Cargo installations are updated through their package manager; direct downloads are updated manually after verification. See the [upgrade and migration guide](./docs/self-update.md). +`stack update` is removed in 0.5.0. Homebrew, Aqua, and Cargo installations are updated through their package manager; direct downloads are updated manually after verification. See the [upgrade and migration guide](./docs/self-update.md). -`stack completions ` and `stack manpage` generate deterministic shell integration and an offline roff manual from the CLI command metadata. The 0.5.0 release archives carry the exact generated files; Homebrew installs them into its managed completion and manual paths, while direct, Aqua, and future Cargo users can generate them into user-owned locations without modifying shell startup files. See the [completion and manual guide](./docs/completions.md). +`stack completions ` and `stack manpage` generate deterministic shell integration and an offline roff manual from the CLI command metadata. The 0.5.1 release archives carry the exact generated files; Homebrew installs them into its managed completion and manual paths, while direct, Aqua, and Cargo users can generate them into user-owned locations without modifying shell startup files. See the [completion and manual guide](./docs/completions.md). `stack icons list [PROVIDER] [QUERY]` searches the asset-free catalog by ID, product name, or category. The catalog currently contains 1,051 IDs: 305 AWS, 45 Google Cloud, 639 Azure, and 62 curated developer and collaboration tool icons. This command reads only metadata embedded in the CLI. diff --git a/distribution/distribution-contract.json b/distribution/distribution-contract.json index 24d8436..90c0c18 100644 --- a/distribution/distribution-contract.json +++ b/distribution/distribution-contract.json @@ -4,15 +4,15 @@ "product": { "binary": "stack", "sourceCargoPackage": "stack-diagram-cli", - "publishedCargoPackage": null, + "publishedCargoPackage": "stack-diagram-cli", "sourceVersionFile": "Cargo.toml", "currentSourceVersion": "0.5.1", "minimumRustVersion": "1.85", - "currentReleaseVersion": "0.5.0" + "currentReleaseVersion": "0.5.1" }, "availability": { "state": "available", - "message": "Stack CLI 0.5.1 prepares the first Cargo distribution. GitHub Releases, Homebrew, and Aqua currently distribute 0.5.0; Cargo remains planned until registry publication and clean-install verification." + "message": "Stack CLI 0.5.1 is available through GitHub Releases, Homebrew, Aqua, and Cargo. Cargo installs the stack-diagram-cli source package using Rust 1.85 or newer and registry-only dependencies." }, "versioning": { "scheme": "Semantic Versioning", @@ -142,14 +142,14 @@ }, { "id": "cargo", - "state": "planned", + "state": "available", "targets": [ "aarch64-apple-darwin", "x86_64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu" ], - "owns": "a future unambiguous crates.io source package and dependency graph; installs the stack binary with Rust 1.85 or newer", + "owns": "the stack-diagram-cli crates.io source package and registry-only locked dependency graph; installs the stack binary with Rust 1.85 or newer", "source": "crates.io", "updatePolicy": "Cargo owns upgrades; stack never replaces its own executable" }, diff --git a/docs/cargo-releasing.md b/docs/cargo-releasing.md index e9e4aa0..2333d39 100644 --- a/docs/cargo-releasing.md +++ b/docs/cargo-releasing.md @@ -9,3 +9,11 @@ The initial publication creates `stack-diagram-cli` version `0.5.1`. The workflo 5. Remove the GitHub bootstrap secret and revoke the crates.io token. Configure a crates.io trusted publisher for the ongoing release workflow before any later publication. Do not reuse this initial workflow for updates or broaden the bootstrap token. The token is supplied only to the publication step through `CARGO_REGISTRY_TOKEN`; the workflow never runs `cargo login` or writes a credentials file. It cannot configure trusted publishing on behalf of a crate owner. See the [Cargo publication reference](https://doc.rust-lang.org/cargo/commands/cargo-publish.html) for upload and timeout behavior. + +## Ongoing trusted publishing + +After initial publication, configure each crate's Settings → Trusted Publishing on crates.io with repository owner `stack-sh`, repository name `cli`, workflow filename `cargo-publish.yaml`, and no environment. The crate owner must save these settings; committing this workflow does not configure or prove registry trust. Follow the [crates.io instructions](https://crates.io/docs/trusted-publishing). + +Dispatch `cargo-publish.yaml` from `main` with the full successful main CI commit and the exact package version. The default `publish: false` validates identity, registry state, and packaging, then checks the OIDC exchange **without uploading a crate**. This proves workflow authentication, not a new version's publication or every crate's owner configuration. The pinned authentication action revokes its short-lived token when the job ends; no long-lived repository secret or credentials file is used. + +For an actual new release, merge the version change and all checks first, publish dependencies before consumers, then dispatch with `publish: true`. Existing versions, missing crates, non-main refs, version/SHA drift, and unsuccessful CI fail closed. Verify the downloaded archive checksum and source SHA after publication; a failed post-upload check does not undo an upload. Never rerun an upload without checking registry state. Keep the native release version/source identical and verify each package-manager channel separately. diff --git a/docs/completions.md b/docs/completions.md index 64eaf52..893040d 100644 --- a/docs/completions.md +++ b/docs/completions.md @@ -26,9 +26,9 @@ The owner-maintained formula installs the archived files through Homebrew's `bas No shell startup file is modified by the formula. -## Direct, Aqua, and future Cargo installs +## Direct, Aqua, and Cargo installs -Aqua's registry `files` mapping owns executable placement, not a user's shell startup or global manual database. A direct binary copy and a future Cargo install have the same boundary. Generate files into a user-owned location after installing the binary: +Aqua's registry `files` mapping owns executable placement, not a user's shell startup or global manual database. A direct binary copy and a Cargo install have the same boundary. Generate files into a user-owned location after installing the binary: ```sh data_root="${XDG_DATA_HOME:-$HOME/.local/share}" diff --git a/docs/distribution.md b/docs/distribution.md index 97028d3..3173650 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, and Aqua implementations. The machine-readable source is [`distribution/distribution-contract.json`](../distribution/distribution-contract.json). -[Stack CLI 0.5.0](https://github.com/stack-sh/cli/releases/tag/v0.5.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 remains **planned**. Version 0.5.0 removes self-update; see the [upgrade guide](./self-update.md). +[Stack CLI 0.5.1](https://github.com/stack-sh/cli/releases/tag/v0.5.1) 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. The registry-only Cargo source package is also available. Version 0.5.0 removes self-update; see the [upgrade guide](./self-update.md). ## 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 | | --- | --- | --- | --- | --- | --- | --- | --- | -| `aarch64-apple-darwin` | macOS | arm64 | macOS 13 | available | available | planned | available | -| `x86_64-apple-darwin` | macOS | x86_64 | macOS 13 | available | — | planned | available | -| `aarch64-unknown-linux-gnu` | Linux | arm64 | glibc 2.31 | available | available | planned | available | -| `x86_64-unknown-linux-gnu` | Linux | x86_64 | glibc 2.31 | available | available | planned | available | +| `aarch64-apple-darwin` | macOS | arm64 | macOS 13 | available | available | available | available | +| `x86_64-apple-darwin` | macOS | x86_64 | macOS 13 | available | — | available | available | +| `aarch64-unknown-linux-gnu` | Linux | arm64 | glibc 2.31 | available | available | available | available | +| `x86_64-unknown-linux-gnu` | Linux | x86_64 | glibc 2.31 | available | available | available | available | 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. @@ -67,6 +67,23 @@ The sorted checksum file uses SHA-256 and covers the release manifest, all archi The release manifest records the tag, commit, source version, `minimumSupportedCliVersion`, each target's artifact names and SHA-256 values, the build identity, and each channel whose own install smoke test passed. Its schema is [`distribution/release-manifest.schema.json`](../distribution/release-manifest.schema.json). Supply-chain generation and user verification are documented in the [supply-chain guide](./supply-chain.md). +## Cargo installation + +Install the official source package with Rust 1.85 or newer and a working native linker: + +```sh +cargo install stack-diagram-cli --version 0.5.1 --locked +stack --version +``` + +The package name is `stack-diagram-cli`; the executable name is `stack`. The unrelated `stack-cli` crate is not this project. The complete locked dependency graph comes from crates.io, including `stack-compiler 0.1.0`, `stack-theme 0.5.0`, `stack-formatter 0.1.0`, and `stack-engine 0.7.0`. The source package contains the embedded templates/catalogs and Apache-2.0 license, notice, and dependency attribution. + +Cargo places the executable in its installation root (normally `$CARGO_HOME/bin`, defaulting to `$HOME/.cargo/bin`); ensure that directory is on `PATH`. macOS needs the Xcode Command Line Tools, and Linux needs a native C compiler/linker. Supported Cargo targets are the same four native targets above. The runtime floors in the table describe prebuilt GitHub archives: Cargo compiles on your host with your local toolchain, and its binary runtime requirements and bytes may differ. Cargo source installation is not a Sigstore-attested prebuilt archive. + +Upgrade through Cargo by installing the desired published version with `--locked`, or use `cargo install stack-diagram-cli --locked` for the latest release. Uninstall with `cargo uninstall stack-diagram-cli`. Choose one installer for each binary location; do not use Cargo to replace a Homebrew- or Aqua-owned executable. Stack never replaces its own executable. Cargo does not place shell completions or manual pages automatically; use the generators described in the [shell integration guide](./completions.md). Configuration and imported icons remain outside Cargo’s binary installation root and are not removed on uninstall. + +Initial publication verifies an isolated, registry-only installation on macOS and GNU/Linux arm64 / x86_64 with both Rust 1.85.0 and stable, then exercises version/help, templates, validation, rendering, JSON output, configuration, and generated shell assets. The [publication procedure](./cargo-releasing.md) records the bootstrap credential boundary. + ## Homebrew installation The owner-maintained [`stack-sh/homebrew-tap`](https://github.com/stack-sh/homebrew-tap) installs the canonical GitHub Release archive without rebuilding or repacking it. Homebrew is available on Apple Silicon macOS and glibc-based Linux on arm64 and x86_64 when the host meets Homebrew's current tier-1 requirements. @@ -81,7 +98,7 @@ brew uninstall stack-sh/tap/stack For releases carrying the generated assets, the formula installs bash, zsh, and fish completions plus `stack.1` through Homebrew's standard path helpers. It does not edit shell startup files. The formula does not remove or replace Stack configuration and icon stores during an upgrade or uninstall. Formula updates verify release checksums, provenance, and SBOM attestations before changing the archive mapping. The fail-closed update and recovery procedure is maintained in the tap's [maintainer guide](https://github.com/stack-sh/homebrew-tap/blob/main/docs/maintaining.md). -The Homebrew v0.5.0 formula was activated after the immutable release assets were published. Its macOS ARM64, Linux ARM64, and Linux x86_64 lifecycle tests verify the archived completion and manual bytes during install, upgrade, and uninstall. The release manifest remains the publication-time record with only `github-release` in `verifiedChannels`; this contract and the tap CI record the later channel verification without replacing any release asset. +The Homebrew v0.5.1 formula was activated after the immutable release assets were published. Its macOS ARM64, Linux ARM64, and Linux x86_64 lifecycle tests verify the archived completion and manual bytes during install, upgrade, and uninstall. The release manifest remains the publication-time record with only `github-release` in `verifiedChannels`; this contract and the tap CI record the later channel verification without replacing any release asset. ## Aqua installation @@ -101,7 +118,7 @@ registries: ref: 42702cda91a4156901b9a601bd143c43dcf05766 path: aqua/registry.yaml packages: - - name: stack-sh/cli@v0.5.0 + - name: stack-sh/cli@v0.5.1 registry: stack-sh ``` @@ -132,24 +149,24 @@ 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 never replaces its own executable. The registry maintainer procedure and four-target test command are in [`aqua/README.md`](../aqua/README.md). -Aqua installs the executable declared by its registry mapping and does not own shell startup files or a global manual database. Stack CLI 0.5.0 includes the generators; use `stack completions` and `stack manpage` to write the desired user-owned files as documented in the [completion guide](./completions.md). +Aqua installs the executable declared by its registry mapping and does not own shell startup files or a global manual database. Stack CLI 0.5.1 includes the generators; use `stack completions` and `stack manpage` to write the desired user-owned files as documented in the [completion guide](./completions.md). -The Aqua v0.5.0 pin was activated after the immutable release assets were published. CI verifies all four target mappings without executing foreign binaries, then installs the native archive and compares all completion and manual generator bytes with the matching checksum-locked canonical release archive. This keeps the published release check independent from later source-tree command additions. The release manifest remains the publication-time record; the pinned registry commit, generated checksum lock, contract, and CI runs are the later verification evidence. No release asset is replaced. +The Aqua v0.5.1 pin was activated after the immutable release assets were published. CI verifies all four target mappings without executing foreign binaries, then installs the native archive and compares all completion and manual generator bytes with the matching checksum-locked canonical release archive. This keeps the published release check independent from later source-tree command additions. The release manifest remains the publication-time record; the pinned registry commit, generated checksum lock, contract, and CI runs are the later verification evidence. No release asset is replaced. ## Direct installation -Download [Stack CLI 0.5.0](https://github.com/stack-sh/cli/releases/tag/v0.5.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: +Download [Stack CLI 0.5.1](https://github.com/stack-sh/cli/releases/tag/v0.5.1), 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: ```sh -tar -xzf "stack-v0.5.0-{target}.tar.gz" +tar -xzf "stack-v0.5.1-{target}.tar.gz" mkdir -p "$HOME/.local/bin" -install -m 0755 "stack-v0.5.0-{target}/stack" "$HOME/.local/bin/stack" +install -m 0755 "stack-v0.5.1-{target}/stack" "$HOME/.local/bin/stack" "$HOME/.local/bin/stack" --version ``` Add `$HOME/.local/bin` to `PATH` if it is not already present. Repeat the verified manual installation to update a directly downloaded binary; never overwrite a package-manager-owned binary. No receipt is created or required. See the [upgrade guide](./self-update.md). -The 0.5.0 archive carries completion and manual assets. Either copy its verified `share/` files into the matching system prefix or use the installed binary to generate user-owned files following the [completion guide](./completions.md). Do not copy these files from a different Stack version; CI and release verification require them to match the binary's command definition. +The 0.5.1 archive carries completion and manual assets. Either copy its verified `share/` files into the matching system prefix or use the installed binary to generate user-owned files following the [completion guide](./completions.md). Do not copy these files from a different Stack version; CI and release verification require them to match the binary's command definition. ## Channel ownership @@ -157,12 +174,12 @@ The 0.5.0 archive carries completion and manual assets. Either copy its verified | --- | --- | --- | | GitHub Releases | Canonical immutable archives with generated completions and manual, manifest, checksums, signature bundle, SBOMs, and provenance | Replace a tag or asset after publication | | Homebrew | Formula metadata, archive URL/digest mapping, standard completion/manual placement, install, upgrade, and uninstall | Rebuild a different binary or delegate upgrades to `stack` | -| Cargo | A future unambiguous crates.io source package, its registry dependency graph, and installation of the `stack` binary | Claim binary-archive identity, promise the local `stack-cli` package name on crates.io, or publish while dependencies remain Git-only | +| Cargo | The `stack-diagram-cli` crates.io source package, registry-only locked dependencies, and installation of the `stack` binary | Claim binary-archive identity, claim the unrelated `stack-cli` package, or publish while dependencies remain Git-only | | Aqua | Registry metadata and version pinning mapped to canonical archives and digests | Repack an archive or select prereleases by default | Stack does not provide a self-updater or a receipt-writing installer. Update through the tool that installed the binary, or verify and manually install a new GitHub archive for a direct download. Existing receipts are neither read nor deleted. -The workspace currently uses `stack-cli` as its local Cargo package name, but that name is already occupied by an unrelated crates.io package. No public Cargo install command is supported yet. The Cargo channel must select and verify an unambiguous registry package name, while keeping the installed binary name `stack`, before changing its state to available. +The source and published Cargo package names are both `stack-diagram-cli`; the installed binary remains `stack`. Registry ownership and supported-target installation are verified before activating this channel. ## Release activation and rollback diff --git a/docs/publication-audit.md b/docs/publication-audit.md index d6f8f02..872b8e7 100644 --- a/docs/publication-audit.md +++ b/docs/publication-audit.md @@ -2,7 +2,7 @@ Audit date: 2026-09-04 -This audit covers making the source repository public. It does not authorize or publish a binary release. +This historical audit describes the repository state on the date above, before the first binary and Cargo releases. Its statements about release availability and package publication are not current status. See the [distribution contract](./distribution.md) for live availability. This audit only covered making the source repository public; it did not authorize or publish a binary release. ## History and private data diff --git a/docs/self-update.md b/docs/self-update.md index c1f9c7d..8a38eea 100644 --- a/docs/self-update.md +++ b/docs/self-update.md @@ -12,7 +12,7 @@ In the project containing your Aqua configuration, run `aqua update`, review the ## Cargo -The crates.io channel is not available yet. Do not install the unrelated `stack-cli` crate. When the channel is released, Cargo will own updates and the supported command will be documented in the [distribution guide](./distribution.md). +Cargo owns updates for the published `stack-diagram-cli` package. Run `cargo install stack-diagram-cli --locked` for the latest release, or add `--version 0.5.1` to select that exact version. The binary remains `stack`; the unrelated `stack-cli` crate is not this project. See the [Cargo installation guide](./distribution.md#cargo-installation). ## Direct GitHub download diff --git a/scripts/cargo-publish-context.mjs b/scripts/cargo-publish-context.mjs new file mode 100644 index 0000000..9bebbfb --- /dev/null +++ b/scripts/cargo-publish-context.mjs @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +const packages = ["stack-diagram-cli"]; + +export function validatePublish(metadata, runs, context) { + assert.match(context.expectedSha, /^[a-f0-9]{40}$/); + assert.match(context.version, /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/); + assert.ok(packages.includes(context.packageName), 'Unexpected crate'); + assert.ok(['true', 'false'].includes(context.publish), 'Explicit publish mode required'); + const matches = metadata.packages.filter(crate => crate.name === context.packageName); + assert.equal(matches.length, 1); + const crate = matches[0]; + assert.equal(crate.version, context.version); + assert.equal(crate.license, 'Apache-2.0'); + assert.equal(crate.rust_version, '1.85'); + assert.deepEqual(crate.publish, ['crates-io']); + for (const dependency of crate.dependencies) { + assert.ok(dependency.source === 'registry+https://github.com/rust-lang/crates.io-index' || (dependency.source === null && dependency.path && /^=[0-9]+\.[0-9]+\.[0-9]+$/.test(dependency.req)), 'Dependencies must resolve from crates.io when packaged'); + } + assert.equal(runs.length, 1, 'Exact main source needs successful CI'); + assert.equal(runs[0].headSha, context.expectedSha); + assert.equal(runs[0].status, 'completed'); + assert.equal(runs[0].conclusion, 'success'); +} + +export function validateRegistry(crateStatus, versionStatus, publish) { + assert.equal(crateStatus, 200, 'Only existing crates may use trusted publishing'); + assert.ok([200, 404].includes(versionStatus), 'Registry version lookup failed'); + if (publish === 'true') assert.equal(versionStatus, 404, 'Published versions are immutable'); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const context = { packageName: process.env.PACKAGE_NAME, version: process.env.EXPECTED_VERSION, expectedSha: process.env.EXPECTED_SHA, publish: process.env.PUBLISH }; + const metadata = JSON.parse(await readFile(process.argv[2], 'utf8')); + const runs = JSON.parse(await readFile(process.argv[3], 'utf8')); + validatePublish(metadata, runs, context); + const base = 'https://crates.io/api/v1/crates/' + context.packageName; + const status = async url => (await fetch(url, { headers: { 'User-Agent': 'stack-sh/cli publication (https://github.com/stack-sh/cli)' }, signal: AbortSignal.timeout(30000) })).status; + validateRegistry(await status(base), await status(base + '/' + context.version), context.publish); + console.log('Exact source, package, main CI, and registry state verified.'); +} diff --git a/scripts/cargo-publish-context.test.mjs b/scripts/cargo-publish-context.test.mjs new file mode 100644 index 0000000..f49e6b7 --- /dev/null +++ b/scripts/cargo-publish-context.test.mjs @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import test from 'node:test'; +import { validatePublish, validateRegistry } from './cargo-publish-context.mjs'; + +const sha = 'a'.repeat(40); +const context = { packageName: 'stack-diagram-cli', version: '0.1.0', expectedSha: sha, publish: 'false' }; +const crate = { name: context.packageName, version: context.version, publish: ['crates-io'], license: 'Apache-2.0', rust_version: '1.85', dependencies: [] }; +const metadata = { packages: [crate] }; +const runs = [{ headSha: sha, status: 'completed', conclusion: 'success' }]; + +test('accepts exact successful source for verification or publication', () => { + for (const publish of ['true', 'false']) validatePublish(metadata, runs, { ...context, publish }); + for (const name of ["stack-diagram-cli"]) validatePublish({ packages: [{ ...crate, name }] }, runs, { ...context, packageName: name }); +}); +test('rejects malformed identity, versions, or implicit publishing', () => { + for (const change of [{ expectedSha: 'main' }, { expectedSha: sha + '\n' }, { version: '0.1.0-rc.1' }, { version: '0x1x0' }, { version: '01.0.0' }, { packageName: 'other' }, { publish: '' }]) assert.throws(() => validatePublish(metadata, runs, { ...context, ...change })); + for (const change of [{ version: '0.2.0' }, { license: 'MIT' }, { rust_version: '1.86' }, { publish: null }, { dependencies: [{ source: 'git+https://example.com/source' }] }, { dependencies: [{ source: null, path: '../library', req: '*' }] }]) assert.throws(() => validatePublish({ packages: [{ ...crate, ...change }] }, runs, context)); +}); +test('rejects missing, stale, incomplete, and unsuccessful CI', () => { + for (const invalid of [[], [...runs, ...runs], [{ ...runs[0], headSha: 'b'.repeat(40) }], [{ ...runs[0], status: 'in_progress' }], [{ ...runs[0], conclusion: 'failure' }]]) assert.throws(() => validatePublish(metadata, invalid, context)); +}); +test('never republishes an existing version or ignores registry failures', () => { + validateRegistry(200, 200, 'false'); + validateRegistry(200, 404, 'false'); + validateRegistry(200, 404, 'true'); + assert.throws(() => validateRegistry(200, 200, 'true')); + for (const code of [401, 403, 429, 500]) assert.throws(() => validateRegistry(200, code, 'false')); + assert.throws(() => validateRegistry(404, 404, 'true')); +}); +test('workflow keeps manual main-only publishing and ephemeral credentials', () => { + const workflow = fs.readFileSync(new URL('../.github/workflows/cargo-publish.yaml', import.meta.url), 'utf8'); + assert.match(workflow, /workflow_dispatch:/); + assert.doesNotMatch(workflow, /\n (push|pull_request|schedule):|secrets\.|cargo login|self-hosted/); + assert.ok(workflow.includes("if: github.ref == 'refs/heads/main'")); + assert.ok(workflow.includes('if: inputs.publish == true')); + assert.ok(workflow.includes('default: false')); + assert.match(workflow, /id-token: write/); + assert.match(workflow, /crates-io-auth-action@[a-f0-9]{40}/); +}); diff --git a/scripts/distribution-contract.test.mjs b/scripts/distribution-contract.test.mjs index 6c7224d..4c77ed0 100644 --- a/scripts/distribution-contract.test.mjs +++ b/scripts/distribution-contract.test.mjs @@ -58,7 +58,7 @@ test("an unverified crates.io package name is rejected", () => { const candidate = changed((value) => { value.product.publishedCargoPackage = "stack-cli"; }); - assert.throws(() => validateDistributionContract(candidate, cargoToml), /must remain unset/); + assert.throws(() => validateDistributionContract(candidate, cargoToml), /must be stack-diagram-cli/); }); test("an incomplete archive contract is rejected", () => { @@ -94,11 +94,11 @@ test("the activated Aqua channel cannot regress to planned", () => { assert.throws(() => validateDistributionContract(candidate, cargoToml), /aqua state must be available/); }); -test("an unactivated package-manager channel cannot become available", () => { +test("the activated Cargo channel cannot regress to planned", () => { const candidate = changed((value) => { - value.channels.find(({ id }) => id === "cargo").state = "available"; + value.channels.find(({ id }) => id === "cargo").state = "planned"; }); - assert.throws(() => validateDistributionContract(candidate, cargoToml), /cargo state must be planned/); + assert.throws(() => validateDistributionContract(candidate, cargoToml), /cargo state must be available/); }); test("removed self-update cannot be reintroduced", () => { diff --git a/scripts/validate-distribution-contract.mjs b/scripts/validate-distribution-contract.mjs index 1546eee..31e7c8f 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"]; -const availableChannels = new Set(["aqua", "github-release", "homebrew"]); +const availableChannels = new Set(["aqua", "cargo", "github-release", "homebrew"]); const requiredArchiveEntries = [ "LICENSE", "NOTICE", @@ -67,7 +67,7 @@ export function validateDistributionContract(contract, cargoToml) { contract.product.sourceCargoPackage === cargoValue(cargoToml, "name"), "sourceCargoPackage must match Cargo.toml", ); - invariant(contract.product?.publishedCargoPackage === null, "published Cargo package must remain unset before registry ownership is verified"); + invariant(contract.product?.publishedCargoPackage === "stack-diagram-cli", "published Cargo package must be stack-diagram-cli"); invariant(contract.availability?.state === "available", "distribution must be available after the verified stable release"); invariant( contract.availability?.message?.includes(`Stack CLI ${cargoVersion}`) && diff --git a/skills/docs-source.json b/skills/docs-source.json index 869499a..9da4786 100644 --- a/skills/docs-source.json +++ b/skills/docs-source.json @@ -1,5 +1,5 @@ { "repository": "stack-sh/docs", - "revision": "d1c4db38fa858900f22a95fb9ff702ef9a8a0f3b", - "manifestSha256": "837070789c1e6474e3a92ecc39920830024a58d6999308ddd72cc5e2bacddc6f" + "revision": "5eed6efed732323008a65274919e107aac8512c0", + "manifestSha256": "a4c33a2ba7bf34af49f6bd80844b009ffba2a2652c658778d2fb423c0a313097" } diff --git a/skills/stack-diagrams/SKILL.md b/skills/stack-diagrams/SKILL.md index 95e19ae..1677899 100644 --- a/skills/stack-diagrams/SKILL.md +++ b/skills/stack-diagrams/SKILL.md @@ -12,9 +12,9 @@ Deliver editable `.stack` source and, when rendering is available, an SVG. Prese ## Start with the available tools -Run `stack --version` and `stack help` before selecting commands. Use the installed binary's help, not unreleased repository instructions. This workflow is tested with published CLI 0.5.0. The commands below use human-readable output; `check`, `fmt`, and `render` also accept `--json` for structured diagnostics, artifacts, and operational errors. Read the [versioned JSON output contract](https://github.com/stack-sh/cli/blob/v0.5.0/docs/machine-readable-output.md) before parsing it, and verify options using `stack help `. +Run `stack --version` and `stack help` before selecting commands. Use the installed binary's help, not unreleased repository instructions. This workflow is tested with published CLI 0.5.1. The commands below use human-readable output; `check`, `fmt`, and `render` also accept `--json` for structured diagnostics, artifacts, and operational errors. Read the [versioned JSON output contract](https://github.com/stack-sh/cli/blob/v0.5.1/docs/machine-readable-output.md) before parsing it, and verify options using `stack help `. -If the CLI is missing, consult the [installation guide](https://stack-diagram.com/docs/guide/getting-started.md) and [release distribution contract](https://github.com/stack-sh/cli/blob/v0.5.0/docs/distribution.md). Install only when the user's authorization allows it. Otherwise provide source with an explicit validation gap; do not report it as checked. No remote source upload is required. Upgrade with the package manager that installed Stack, or verify and manually replace a direct download; `stack update` has been removed. For local configuration problems, `stack doctor` and `stack config path` provide read-only diagnosis. +If the CLI is missing, consult the [installation guide](https://stack-diagram.com/docs/guide/getting-started.md) and [release distribution contract](https://github.com/stack-sh/cli/blob/v0.5.1/docs/distribution.md). Install only when the user's authorization allows it. Otherwise provide source with an explicit validation gap; do not report it as checked. No remote source upload is required. Upgrade with the package manager that installed Stack, or verify and manually replace a direct download; `stack update` has been removed. For local configuration problems, `stack doctor` and `stack config path` provide read-only diagnosis. ## Read only the relevant reference diff --git a/tests/aqua/aqua-checksums.json b/tests/aqua/aqua-checksums.json index a42a78e..e251500 100644 --- a/tests/aqua/aqua-checksums.json +++ b/tests/aqua/aqua-checksums.json @@ -1,23 +1,23 @@ { "checksums": [ { - "id": "github_release/github.com/stack-sh/cli/v0.5.0/stack-v0.5.0-aarch64-apple-darwin.tar.gz", - "checksum": "CFA5E6459481DEC73C0ACA5B32D52293C977A4F5D72273F8EA9CE71C4F689EA2", + "id": "github_release/github.com/stack-sh/cli/v0.5.1/stack-v0.5.1-aarch64-apple-darwin.tar.gz", + "checksum": "00572032C4DCF54602E41C23FFEB2FA189709D16A8939FE3549DE335F2DE811E", "algorithm": "sha256" }, { - "id": "github_release/github.com/stack-sh/cli/v0.5.0/stack-v0.5.0-aarch64-unknown-linux-gnu.tar.gz", - "checksum": "506A03D1B430497539BFC2C57FF4A97962A983D1343C983700BC85719E5740CB", + "id": "github_release/github.com/stack-sh/cli/v0.5.1/stack-v0.5.1-aarch64-unknown-linux-gnu.tar.gz", + "checksum": "4FA61751A4BDD7D066255751549459B6303CFCED9243F78A2BAA41844719C0CF", "algorithm": "sha256" }, { - "id": "github_release/github.com/stack-sh/cli/v0.5.0/stack-v0.5.0-x86_64-apple-darwin.tar.gz", - "checksum": "D38E017C93A41855319FD583C4F0D6E62DC688B10EBEEDBE925D07BA6DBB7E2B", + "id": "github_release/github.com/stack-sh/cli/v0.5.1/stack-v0.5.1-x86_64-apple-darwin.tar.gz", + "checksum": "D60F57A7A8F4FD1280FDD3B7C0871E9DBB53DF23F4A5A168D843E469C9A05984", "algorithm": "sha256" }, { - "id": "github_release/github.com/stack-sh/cli/v0.5.0/stack-v0.5.0-x86_64-unknown-linux-gnu.tar.gz", - "checksum": "B159E58C899F77798196616DD1A33FC6A04026CDC582861EE8AC257211ABDB9D", + "id": "github_release/github.com/stack-sh/cli/v0.5.1/stack-v0.5.1-x86_64-unknown-linux-gnu.tar.gz", + "checksum": "A551F526501AC051457AC354F67D56A14817D59B7DB37DA103DF30DA4E7C96A9", "algorithm": "sha256" }, { diff --git a/tests/aqua/aqua.yaml b/tests/aqua/aqua.yaml index dd58cfd..eae0fc4 100644 --- a/tests/aqua/aqua.yaml +++ b/tests/aqua/aqua.yaml @@ -13,5 +13,5 @@ registries: ref: 42702cda91a4156901b9a601bd143c43dcf05766 path: aqua/registry.yaml packages: - - name: stack-sh/cli@v0.5.0 + - name: stack-sh/cli@v0.5.1 registry: stack-sh