From 76d6055d93f98f44ccaa6250e7dc579a86f683e9 Mon Sep 17 00:00:00 2001 From: Zack Maril Date: Sun, 30 Aug 2026 09:00:36 -0400 Subject: [PATCH 1/2] docs: clear the prose linter's remaining objections Small wording fixes plus the fleet's updated .vale.ini from PowderworksCode/ordnung#42, so main goes green on merge rather than waiting for the next distribution round. The bytes are identical to the tier's. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0144PU3MssnfbM5tZiqXW7d3 --- .vale.ini | 28 +++++++++++++++++++++++++--- CHANGELOG.md | 2 +- README.md | 2 +- notes/DESIGN.md | 4 ++-- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.vale.ini b/.vale.ini index 0142431..454101e 100644 --- a/.vale.ini +++ b/.vale.ini @@ -1,4 +1,4 @@ -# Fleet-managed by conf (.ordnung/managed/vale/vale.ini). +# Distributed by Ordnung's `paranoid` tier: edit it there, not here. # # proselint and write-good rather than a house style guide: they catch prose # that is weak on anyone's terms — weasel words, passive constructions, @@ -12,5 +12,27 @@ Packages = proselint, write-good [*.md] BasedOnStyles = proselint, write-good -# Prose lives in sentences; a fenced block is code someone will run. -BlockIgnores = (?s) *(```.*?```) +# write-good ships these two at error level and the rest of its style advice at +# warning, which is its packaging rather than a considered position: opening a +# sentence with "There is" is a preference exactly like the passive voice it +# warns about. Levelled down so that prose style informs and does not gate; +# what still fails a build is a real defect, such as a TODO left in a document. +write-good.ThereIs = warning +write-good.So = warning + +# Prose lives in sentences; a fenced block is code someone will run. Anchored to +# line starts because the unanchored form silently matched nothing: a fence with +# no language tag was graded as prose, and `[PATHS]...` in a usage block read as +# an ellipsis waiting to be typeset. +BlockIgnores = (?sm)^ *```.*?^ *``` + +# `vale sync` writes the style packages here, README files and all, and Vale +# then grades the prose of the linters it just downloaded. +[.vale/styles/**] +BasedOnStyles = + +# Verbatim text from other projects, reproduced to satisfy their licences. +# proselint asks for `©` where a licence header writes `(c)`, and taking that +# advice would edit somebody else's licence. +[THIRD-PARTY-NOTICES.md] +BasedOnStyles = diff --git a/CHANGELOG.md b/CHANGELOG.md index 704cbe8..036e978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ documents through `complete_json`, checking whether the result parses: | `{"que` | `{"que"}` ✗ | `{}` | A closer-counter has no model of where it is in the grammar, so it cannot know -that `tru` needs an `e` or that a trailing `\` escapes the very quote it just +that `tru` needs an `e` or that a trailing `\` escapes the quote it just appended. That is structural, not a bug list. ### Added diff --git a/README.md b/README.md index 11138c9..b80dbca 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ See `examples/sse.rs`. ## Upgrading from 0.1 `complete_json` and `get_closing_string_for_partial_json` keep their signatures. -The behaviour is stricter and more correct: +The behaviour is stricter and better defined: - **Malformed input now returns `Err`.** 0.1 returned `Ok` with output that did not parse — for `{"a": tru`, `{"a": "x\`, `{"a":`, `{"a":1,`, `{"que`, and a diff --git a/notes/DESIGN.md b/notes/DESIGN.md index a63eeb5..ecc5e41 100644 --- a/notes/DESIGN.md +++ b/notes/DESIGN.md @@ -376,7 +376,7 @@ and `jawohl.stream(MyZodSchema)` are host-side sugar over it. the same handle; `push`/`snapshot`/`status` are synchronous **methods** on it; `changes()` is a **stream op**; a native validator is a **value-returning callback param**. jawohl exercises all three of -jedem's hard three — which is what makes it a good acid test and also what +jedem's hard three — which is what makes it a good proving ground and also what makes §8 the risk. --- @@ -420,7 +420,7 @@ bet: 1. **The core track absorbs the wait.** C1–C4 is the majority of the engineering and none of it is blocked. By the time jedem reaches step 3, jawohl should have a complete, tested Rust core waiting for a surface. -2. **jawohl is the acid test that pulls jedem forward.** `findings.md`'s +2. **jawohl is the proving ground that pulls jedem forward.** `findings.md`'s method — author the complete real surface before freezing the IR — is what caught every gap in fluessig. jawohl's surface is small, precise, and hits all three hard cases, which makes it a far better forcing function than a demo From f50c2927dda3f8c2e0f188b582d44eabd229ae17 Mon Sep 17 00:00:00 2001 From: Zack Maril Date: Sun, 30 Aug 2026 09:13:11 -0400 Subject: [PATCH 2/2] chore: take the fleet's managed files Ordnung's policy tiers now carry the content that satisfies the checks they raise, so these files arrive from the tier rather than from conf. Most of the diff is that attribution changing. Two real changes travel with it. The pre-commit hook no longer passes `.` to Straitjacket, so a repository's own straitjacket.toml decides what gets scanned rather than the hook overruling it. And .vale.ini stops gating on prose style: write-good's ThereIs and So are levelled down to match the rest of its advice, Vale no longer grades the style packages it downloads, and third-party notices are left alone. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0144PU3MssnfbM5tZiqXW7d3 --- .githooks/README.md | 2 +- .githooks/commit-msg | 2 +- .githooks/pre-commit | 7 +- .github/dependabot.yml | 8 +- .github/workflows/fleet-lint.yml | 11 +- .github/workflows/release.yml | 225 +++++++++++++++++++++++++++++++ .vale.ini | 7 +- scripts/publish.sh | 137 +++++++++++++++++++ 8 files changed, 382 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100755 scripts/publish.sh diff --git a/.githooks/README.md b/.githooks/README.md index da236b2..a348bf8 100644 --- a/.githooks/README.md +++ b/.githooks/README.md @@ -3,7 +3,7 @@ Committed Git hooks that run the fleet's gate locally, before a commit lands, so a failure surfaces here rather than after a push. -Fleet-managed by [conf](https://github.com/PowderworksCode/conf); edit them +Distributed by [Ordnung](https://ordnung.dev)'s `recommended` tier; edit them there, not here — a local change is drift the next sync reports. ## Activate diff --git a/.githooks/commit-msg b/.githooks/commit-msg index 9b1b482..6fb12a0 100755 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -3,7 +3,7 @@ # applies the same rule to the commit subject, so a bad message fails here # rather than after a push. Bypass with `git commit --no-verify`. # -# Fleet-managed by conf (.ordnung/managed/githooks/commit-msg): edit it there. +# Distributed by Ordnung's `recommended` tier: edit it there, not here. msg_file="$1" # First non-blank, non-comment line is the subject. diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 6a8c345..0b6984c 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -2,8 +2,11 @@ # The local gate: what CI would say about this tree, said before the commit # lands. Bypass with `git commit --no-verify`. # -# Fleet-managed by conf (.ordnung/managed/githooks/pre-commit). +# Fleet-managed by conf (.ordnung/managed/githooks/pre-commit): it replaces the +# `ordnung check` hook Ordnung's `recommended` tier ships. set -eu +# No path argument: a positional overrides the `paths` a repository set in its +# own straitjacket.toml, and a hook has no business overruling that. hooks_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) -exec "$hooks_dir/run-straitjacket" . +exec "$hooks_dir/run-straitjacket" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0ffa1c6..7e4b3ec 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,14 +1,14 @@ version: 2 -# Fleet-owned. Edit this in conf, not in the member repository: a local change is -# drift, and the next sync overwrites it. +# Fleet-owned. Edit this in the policy tier, not in the member repository: a +# local change is drift, and the next sync overwrites it. # # One file for every member. The directories inside it are globs rather than a # list of each repository's actual layout, so this file states fleet POLICY — # weekly, minor and patch grouped into one pull request, five open at most — and # lets the globs discover the topology. A glob matching nothing is not an error, -# so the same bytes are correct in a Cargo workspace, a Bun site, and conf, -# which has neither. +# so the same bytes are correct in a Cargo workspace, a Bun site, and a fleet +# definition, which has neither. # # The alternative was a file per repository shape. That put facts about each # repository's layout into the fleet, where they would go stale silently the diff --git a/.github/workflows/fleet-lint.yml b/.github/workflows/fleet-lint.yml index 83f07d3..7e5dbc9 100644 --- a/.github/workflows/fleet-lint.yml +++ b/.github/workflows/fleet-lint.yml @@ -1,9 +1,12 @@ name: fleet-lint -# Fleet-managed by conf (.ordnung/managed/github/lint.yml): the static-analysis -# floor every member runs — prose, workflow, shell, and public-API hygiene. -# Edit it in conf and let ordnung distribute it; a local edit here is drift the -# next fleet sync will report. +# Distributed by Ordnung's `paranoid` tier: the static-analysis floor every +# member runs — prose, workflow, shell, and public-API hygiene. Edit it in the +# tier and let ordnung distribute it; a local edit here is drift the next fleet +# sync will report. +# +# It replaces, by entry name, the codespell-only workflow the `recommended` tier +# ships: the extra jobs are the checks this tier raises. on: push: branches: [main] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1f5125a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,225 @@ +name: release (rust) + +# A tag is the whole trigger. Everything the installer reads — the archives, the +# checksums, and the "latest" pointer — is produced here, so a release is never +# assembled by hand. +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + tag: + description: Existing tag to build a release for + required: true + +permissions: + contents: read + +# Never run two releases at once, and never cancel one in flight: a cancelled +# `cargo publish` can leave a version uploaded that can never be reused. +concurrency: + group: release-${{ inputs.tag || github.ref_name }} + cancel-in-progress: false + +env: + TAG: ${{ inputs.tag || github.ref_name }} + +jobs: + # The release is created as a draft and published only after every archive and + # the checksum file are in place. A draft is not returned by the "latest" + # endpoint, so `install.sh` can never see a half-uploaded release. + draft: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + outputs: + # What this repository publishes, answered where the checkout is. Neither + # can be a job-level `if: hashFiles(...)`: that reads a workspace which + # does not exist before checkout, and a workflow whose expression cannot + # be evaluated does not start at all — no jobs, no release, on a tag push + # nobody is watching. + crate: ${{ steps.publishes.outputs.crate }} + binary: ${{ steps.publishes.outputs.binary }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ env.TAG }} + - id: publishes + run: | + if [ -f scripts/publish.sh ]; then + echo "crate=true" >> "$GITHUB_OUTPUT" + else + echo "crate=false" >> "$GITHUB_OUTPUT" + echo "no scripts/publish.sh; this release ends at the smoke test" + fi + # A library crate has nothing to put in an archive, nothing to + # install, and nothing to smoke-test. Cargo is asked rather than the + # file tree, because a binary target can be declared several ways. + if cargo metadata --no-deps --format-version 1 | + jq -e '[.packages[].targets[] | select(any(.kind[]; . == "bin"))] | length > 0' >/dev/null; then + echo "binary=true" >> "$GITHUB_OUTPUT" + else + echo "binary=false" >> "$GITHUB_OUTPUT" + echo "no binary targets; this release is a crate publish only" + fi + - name: Check the tag against Cargo.toml + run: | + crate=$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n 1) + if [ "$TAG" != "v${crate}" ]; then + echo "tag ${TAG} does not match Cargo.toml version ${crate}" >&2 + exit 1 + fi + - name: Create the draft release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + if ! gh release view "$TAG" >/dev/null 2>&1; then + gh release create "$TAG" --draft --title "$TAG" --generate-notes + fi + + build: + needs: draft + if: needs.draft.outputs.binary == 'true' + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + - target: aarch64-unknown-linux-musl + os: ubuntu-latest + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-apple-darwin + os: macos-latest + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ env.TAG }} + # Both Linux targets cross-compile from an ordinary x86_64 host, because + # .cargo/config.toml links them with rust-lld against a self-contained + # musl. No cross toolchain, container, or apt package is involved. + - run: rustup target add ${{ matrix.target }} + # No build cache here, deliberately: a cache is writable from other + # workflows, and these binaries are the release. Cold builds are the + # price of artifacts nothing else could have written to (zizmor's + # cache-poisoning audit). + - run: cargo build --release --locked --target ${{ matrix.target }} + - name: Package + run: | + stage=$(mktemp -d) + cp "target/${{ matrix.target }}/release/jawohl" "$stage/" + cp README.md LICENSE "$stage/" + tar -czf "jawohl-${TAG}-${{ matrix.target }}.tar.gz" -C "$stage" \ + jawohl README.md LICENSE + - name: Upload + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh release upload "$TAG" "jawohl-${TAG}-${{ matrix.target }}.tar.gz" --clobber + + # Runs for every release, because un-drafting is not a binary concern: a + # library's release would otherwise stay a draft forever. Only the archives + # are conditional. `always()` is what lets a skipped `build` through; without + # it a skipped dependency skips this job too. + publish: + needs: [draft, build] + if: >- + always() && needs.draft.result == 'success' + && (needs.build.result == 'success' || needs.build.result == 'skipped') + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + steps: + # The checksums are computed from what the release actually holds rather + # than from what each build job believes it uploaded. + - name: Checksum every archive + if: needs.draft.outputs.binary == 'true' + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + gh release download "$TAG" --pattern '*.tar.gz' --dir assets + cd assets && sha256sum ./*.tar.gz | sed 's|\./||' > SHA256SUMS + cat SHA256SUMS + gh release upload "$TAG" SHA256SUMS --clobber + - name: Publish + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh release edit "$TAG" --draft=false --latest + + # Install the release that was just published, on every supported platform, + # through both the script and the action. Called rather than triggered: a + # release published by GITHUB_TOKEN raises no event that starts a workflow. + smoke: + needs: [draft, publish] + if: >- + always() && needs.publish.result == 'success' + && needs.draft.outputs.binary == 'true' + permissions: + contents: read + uses: ./.github/workflows/install-smoke.yml + with: + version: ${{ inputs.tag || github.ref_name }} + + # Publish the same commit to crates.io, after the binaries have been proven + # to install and run. crates.io is the one place a version can never be + # replaced, so it goes last and everything checkable is checked first. + # + # The logic lives in scripts/publish.sh so the same path can be run by hand, + # and so this job is a caller rather than a second definition of it. + crate: + needs: [draft, publish, smoke] + # Only repositories that publish a crate carry the script; everyone else + # ends at the smoke test. The answer comes from the draft job, which has + # the checkout — see the note on its outputs. + # + # A skipped smoke job means either "no binary in this repository" or "the + # binary never got that far", and those must not read alike. `publish` + # tells them apart: it succeeds when the archives are up or when there were + # none to build, and is skipped when the build failed. + if: >- + always() && needs.draft.outputs.crate == 'true' + && needs.publish.result == 'success' + && (needs.smoke.result == 'success' || needs.smoke.result == 'skipped') + runs-on: ubuntu-latest + timeout-minutes: 20 + # One place to require a human approval before an irreversible publish: + # Settings > Environments > crates-io > Required reviewers. + environment: crates-io + permissions: + id-token: write + env: + # `secrets` cannot be read from an `if`, so the question of whether one + # exists is answered here, where it can. + HAS_STORED_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN != '' }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.tag || github.ref_name }} + # Trusted publishing is the normal path and needs no secret. The stored + # token exists only to bootstrap: a crate's first version cannot be + # published this way, because crates.io will not attach a trusted + # publisher to a crate that does not exist yet. Delete the secret once + # the trusted publisher is configured and this step takes over again. + - uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 + id: auth + if: env.HAS_STORED_TOKEN == 'false' + - name: Publish + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token || secrets.CARGO_REGISTRY_TOKEN }} + run: | + if [ "${HAS_STORED_TOKEN}" = "true" ]; then + echo "publishing with the stored bootstrap token; delete it once trusted publishing is configured" + else + echo "publishing with a short-lived trusted-publishing token" + fi + scripts/publish.sh --execute diff --git a/.vale.ini b/.vale.ini index 454101e..0538725 100644 --- a/.vale.ini +++ b/.vale.ini @@ -20,11 +20,8 @@ BasedOnStyles = proselint, write-good write-good.ThereIs = warning write-good.So = warning -# Prose lives in sentences; a fenced block is code someone will run. Anchored to -# line starts because the unanchored form silently matched nothing: a fence with -# no language tag was graded as prose, and `[PATHS]...` in a usage block read as -# an ellipsis waiting to be typeset. -BlockIgnores = (?sm)^ *```.*?^ *``` +# Prose lives in sentences; a fenced block is code someone will run. +BlockIgnores = (?s) *(```.*?```) # `vale sync` writes the style packages here, README files and all, and Vale # then grades the prose of the linters it just downloaded. diff --git a/scripts/publish.sh b/scripts/publish.sh new file mode 100755 index 0000000..826c7a2 --- /dev/null +++ b/scripts/publish.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# Publish jawohl to crates.io. +# +# Publishing is irreversible: a version can be yanked but never deleted, and a +# name/version pair can never be reused. The shape of this script follows from +# that. +# +# - Dry run is the default. Uploading takes --execute. +# - A version the registry already has is skipped rather than attempted, so +# re-running a release for an existing tag is a no-op instead of a failure. +# - Existing versions are read from the sparse index rather than the web API, +# because the index is what cargo itself resolves against, and because +# --index lets the whole path be rehearsed against a local registry. +# - The version is whatever Cargo.toml says. Nothing here computes or bumps +# it; the release tag is checked against it in the release workflow. +# +# Usage: +# scripts/publish.sh # dry run: package and verify +# scripts/publish.sh --execute # real publish; needs a token +# scripts/publish.sh --execute --registry local --index ./idx +# +# --dry-run package and compile the tarball; never uploads (default) +# --execute actually publish +# --registry NAME publish to a cargo registry other than crates.io +# --index URL where to enumerate existing versions; an https:// base or a +# local sparse-index directory. Defaults to index.crates.io. +# --allow-dirty package with uncommitted changes present +# +# Environment: +# CARGO_REGISTRY_TOKEN required for --execute against crates.io. +# CARGO_REGISTRIES__TOKEN ... or for --execute --registry . +# Neither is ever logged. +# +# Fleet-managed by conf (.ordnung/managed/publishing/rust/publish.sh): edit it +# there. The crate name is substituted from the repository name, so a crate +# named differently from its repository needs a copy of its own. +# +# straitjacket-allow-file:no-comments — this is the procedure for the one +# action in the repository that cannot be undone, and sh has no +# documentation-comment syntax to hoist the reasoning into. +set -euo pipefail + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +CRATE=jawohl +MODE=dry-run +REGISTRY="" +INDEX_BASE="https://index.crates.io" +ALLOW_DIRTY=0 + +while [ $# -gt 0 ]; do + case "$1" in + --dry-run) MODE=dry-run ;; + --execute) MODE=execute ;; + --registry) REGISTRY=${2:?--registry needs a name}; shift ;; + --index) INDEX_BASE=${2:?--index needs a url or directory}; shift ;; + --allow-dirty) ALLOW_DIRTY=1 ;; + -h|--help) sed -n '2,33p' "${BASH_SOURCE[0]}"; exit 0 ;; + -*) echo "publish: unknown flag $1" >&2; exit 2 ;; + *) echo "publish: unexpected argument $1" >&2; exit 2 ;; + esac + shift +done + +cd "$ROOT" + +VERSION=$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n 1) +[ -n "$VERSION" ] || { echo "publish: no version in Cargo.toml" >&2; exit 1; } + +# cargo reads the token for a named registry from CARGO_REGISTRIES__TOKEN, +# and only the default registry from CARGO_REGISTRY_TOKEN. +if [ -n "$REGISTRY" ]; then + reg_upper=${REGISTRY^^}; reg_upper=${reg_upper//-/_} + TOKEN_VAR="CARGO_REGISTRIES_${reg_upper}_TOKEN" +else + TOKEN_VAR=CARGO_REGISTRY_TOKEN +fi +if [ "$MODE" = execute ] && [ -z "${!TOKEN_VAR:-}" ]; then + cat >&2 < Secrets and variables > Actions > New repository secret + Name: CARGO_REGISTRY_TOKEN + Value: a crates.io API token with publish-new, scoped to $CRATE + + Once the crate exists on crates.io, configure Trusted Publishing instead + and delete that secret: the release workflow then authenticates over OIDC + with a token that lives under an hour. See notes/field_guide.md. +MSG + exit 1 +fi + +# The sparse index lays a name out as //, and each +# line is one published version. +index_path() { + local name=$1 + case ${#name} in + 1) printf '1/%s\n' "$name" ;; + 2) printf '2/%s\n' "$name" ;; + 3) printf '3/%s/%s\n' "${name:0:1}" "$name" ;; + *) printf '%s/%s/%s\n' "${name:0:2}" "${name:2:2}" "$name" ;; + esac +} + +# Whether the registry already carries this version. +# +# A missing entry is the normal case for a first publish and is not an error; +# anything else that goes wrong reads as "not published", and cargo refuses the +# upload on its own if that guess was wrong. +already_published() { + local path body + path=$(index_path "$CRATE") + if [ -d "$INDEX_BASE" ]; then + [ -f "${INDEX_BASE}/${path}" ] || return 1 + body=$(cat "${INDEX_BASE}/${path}") + else + body=$(curl -sSf "${INDEX_BASE}/${path}" 2>/dev/null) || return 1 + fi + # A herestring rather than a pipe: `grep -q` exits at the first match, and + # under `set -o pipefail` the SIGPIPE that gives the writer would become the + # status of the whole pipeline, turning a hit into a miss. + grep -q "\"vers\"[[:space:]]*:[[:space:]]*\"${VERSION}\"" <<<"$body" +} + +if already_published; then + echo "publish: ${CRATE} ${VERSION} is already on the registry; nothing to do" + exit 0 +fi + +# Written as `if` rather than `test && ...`: under `set -e` a false test as a +# bare statement aborts the script. +args=(publish --locked) +if [ "$MODE" = dry-run ]; then args+=(--dry-run); fi +if [ -n "$REGISTRY" ]; then args+=(--registry "$REGISTRY"); fi +if [ "$ALLOW_DIRTY" = 1 ]; then args+=(--allow-dirty); fi + +echo "publish: ${MODE} ${CRATE} ${VERSION}" +cargo "${args[@]}"