diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml new file mode 100644 index 0000000..3a29820 --- /dev/null +++ b/.github/workflows/publish-cli.yml @@ -0,0 +1,83 @@ +# One-shot bootstrap: the first `termlens-cli` publish (#255). +# +# Every other publish in this repository is crates.io Trusted Publishing — +# release.yml exchanges a job's OIDC token for a short-lived one and no +# secret is stored. That cannot be used here: Trusted Publishing is +# configured per crate, on a crate that already exists, so the first publish +# of a new name has nothing to configure it against. release.yml therefore +# publishes termlens-cli only when crates.io already knows it, and warns +# otherwise. +# +# This workflow closes that gap exactly once, with a token, from a tag. Once +# it has run: link Trusted Publishing for termlens-cli (crates.io → +# termlens-cli → Settings → Trusted Publishing → GitHub, repository +# vyncint/termlens, workflow release.yml, environment release), then revoke +# the token, delete the CARGO_REGISTRY_TOKEN secret and delete this file. +# Every release after that goes through release.yml with no secret at all. +# +# Deliberately not wired to the `release` environment: that environment +# exists so an OIDC token cannot be minted from a branch, and its policy +# admits only `v*` refs. CARGO_REGISTRY_TOKEN is a repository secret, so the +# environment would add no protection here while blocking the dispatch. +name: publish-cli (bootstrap) + +on: + workflow_dispatch: + inputs: + ref: + description: The tag to publish from. Its version must match the manifest. + required: true + default: v0.10.0 + confirm: + description: Type the crate name to confirm this irreversible publish. + required: true + default: "" + +permissions: + contents: read + +concurrency: + group: publish-cli + cancel-in-progress: false + +jobs: + publish: + name: publish termlens-cli + runs-on: ubuntu-latest + steps: + - name: Refuse an unconfirmed dispatch + env: + CONFIRM: ${{ inputs.confirm }} + run: | + if [ "$CONFIRM" != "termlens-cli" ]; then + echo "::error::type 'termlens-cli' in the confirm box; a publish cannot be undone" + exit 1 + fi + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ inputs.ref }} + persist-credentials: false + - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1 + with: + toolchain: stable + # The same guard release.yml puts in front of every publish: the crate + # that goes out must be the version the tag names, or the registry gets + # a number that means nothing. + - name: Compare the ref against the crate version + env: + REF: ${{ inputs.ref }} + run: | + tag="${REF#v}" + version="$(cargo metadata --no-deps --format-version 1 \ + | jq -r '.packages[] | select(.name == "termlens-cli") | .version')" + echo "ref=${tag} crate=${version}" + if [ "$tag" != "$version" ]; then + echo "::error::${REF} does not match termlens-cli ${version}" + exit 1 + fi + # --locked so the published crate is the tested tree, not whatever the + # index offers today. The verify build resolves `termlens` from + # crates.io rather than the path, which is what a consumer will do. + - run: cargo publish -p termlens-cli --locked + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/zizmor.yml b/.github/zizmor.yml index b6dabed..9bbcf87 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -14,3 +14,14 @@ rules: - release.yml - install.yml - windows.yml + - publish-cli.yml + + # Correct advice that this one workflow cannot take, which is the whole + # reason it exists. Trusted Publishing is configured per crate on a crate + # that already exists, so the *first* publish of a new name has nothing to + # configure it against and must use a token. Every other publish here is + # Trusted Publishing (release.yml), and this file is deleted along with the + # secret once crates.io knows termlens-cli. + use-trusted-publishing: + ignore: + - publish-cli.yml