From bcc91ce45647e78d184fce2e6bb03adaaf1f6e1e Mon Sep 17 00:00:00 2001 From: konojunya Date: Sun, 6 Sep 2026 05:01:46 +0900 Subject: [PATCH 1/2] Prepare Stack CLI 0.5.1 for registry-only Cargo installation --- .github/workflows/ci.yaml | 6 + .github/workflows/initial-publish.yaml | 123 ++++++ Cargo.lock | 26 +- Cargo.toml | 20 +- README.md | 5 +- THIRD_PARTY_LICENSES.md | 6 +- .../distribution-contract-v3.schema.json | 404 ++++++++++++++++++ distribution/distribution-contract.json | 10 +- distribution/generated/share/man/man1/stack.1 | 2 +- docs/cargo-releasing.md | 11 + docs/distribution.md | 16 +- docs/releases/v0.5.1.md | 11 + scripts/distribution-contract.test.mjs | 4 +- scripts/initial-publish-context.mjs | 31 ++ scripts/initial-publish-context.test.mjs | 37 ++ scripts/release-security.test.mjs | 4 +- scripts/resolve-release-context.test.mjs | 26 +- scripts/test_package_release.py | 2 +- scripts/validate-distribution-contract.mjs | 4 +- 19 files changed, 693 insertions(+), 55 deletions(-) create mode 100644 .github/workflows/initial-publish.yaml create mode 100644 distribution/distribution-contract-v3.schema.json create mode 100644 docs/cargo-releasing.md create mode 100644 docs/releases/v0.5.1.md create mode 100644 scripts/initial-publish-context.mjs create mode 100644 scripts/initial-publish-context.test.mjs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c028494..23b34d5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,6 +15,9 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v7 + - name: Validate initial Cargo publication guards + run: node --test scripts/initial-publish-context.test.mjs + - name: Read supported specification revision id: specification shell: bash @@ -140,6 +143,9 @@ jobs: COMP_CWORD=3 _stack_completion test "${COMPREPLY[*]}" = default_icons_path + - name: Verify crates.io source package + run: cargo +stable publish --dry-run -p stack-diagram-cli --locked + - name: Run canonical formatter suite env: STACK_SPECIFICATION_DIR: ${{ github.workspace }}/.stack-specification diff --git a/.github/workflows/initial-publish.yaml b/.github/workflows/initial-publish.yaml new file mode 100644 index 0000000..57ba5b6 --- /dev/null +++ b/.github/workflows/initial-publish.yaml @@ -0,0 +1,123 @@ +name: Initial crates.io publish + +on: + workflow_dispatch: + inputs: + expected_sha: + description: Exact main commit whose CI has succeeded + required: true + type: string + +permissions: + contents: read + actions: read + +concurrency: + group: initial-crates-io-publish + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + publish: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + env: + EXPECTED_SHA: ${{ inputs.expected_sha }} + 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" + + - name: Check out the exact dispatch commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Install the minimum supported Rust toolchain + run: rustup toolchain install 1.85.0 --profile minimal + + - name: Verify package identity and successful main CI + env: + GH_TOKEN: ${{ github.token }} + run: | + test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + 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/initial-publish-context.mjs "$RUNNER_TEMP/package.json" "$RUNNER_TEMP/ci.json" + + - name: Require an unpublished crate name + run: | + code=$(curl --silent --show-error --max-time 30 --user-agent 'stack-sh/cli initial publication (https://github.com/stack-sh/cli)' --output "$RUNNER_TEMP/crate-state.json" --write-out '%{http_code}' https://crates.io/api/v1/crates/stack-diagram-cli) + test "$code" = 404 + + - name: Verify the exact source package without credentials + run: cargo +1.85.0 publish --package stack-diagram-cli --registry crates-io --locked --dry-run + + - name: Publish the initial crate + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_INITIAL_PUBLISH_TOKEN }} + run: | + test -n "$CARGO_REGISTRY_TOKEN" + cargo +1.85.0 publish --package stack-diagram-cli --registry crates-io --locked + + verify-install: + needs: publish + name: Cargo install (${{ matrix.target }}, Rust ${{ matrix.rust }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 25 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + rust: ["1.85.0", stable] + target: [aarch64-apple-darwin, x86_64-apple-darwin, aarch64-unknown-linux-gnu, x86_64-unknown-linux-gnu] + include: + - target: aarch64-apple-darwin + runner: macos-15 + - target: x86_64-apple-darwin + runner: macos-15-intel + - target: aarch64-unknown-linux-gnu + runner: ubuntu-24.04-arm + - target: x86_64-unknown-linux-gnu + runner: ubuntu-24.04 + steps: + - name: Check out smoke tests at the published source commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.sha }} + persist-credentials: false + - name: Install the selected Rust toolchain + env: + RUST_VERSION: ${{ matrix.rust }} + TARGET: ${{ matrix.target }} + run: | + rustup toolchain install "$RUST_VERSION" --profile minimal + rustc "+$RUST_VERSION" -vV | grep -F "host: $TARGET" + - name: Install only from crates.io into an isolated directory + env: + CARGO_HOME: ${{ runner.temp }}/cargo-registry-home + CARGO_TARGET_DIR: ${{ runner.temp }}/cargo-registry-target + RUST_VERSION: ${{ matrix.rust }} + run: cargo "+$RUST_VERSION" install stack-diagram-cli --version 0.5.1 --locked --registry crates-io --root "$RUNNER_TEMP/cargo-install" + - name: Verify commands, generated assets, and machine-readable output + env: + STACK_BINARY: ${{ runner.temp }}/cargo-install/bin/stack + TARGET: ${{ matrix.target }} + run: | + python3 - <<'PY' + import os + from pathlib import Path + from scripts.verify_release_binary import verify_architecture, verify_commands + binary = Path(os.environ["STACK_BINARY"]) + verify_architecture(binary, os.environ["TARGET"]) + verify_commands(binary, "0.5.1") + print("Verified registry-only Cargo installation and CLI commands") + PY diff --git a/Cargo.lock b/Cargo.lock index c80ea2c..981b7b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -368,8 +368,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] -name = "stack-cli" -version = "0.5.0" +name = "stack-compiler" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdba45c0f7a85ef0ec0bfb766b387dbd90fc6141237ac74b559d08a424750bc6" + +[[package]] +name = "stack-diagram-cli" +version = "0.5.1" dependencies = [ "roxmltree", "serde", @@ -383,15 +389,11 @@ dependencies = [ "zip", ] -[[package]] -name = "stack-compiler" -version = "0.1.0" -source = "git+https://github.com/stack-sh/compiler.git?rev=84ab5663a7f7c5b7dc0b5e9e2f04c8894ed02820#84ab5663a7f7c5b7dc0b5e9e2f04c8894ed02820" - [[package]] name = "stack-engine" -version = "0.6.0" -source = "git+https://github.com/stack-sh/engine.git?rev=9af727aea79233b8389e0ed6fdbae7d3f388dc29#9af727aea79233b8389e0ed6fdbae7d3f388dc29" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3177ed242771e530b8d245553a93a620b44f417a2f9968923edd45ee16f15c" dependencies = [ "roxmltree", "serde_json", @@ -404,7 +406,8 @@ dependencies = [ [[package]] name = "stack-formatter" version = "0.1.0" -source = "git+https://github.com/stack-sh/engine.git?rev=9af727aea79233b8389e0ed6fdbae7d3f388dc29#9af727aea79233b8389e0ed6fdbae7d3f388dc29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6dda1964dc7347321e4c48d55303ef6b0a657e24a6294230f673778ed76520" dependencies = [ "stack-compiler", ] @@ -412,7 +415,8 @@ dependencies = [ [[package]] name = "stack-theme" version = "0.5.0" -source = "git+https://github.com/stack-sh/theme.git?rev=7e208d6a3c90d255799f390a4e8b86248c73caee#7e208d6a3c90d255799f390a4e8b86248c73caee" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98e0418c819d61897beded19d5333b4f6c7a8dcf1be46d0a8c62409ff2e6db7" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index ce77d02..521644c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,22 @@ [package] -name = "stack-cli" -version = "0.5.0" +name = "stack-diagram-cli" +version = "0.5.1" edition = "2024" rust-version = "1.85" -publish = false +publish = ["crates-io"] +homepage = "https://stack-diagram.com/" +documentation = "https://stack-diagram.com/docs/guide/getting-started" +keywords = ["stack", "diagram", "cli", "svg"] +categories = ["command-line-utilities", "visualization"] +include = ["src/**", "templates/**", "catalogs/**", "tests/fixtures/**", "LICENSE", "NOTICE", "THIRD_PARTY_LICENSES.md", "README.md", "Cargo.lock"] description = "Native command-line interface for Stack diagrams" repository = "https://github.com/stack-sh/cli" license = "Apache-2.0" readme = "README.md" +[lib] +name = "stack_cli" + [[bin]] name = "stack" path = "src/main.rs" @@ -19,9 +27,9 @@ serde = { version = "=1.0.229", features = ["derive"] } serde_json = "=1.0.151" serde_yaml_ng = "=0.10.0" sha2 = "=0.11.0" -stack-compiler = { git = "https://github.com/stack-sh/compiler.git", rev = "84ab5663a7f7c5b7dc0b5e9e2f04c8894ed02820" } -stack-engine = { git = "https://github.com/stack-sh/engine.git", rev = "9af727aea79233b8389e0ed6fdbae7d3f388dc29" } -stack-theme = { git = "https://github.com/stack-sh/theme.git", rev = "7e208d6a3c90d255799f390a4e8b86248c73caee" } +stack-compiler = "=0.1.0" +stack-engine = "=0.7.0" +stack-theme = "=0.5.0" ureq = { version = "=3.4.0", default-features = false, features = ["rustls"] } zip = { version = "=6.0.0", default-features = false, features = ["deflate-flate2-zlib-rs"] } diff --git a/README.md b/README.md index 5867829..271278a 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ stack fmt - stack render arch.stack stack render arch.stack -o arch.svg stack render arch.stack --json -stack update --check stack lsp stack doctor stack doctor --provider-pack .stack-icons @@ -146,3 +145,7 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a change. Please report ## Licensing Repository-authored work is licensed under the [Apache License 2.0](./LICENSE) for personal and commercial use. Runtime and build dependency licenses are recorded in [THIRD_PARTY_LICENSES.md](./THIRD_PARTY_LICENSES.md). Every published binary archive ships the applicable license and notice files described there. + +## Cargo source package + +The source package is named `stack-diagram-cli`; its binary remains `stack`. All Rust dependencies resolve from crates.io. Consult the [current installation guide](https://stack-diagram.com/docs/guide/getting-started) for verified releases and installation commands. A source change does not activate a distribution channel or replace existing release archives. Maintainers follow the [Cargo publication procedure](https://github.com/stack-sh/cli/blob/main/docs/cargo-releasing.md). diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md index 0463af8..3443c1a 100644 --- a/THIRD_PARTY_LICENSES.md +++ b/THIRD_PARTY_LICENSES.md @@ -6,9 +6,9 @@ Audit date: 2026-09-05 | Component | Version or revision | License | Source | Distribution note | | --- | --- | --- | --- | --- | -| `stack-engine` / `stack-formatter` | `9af727aea79233b8389e0ed6fdbae7d3f388dc29` | Apache-2.0 | | Linked into the native binary; validates and renders caller-owned provider packs without bundling vendor assets. | -| `stack-compiler` | `84ab5663a7f7c5b7dc0b5e9e2f04c8894ed02820` | Apache-2.0 | | Linked directly for protocol-neutral language intelligence and transitively through `stack-engine`; performs no runtime I/O. | -| `stack-theme` | `7e208d6a3c90d255799f390a4e8b86248c73caee` | Apache-2.0 | | Linked directly and through `stack-engine`; its 30 fallback and 12 explicit core SVGs are Stack-authored Apache-2.0 assets. It also provides the asset-free provider-pack contract and types. | +| `stack-engine` / `stack-formatter` | `0.7.0` / `0.1.0` (crates.io) | Apache-2.0 | | Linked into the native binary; validates and renders caller-owned provider packs without bundling vendor assets. | +| `stack-compiler` | `0.1.0 (crates.io)` | Apache-2.0 | | Linked directly for protocol-neutral language intelligence and transitively through `stack-engine`; performs no runtime I/O. | +| `stack-theme` | `0.5.0 (crates.io)` | Apache-2.0 | | Linked directly and through `stack-engine`; its 30 fallback and 12 explicit core SVGs are Stack-authored Apache-2.0 assets. It also provides the asset-free provider-pack contract and types. | | `roxmltree` | `0.21.1` | MIT OR Apache-2.0 | | Parses untrusted local SVG into a read-only tree before allowlisted serialization. | | `sha2`, `digest`, `block-buffer`, `crypto-common`, `hybrid-array`, `const-oid`, `typenum` | `0.11.0`, `0.11.3`, `0.12.1`, `0.2.2`, `0.4.14`, `0.10.2`, `1.20.1` | MIT OR Apache-2.0 | | Computes complete archive and per-asset SHA-256 identities. | | `zip` | `6.0.0` | MIT | | Reads audited, allowlisted entries from verified official ZIP archives. | diff --git a/distribution/distribution-contract-v3.schema.json b/distribution/distribution-contract-v3.schema.json new file mode 100644 index 0000000..0a56412 --- /dev/null +++ b/distribution/distribution-contract-v3.schema.json @@ -0,0 +1,404 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/stack-sh/cli/main/distribution/distribution-contract-v3.schema.json", + "title": "Stack CLI distribution contract", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "product", + "availability", + "versioning", + "artifacts", + "targets", + "unsupported", + "channels", + "verification" + ], + "properties": { + "$schema": { + "type": "string" + }, + "schemaVersion": { + "const": 3 + }, + "product": { + "type": "object", + "additionalProperties": false, + "required": [ + "binary", + "sourceCargoPackage", + "publishedCargoPackage", + "sourceVersionFile", + "currentSourceVersion", + "minimumRustVersion", + "currentReleaseVersion" + ], + "properties": { + "binary": { + "const": "stack" + }, + "sourceCargoPackage": { + "const": "stack-diagram-cli" + }, + "publishedCargoPackage": { + "enum": [null, "stack-diagram-cli"] + }, + "sourceVersionFile": { + "const": "Cargo.toml" + }, + "currentSourceVersion": { + "$ref": "#/$defs/version" + }, + "minimumRustVersion": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+$" + }, + "currentReleaseVersion": { + "$ref": "#/$defs/version" + } + } + }, + "availability": { + "type": "object", + "additionalProperties": false, + "required": [ + "state", + "message" + ], + "properties": { + "state": { + "enum": [ + "planned", + "available" + ] + }, + "message": { + "type": "string", + "minLength": 1 + } + } + }, + "versioning": { + "type": "object", + "additionalProperties": false, + "required": [ + "scheme", + "tagTemplate", + "stableVersionRequirement", + "prereleaseVersionRequirement", + "prereleasePolicy", + "minimumSupportedVersionSource", + "preOneSupportWindow", + "stableSupportWindow" + ], + "properties": { + "scheme": { + "const": "Semantic Versioning" + }, + "tagTemplate": { + "const": "v{version}" + }, + "stableVersionRequirement": { + "type": "string", + "minLength": 1 + }, + "prereleaseVersionRequirement": { + "type": "string", + "minLength": 1 + }, + "prereleasePolicy": { + "type": "string", + "minLength": 1 + }, + "minimumSupportedVersionSource": { + "type": "string", + "minLength": 1 + }, + "preOneSupportWindow": { + "type": "string", + "minLength": 1 + }, + "stableSupportWindow": { + "type": "string", + "minLength": 1 + } + } + }, + "artifacts": { + "type": "object", + "additionalProperties": false, + "required": [ + "archiveNameTemplate", + "archiveRootTemplate", + "requiredEntries", + "completionPaths", + "manpagePath", + "releaseManifestNameTemplate", + "checksumNameTemplate", + "signatureBundleNameTemplate", + "sbomNameTemplate", + "provenanceNameTemplate", + "sbomAttestationNameTemplate", + "checksumAlgorithm", + "reproducibility" + ], + "properties": { + "archiveNameTemplate": { + "$ref": "#/$defs/template" + }, + "archiveRootTemplate": { + "$ref": "#/$defs/template" + }, + "requiredEntries": { + "type": "array", + "minItems": 8, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "completionPaths": { + "type": "object", + "additionalProperties": false, + "required": [ + "bash", + "zsh", + "fish" + ], + "properties": { + "bash": { + "const": "share/bash-completion/completions/stack" + }, + "zsh": { + "const": "share/zsh/site-functions/_stack" + }, + "fish": { + "const": "share/fish/vendor_completions.d/stack.fish" + } + } + }, + "manpagePath": { + "const": "share/man/man1/stack.1" + }, + "releaseManifestNameTemplate": { + "$ref": "#/$defs/template" + }, + "checksumNameTemplate": { + "$ref": "#/$defs/template" + }, + "signatureBundleNameTemplate": { + "$ref": "#/$defs/template" + }, + "sbomNameTemplate": { + "$ref": "#/$defs/template" + }, + "provenanceNameTemplate": { + "$ref": "#/$defs/template" + }, + "sbomAttestationNameTemplate": { + "$ref": "#/$defs/template" + }, + "checksumAlgorithm": { + "const": "sha256" + }, + "reproducibility": { + "type": "object", + "additionalProperties": false, + "required": [ + "archiveOrder", + "uid", + "gid", + "mtime", + "gzipHeader" + ], + "properties": { + "archiveOrder": { + "type": "string", + "minLength": 1 + }, + "uid": { + "const": 0 + }, + "gid": { + "const": 0 + }, + "mtime": { + "const": "SOURCE_DATE_EPOCH" + }, + "gzipHeader": { + "type": "string", + "minLength": 1 + } + } + } + } + }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/target" + } + }, + "unsupported": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "platform", + "reason" + ], + "properties": { + "platform": { + "type": "string", + "minLength": 1 + }, + "reason": { + "type": "string", + "minLength": 1 + } + } + } + }, + "channels": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/channel" + } + }, + "verification": { + "type": "object", + "additionalProperties": false, + "required": [ + "releaseActivation", + "rollback" + ], + "properties": { + "releaseActivation": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "rollback": { + "type": "string", + "minLength": 1 + } + } + } + }, + "$defs": { + "version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-rc\\.[1-9][0-9]*)?$" + }, + "template": { + "type": "string", + "pattern": "\\{version\\}" + }, + "target": { + "type": "object", + "additionalProperties": false, + "required": [ + "target", + "os", + "architecture", + "libc", + "minimumRuntime", + "supportTier", + "state" + ], + "properties": { + "target": { + "type": "string", + "minLength": 1 + }, + "os": { + "enum": [ + "macos", + "linux" + ] + }, + "architecture": { + "enum": [ + "arm64", + "x86_64" + ] + }, + "libc": { + "enum": [ + "system", + "glibc" + ] + }, + "minimumRuntime": { + "type": "string", + "minLength": 1 + }, + "supportTier": { + "const": "tier-1" + }, + "state": { + "enum": [ + "planned", + "available" + ] + } + } + }, + "channel": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "state", + "targets", + "owns", + "source", + "updatePolicy" + ], + "properties": { + "id": { + "enum": [ + "github-release", + "homebrew", + "cargo", + "aqua" + ] + }, + "state": { + "enum": [ + "planned", + "available" + ] + }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + }, + "owns": { + "type": "string", + "minLength": 1 + }, + "source": { + "type": "string", + "minLength": 1 + }, + "updatePolicy": { + "type": "string", + "minLength": 1 + } + } + } + } +} diff --git a/distribution/distribution-contract.json b/distribution/distribution-contract.json index 36585ab..24d8436 100644 --- a/distribution/distribution-contract.json +++ b/distribution/distribution-contract.json @@ -1,18 +1,18 @@ { - "$schema": "./distribution-contract-v2.schema.json", - "schemaVersion": 2, + "$schema": "./distribution-contract-v3.schema.json", + "schemaVersion": 3, "product": { "binary": "stack", - "sourceCargoPackage": "stack-cli", + "sourceCargoPackage": "stack-diagram-cli", "publishedCargoPackage": null, "sourceVersionFile": "Cargo.toml", - "currentSourceVersion": "0.5.0", + "currentSourceVersion": "0.5.1", "minimumRustVersion": "1.85", "currentReleaseVersion": "0.5.0" }, "availability": { "state": "available", - "message": "Stack CLI 0.5.0 is available through GitHub Releases, Homebrew, and Aqua. Cargo remains planned; self-update was removed in 0.5.0." + "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." }, "versioning": { "scheme": "Semantic Versioning", diff --git a/distribution/generated/share/man/man1/stack.1 b/distribution/generated/share/man/man1/stack.1 index cb916f3..9023761 100644 --- a/distribution/generated/share/man/man1/stack.1 +++ b/distribution/generated/share/man/man1/stack.1 @@ -1,4 +1,4 @@ -.TH STACK 1 "" "Stack CLI 0.5.0" "Stack CLI Manual" +.TH STACK 1 "" "Stack CLI 0.5.1" "Stack CLI Manual" .SH NAME stack \- Stack diagram toolchain .SH SYNOPSIS diff --git a/docs/cargo-releasing.md b/docs/cargo-releasing.md new file mode 100644 index 0000000..e9e4aa0 --- /dev/null +++ b/docs/cargo-releasing.md @@ -0,0 +1,11 @@ +# Initial crates.io publication + +The initial publication creates `stack-diagram-cli` version `0.5.1`. The workflow is deliberately limited to this bootstrap operation; it is not the recurring release mechanism. + +1. Merge the release preparation through a reviewed pull request and wait for both main CI jobs to succeed. +2. Create a short-lived crates.io token limited to `publish-new` and the exact crate name `stack-diagram-cli`. Store it only as the repository Actions secret `CARGO_INITIAL_PUBLISH_TOKEN`; never paste it into an issue, pull request, workflow input, or source file. +3. Dispatch `initial-publish.yaml` on `main`, with `expected_sha` equal to the full successful main commit. The workflow rejects a different ref, commit, package identity, initial version, or CI state. It requires the crate name to be absent and performs a credential-free packaging dry run before publishing. +4. Verify the registry version, checksum, and downloaded `.cargo_vcs_info.json`. After publishing, the workflow installs the exact registry package with an empty Cargo home on all four supported native targets, using both Rust 1.85.0 and stable, and checks commands, generated assets, and JSON output. If the upload or verification times out, inspect the registry before retrying: a failure after upload does not undo publication. Re-run only failed verification jobs, not the successful publish job. +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. diff --git a/docs/distribution.md b/docs/distribution.md index 985bed1..97028d3 100644 --- a/docs/distribution.md +++ b/docs/distribution.md @@ -1,6 +1,6 @@ # Distribution contract -This document defines the shared release contract for the Stack CLI. It is normative for GitHub Releases, Homebrew, Cargo, Aqua, implementations. The machine-readable source is [`distribution/distribution-contract.json`](../distribution/distribution-contract.json). +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). @@ -8,12 +8,12 @@ This document defines the shared release contract for the Stack CLI. It is norma 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 | 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 | +| 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 | 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. @@ -24,7 +24,7 @@ Windows, musl-based Linux distributions such as Alpine, BSD, and 32-bit architec - Cargo `package.version`, CLI output, the Git tag `v{version}`, release title, archive names, and release manifest version must agree exactly. - Stable versions use `MAJOR.MINOR.PATCH`. Release candidates use `MAJOR.MINOR.PATCH-rc.N`, are GitHub prereleases, and are never selected by default by package managers. - Before 1.0, only the latest stable release is supported. Starting at 1.0, the latest two minor lines are supported. -- Release-manifest schema v1 retains `minimumSupportedCliVersion` for compatibility and sets it to the release version. It does not enable self-update: new manifests never include that channel. Distribution contract v2 removes the updater channel, receipt requirement, and activation rules; the original v1 schema and receipt schema remain unchanged for historical consumers. +- Release-manifest schema v1 retains `minimumSupportedCliVersion` for compatibility and sets it to the release version. It does not enable self-update: new manifests never include that channel. Distribution contract v3 declares the Cargo package name while retaining installer ownership. Historical v1/v2 schemas remain unchanged. Distribution contract v2 removed the updater channel, receipt requirement, and activation rules; the original v1 schema and receipt schema remain unchanged for historical consumers. - A Cargo source version alone is not a supported distribution. Support starts only when a stable GitHub Release built from that exact source passes every activation check; changing a version does not reserve or silently publish it. `.github/workflows/release.yaml` accepts a version-checked manual run from `main` without publication and an annotated `v{version}` tag for publication. A tag run is allowed only for a commit contained in `main`. The manual path must pass first for the same commit and version before a release tag is created. diff --git a/docs/releases/v0.5.1.md b/docs/releases/v0.5.1.md new file mode 100644 index 0000000..8d770bc --- /dev/null +++ b/docs/releases/v0.5.1.md @@ -0,0 +1,11 @@ +# Stack CLI 0.5.1 + +## Changed + +- Prepare the first `stack-diagram-cli` Cargo source distribution; the command remains `stack`. Compiler 0.1.0, theme 0.5.0, formatter 0.1.0, and engine 0.7.0 resolve from crates.io without Git dependencies. +- Preserve existing CLI commands, JSON schema v1, native LSP compatibility, installer-owned upgrades, and user configuration. No layout implementation or approved snapshot changes are included. +- Distribution contract v3 names the Cargo package. Historical schemas and existing release artifacts remain immutable. + +## Installation and verification + +GitHub archives must pass the same four-target reproducibility, signing, provenance, and command checks as previous releases. Homebrew, Aqua, and Cargo activate independently after their actual published artifacts and clean installations are verified. Use the [current distribution guide](../distribution.md) for live channel availability. diff --git a/scripts/distribution-contract.test.mjs b/scripts/distribution-contract.test.mjs index b4d2770..6c7224d 100644 --- a/scripts/distribution-contract.test.mjs +++ b/scripts/distribution-contract.test.mjs @@ -106,8 +106,8 @@ test("removed self-update cannot be reintroduced", () => { assert.throws(() => validateDistributionContract(candidate, cargoToml), /channel set must be exactly/); }); -test("distribution v2 schema rejects removed updater fields", () => { - const schema = JSON.parse(fs.readFileSync(path.join(root, "distribution/distribution-contract-v2.schema.json"), "utf8")); +test("distribution v3 schema rejects removed updater fields", () => { + const schema = JSON.parse(fs.readFileSync(path.join(root, "distribution/distribution-contract-v3.schema.json"), "utf8")); const validate = new Ajv2020({ strict: false }).compile(schema); assert.equal(validate(contract), true, JSON.stringify(validate.errors)); for (const mutate of [value => value.artifacts.installReceiptSchema = "distribution/install-receipt.schema.json", value => value.verification.selfUpdateActivation = ["obsolete"], value => value.channels[0].minimumSupportedCliVersion = "0.4.0"]) { diff --git a/scripts/initial-publish-context.mjs b/scripts/initial-publish-context.mjs new file mode 100644 index 0000000..e2f8c5e --- /dev/null +++ b/scripts/initial-publish-context.mjs @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +export function validateInitialPublish(metadata, runs, expectedSha) { + assert.match(expectedSha, /^[a-f0-9]{40}$/); + assert.equal(metadata.packages.length, 1, 'Expected one source package'); + const crate = metadata.packages[0]; + assert.equal(crate.name, 'stack-diagram-cli'); + assert.equal(crate.version, '0.5.1', 'Only the initial version may use this workflow'); + assert.deepEqual(crate.publish, ['crates-io']); + assert.equal(crate.license, 'Apache-2.0'); + assert.equal(crate.rust_version, '1.85'); + assert.ok(crate.dependencies.length > 0, 'Expected the published dependency graph'); + for (const dependency of crate.dependencies) { + assert.equal(dependency.source, 'registry+https://github.com/rust-lang/crates.io-index', 'Every dependency must come from crates.io'); + assert.equal(dependency.path, undefined, 'Local path dependencies are not publishable'); + } + assert.equal(runs.length, 1, 'The exact main commit needs a CI run'); + assert.equal(runs[0].headSha, expectedSha); + assert.equal(runs[0].status, 'completed'); + assert.equal(runs[0].conclusion, 'success'); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const metadata = JSON.parse(await readFile(process.argv[2], 'utf8')); + const runs = JSON.parse(await readFile(process.argv[3], 'utf8')); + validateInitialPublish(metadata, runs, process.env.EXPECTED_SHA); + console.log('Initial package identity and exact-commit CI verified.'); +} diff --git a/scripts/initial-publish-context.test.mjs b/scripts/initial-publish-context.test.mjs new file mode 100644 index 0000000..1631138 --- /dev/null +++ b/scripts/initial-publish-context.test.mjs @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { validateInitialPublish } from './initial-publish-context.mjs'; + +const sha = 'a'.repeat(40); +const metadata = { packages: [{ name: 'stack-diagram-cli', version: '0.5.1', publish: ['crates-io'], license: 'Apache-2.0', rust_version: '1.85', dependencies: [{ name: 'stack-engine', source: 'registry+https://github.com/rust-lang/crates.io-index' }] }] }; +const runs = [{ headSha: sha, status: 'completed', conclusion: 'success' }]; + +test('accepts only the initial package and successful exact-commit CI', () => { + validateInitialPublish(metadata, runs, sha); +}); + +test('rejects missing, stale, running, failed, and skipped CI', () => { + for (const invalid of [[], [...runs, ...runs], [{ ...runs[0], headSha: 'b'.repeat(40) }], [{ ...runs[0], status: 'in_progress' }], [{ ...runs[0], conclusion: 'failure' }], [{ ...runs[0], conclusion: 'skipped' }]]) { + assert.throws(() => validateInitialPublish(metadata, invalid, sha)); + } +}); + +test('rejects changed package identity, registry, version, license, and MSRV', () => { + for (const change of [{ name: 'other' }, { version: '0.5.2' }, { publish: null }, { publish: ['other-registry'] }, { license: 'MIT' }, { rust_version: '1.86' }]) { + assert.throws(() => validateInitialPublish({ packages: [{ ...metadata.packages[0], ...change }] }, runs, sha)); + } + assert.throws(() => validateInitialPublish({ packages: [] }, runs, sha)); + assert.throws(() => validateInitialPublish({ packages: [...metadata.packages, ...metadata.packages] }, runs, sha)); +}); + +test('rejects mutable, malformed, and shell-like commit inputs', () => { + for (const invalid of ['main', 'a'.repeat(39), 'A'.repeat(40), `${sha}\n`, '$(echo unsafe)', undefined]) { + assert.throws(() => validateInitialPublish(metadata, runs, invalid)); + } +}); + +test('rejects Git, path, and alternative registry dependencies', () => { + for (const dependencies of [[], [{ source: 'git+https://github.com/stack-sh/engine' }], [{ source: null, path: '../engine' }], [{ source: 'registry+https://example.com/index' }]]) { + assert.throws(() => validateInitialPublish({ packages: [{ ...metadata.packages[0], dependencies }] }, runs, sha)); + } +}); diff --git a/scripts/release-security.test.mjs b/scripts/release-security.test.mjs index 121feac..2b28c17 100644 --- a/scripts/release-security.test.mjs +++ b/scripts/release-security.test.mjs @@ -12,7 +12,7 @@ import { verifyReleaseMetadata, } from "./release-security.mjs"; -const version = "0.5.0"; +const version = "0.5.1"; const commit = "0123456789abcdef0123456789abcdef01234567"; const provenancePredicate = "https://slsa.dev/provenance/v1"; const sbomPredicate = "https://spdx.dev/Document/v2.3"; @@ -232,7 +232,7 @@ test("release metadata cannot drift from the source version", (t) => { assert.throws( () => generateReleaseMetadata({ directory, - version: "0.5.0-rc.1", + version: "0.5.1-rc.1", commit, minimumSupportedCliVersion: "0.3.0", sourceDateEpoch: 1_788_566_400, diff --git a/scripts/resolve-release-context.test.mjs b/scripts/resolve-release-context.test.mjs index 3a074c0..6a8d6d8 100644 --- a/scripts/resolve-release-context.test.mjs +++ b/scripts/resolve-release-context.test.mjs @@ -20,17 +20,17 @@ test("main dispatch resolves a non-publishing verification run", () => { ref: "refs/heads/main", refName: "main", sha, - requestedVersion: "0.5.0", + requestedVersion: "0.5.1", cargoToml, contract, }), { - version: "0.5.0", - tag: "v0.5.0", + version: "0.5.1", + tag: "v0.5.1", sourceRef: "refs/heads/main", publish: false, verifiedChannels: "", - minimumSupportedCliVersion: "0.5.0", + minimumSupportedCliVersion: "0.5.1", }, ); }); @@ -39,20 +39,20 @@ test("an exact version tag resolves a publishing run", () => { assert.deepEqual( resolveReleaseContext({ eventName: "push", - ref: "refs/tags/v0.5.0", - refName: "v0.5.0", + ref: "refs/tags/v0.5.1", + refName: "v0.5.1", sha, requestedVersion: "", cargoToml, contract, }), { - version: "0.5.0", - tag: "v0.5.0", - sourceRef: "refs/tags/v0.5.0", + version: "0.5.1", + tag: "v0.5.1", + sourceRef: "refs/tags/v0.5.1", publish: true, verifiedChannels: "github-release", - minimumSupportedCliVersion: "0.5.0", + minimumSupportedCliVersion: "0.5.1", }, ); }); @@ -60,7 +60,7 @@ test("an exact version tag resolves a publishing run", () => { test("removed self-update cannot be activated", () => { const activated = structuredClone(contract); activated.channels.push({ id: "self-update", state: "available" }); - assert.throws(() => resolveReleaseContext({eventName: "push", ref: "refs/tags/v0.5.0", refName: "v0.5.0", sha, cargoToml, contract: activated}), /self-update has been removed/); + assert.throws(() => resolveReleaseContext({eventName: "push", ref: "refs/tags/v0.5.1", refName: "v0.5.1", sha, cargoToml, contract: activated}), /self-update has been removed/); }); test("manual runs from another ref or version are rejected", () => { @@ -72,7 +72,7 @@ test("manual runs from another ref or version are rejected", () => { contract, }; assert.throws( - () => resolveReleaseContext({ ...common, ref: "refs/heads/topic", requestedVersion: "0.5.0" }), + () => resolveReleaseContext({ ...common, ref: "refs/heads/topic", requestedVersion: "0.5.1" }), /must run from main/, ); assert.throws( @@ -107,7 +107,7 @@ test("source and contract version drift is rejected", () => { ref: "refs/heads/main", refName: "main", sha, - requestedVersion: "0.5.0", + requestedVersion: "0.5.1", cargoToml, contract: drifted, }), diff --git a/scripts/test_package_release.py b/scripts/test_package_release.py index ae33930..8328631 100644 --- a/scripts/test_package_release.py +++ b/scripts/test_package_release.py @@ -9,7 +9,7 @@ class PackageReleaseTest(unittest.TestCase): - version = "0.5.0" + version = "0.5.1" target = "aarch64-apple-darwin" source_date_epoch = 1_788_566_400 diff --git a/scripts/validate-distribution-contract.mjs b/scripts/validate-distribution-contract.mjs index 2e993cb..1546eee 100644 --- a/scripts/validate-distribution-contract.mjs +++ b/scripts/validate-distribution-contract.mjs @@ -60,9 +60,9 @@ function cargoValue(cargoToml, field) { export function validateDistributionContract(contract, cargoToml) { const cargoVersion = cargoValue(cargoToml, "version"); - invariant(contract.schemaVersion === 2, "schemaVersion must be 2"); + invariant(contract.schemaVersion === 3, "schemaVersion must be 3"); invariant(contract.product?.binary === "stack", "binary must be stack"); - invariant(contract.product?.sourceCargoPackage === "stack-cli", "source Cargo package must be stack-cli"); + invariant(contract.product?.sourceCargoPackage === "stack-diagram-cli", "source Cargo package must be stack-diagram-cli"); invariant( contract.product.sourceCargoPackage === cargoValue(cargoToml, "name"), "sourceCargoPackage must match Cargo.toml", From d6f6370d1809d781563a0055d811d7d5f83d0aa6 Mon Sep 17 00:00:00 2001 From: konojunya Date: Sun, 6 Sep 2026 05:02:22 +0900 Subject: [PATCH 2/2] Anchor Cargo package includes to the repository root --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 521644c..a9feee3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ homepage = "https://stack-diagram.com/" documentation = "https://stack-diagram.com/docs/guide/getting-started" keywords = ["stack", "diagram", "cli", "svg"] categories = ["command-line-utilities", "visualization"] -include = ["src/**", "templates/**", "catalogs/**", "tests/fixtures/**", "LICENSE", "NOTICE", "THIRD_PARTY_LICENSES.md", "README.md", "Cargo.lock"] +include = ["/src/**", "/templates/**", "/catalogs/**", "/tests/fixtures/**", "/LICENSE", "/NOTICE", "/THIRD_PARTY_LICENSES.md", "/README.md", "/Cargo.lock"] description = "Native command-line interface for Stack diagrams" repository = "https://github.com/stack-sh/cli" license = "Apache-2.0"