krz/orgo

Lightning fast org-mode static site generator.

clone: git clone https://gitbay.org/krz/orgo.git

6a024f243e9341c69cf714dd4e697bcabb092975

unsigned

author: Christian Cleberg <hello@cleberg.net> · 2026-08-11T23:43:16Z

Publish to crates.io from CI, with no stored token

Trusted publishing: the job mints a GitHub OIDC token, exchanges it with
crates.io for a token that lives for the length of the job, and crates.io revokes
it at the end. Nothing is stored in this repository, so there is nothing to leak
and nothing to rotate.

Two guards around it, because publishing is the one step here that cannot be
undone — a version can be yanked but never replaced:

- `needs: build`, so nothing publishes unless all four target binaries compiled.
- `environment: crates-io`. Adding a required reviewer to that environment in the
  repository settings turns a tag push into "waiting for a human". Until someone
  does, the environment exists but gates nothing.

`--locked` publishes the dependency versions the tests actually ran against.

Also runs `cargo package` in CI on Linux. Packaging breaks in ways an ordinary
build does not — an `exclude` that drops a file the tests need, a `readme` path
that moved — and finding that at a tag is finding it too late.
 .github/workflows/ci.yml      |  7 +++++++
 .github/workflows/release.yml | 40 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 47 insertions(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6a8af1d..91e3cad 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -70,6 +70,13 @@ jobs:
       - name: Clippy
         run: cargo clippy --all-targets --locked -- -D warnings
 
+      # `cargo package` builds the crate exactly as crates.io will receive it, which is
+      # how an `exclude` that drops a file the tests need, or a `readme` pointing at a
+      # file that was renamed, gets caught here rather than during a release.
+      - name: Package
+        if: runner.os == 'Linux'
+        run: cargo package --locked
+
       # The documentation site is built by the tool it documents, so a docs page that no
       # longer builds is a product defect. `--strict` fails on broken internal links,
       # which is the failure mode a docs site actually has.
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5188e4a..310fb2b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -111,3 +111,43 @@ jobs:
           files: |
             *.tar.gz
             *.tar.gz.sha256
+
+  publish:
+    name: publish to crates.io
+    needs: build
+    runs-on: ubuntu-latest
+    # Named so it can be gated. Adding a required reviewer to this environment in the
+    # repository settings turns a tag push into "waiting for a human", which is the right
+    # shape for the one step in this workflow that cannot be undone: a published version
+    # can be yanked but never replaced.
+    environment: crates-io
+    permissions:
+      # The OIDC token this mints *is* the credential — there is no API token stored in
+      # this repository, and nothing to leak from a compromised job beyond a token that
+      # crates.io revokes when the job ends.
+      id-token: write
+      contents: read
+    steps:
+      - name: Checkout
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+        with:
+          ref: ${{ github.event.inputs.tag || github.ref }}
+
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable
+        with:
+          toolchain: stable
+
+      # Exchanges GitHub's OIDC token for a short-lived crates.io token, and revokes it
+      # when the job finishes. Configured on crates.io against this repository, this
+      # workflow's filename, and the environment above.
+      - name: Authenticate with crates.io
+        id: auth
+        uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
+
+      # `--locked` publishes exactly the dependency versions the tests ran against, rather
+      # than whatever resolves at publish time.
+      - name: Publish
+        run: cargo publish --locked
+        env:
+          CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}