| 1 | name: Publish to crates.io |
| 2 | |
| 3 | on: |
| 4 | release: |
| 5 | types: [published] |
| 6 | workflow_dispatch: |
| 7 | inputs: |
| 8 | tag: |
| 9 | description: "Release tag (e.g. v0.1.0)" |
| 10 | required: true |
| 11 | dry_run: |
| 12 | description: "Dry run (do not publish)" |
| 13 | type: boolean |
| 14 | default: false |
| 15 | |
| 16 | permissions: |
| 17 | contents: read |
| 18 | # Required for crates.io trusted publishing — allows the job to request |
| 19 | # a GitHub OIDC token and exchange it for a short-lived crates.io token. |
| 20 | id-token: write |
| 21 | |
| 22 | env: |
| 23 | CARGO_TERM_COLOR: always |
| 24 | |
| 25 | jobs: |
| 26 | publish: |
| 27 | name: Publish to crates.io |
| 28 | runs-on: ubuntu-latest |
| 29 | |
| 30 | steps: |
| 31 | - name: Resolve tag and version |
| 32 | id: release |
| 33 | shell: bash |
| 34 | run: | |
| 35 | if [[ "${{ github.event_name }}" == "release" ]]; then |
| 36 | TAG="${{ github.event.release.tag_name }}" |
| 37 | else |
| 38 | TAG="${{ github.event.inputs.tag }}" |
| 39 | fi |
| 40 | VERSION="${TAG#v}" |
| 41 | |
| 42 | echo "tag=${TAG}" >> "$GITHUB_OUTPUT" |
| 43 | echo "version=${VERSION}" >> "$GITHUB_OUTPUT" |
| 44 | |
| 45 | - name: Checkout |
| 46 | uses: actions/checkout@v6 |
| 47 | with: |
| 48 | ref: ${{ steps.release.outputs.tag }} |
| 49 | |
| 50 | - name: Install Rust toolchain |
| 51 | uses: dtolnay/rust-toolchain@stable |
| 52 | |
| 53 | - name: Setup Rust cache |
| 54 | uses: Swatinem/rust-cache@v2 |
| 55 | with: |
| 56 | key: publish |
| 57 | |
| 58 | - name: Verify Cargo.toml version matches tag |
| 59 | shell: bash |
| 60 | run: | |
| 61 | CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 \ |
| 62 | | jq -r '.packages[] | select(.name == "proses") | .version') |
| 63 | TAG_VERSION="${{ steps.release.outputs.version }}" |
| 64 | |
| 65 | echo "Cargo.toml version : ${CARGO_VERSION}" |
| 66 | echo "Release tag version: ${TAG_VERSION}" |
| 67 | |
| 68 | if [[ "${CARGO_VERSION}" != "${TAG_VERSION}" ]]; then |
| 69 | echo "::error::Version mismatch — bump version in Cargo.toml before releasing." |
| 70 | exit 1 |
| 71 | fi |
| 72 | |
| 73 | - name: Run tests |
| 74 | shell: bash |
| 75 | run: cargo test --locked |
| 76 | |
| 77 | # ── Trusted publishing ──────────────────────────────────────────────── |
| 78 | # 1. Request a GitHub OIDC token scoped to crates.io. |
| 79 | # 2. Exchange it with the crates.io trusted-publishing endpoint for a |
| 80 | # short-lived API token (valid for one publish operation only). |
| 81 | # 3. Mask the token immediately so it never appears in logs. |
| 82 | # |
| 83 | # Prerequisites (one-time setup): |
| 84 | # • On crates.io → your crate → "Trusted Publishing" tab, add a publisher: |
| 85 | # Repository owner : smbcloudXYZ |
| 86 | # Repository name : proses |
| 87 | # Workflow filename: release-crate.yml |
| 88 | # Environment name : (leave blank, or match the `environment:` key below) |
| 89 | # • No repository secret is required — the OIDC handshake replaces it. |
| 90 | - name: Obtain crates.io publish token via OIDC |
| 91 | id: crates-token |
| 92 | shell: bash |
| 93 | run: | |
| 94 | # Request a GitHub OIDC JWT scoped to crates.io. |
| 95 | OIDC_TOKEN=$(curl --silent --fail \ |
| 96 | -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ |
| 97 | "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=crates.io" \ |
| 98 | | jq -r '.value') |
| 99 | |
| 100 | if [[ -z "${OIDC_TOKEN}" || "${OIDC_TOKEN}" == "null" ]]; then |
| 101 | echo "::error::Failed to obtain OIDC token — ensure id-token: write is set." |
| 102 | exit 1 |
| 103 | fi |
| 104 | |
| 105 | # Exchange the OIDC JWT for a short-lived crates.io publish token. |
| 106 | RESPONSE=$(curl --silent --fail \ |
| 107 | -X POST "https://crates.io/api/v1/trusted_publishing/tokens" \ |
| 108 | -H "Content-Type: application/json" \ |
| 109 | -d "{\"jwt\": \"${OIDC_TOKEN}\"}") |
| 110 | |
| 111 | CRATES_TOKEN=$(echo "${RESPONSE}" | jq -r '.token') |
| 112 | |
| 113 | if [[ -z "${CRATES_TOKEN}" || "${CRATES_TOKEN}" == "null" ]]; then |
| 114 | echo "::error::Failed to exchange OIDC token — check your trusted publisher config on crates.io." |
| 115 | echo "Response: ${RESPONSE}" |
| 116 | exit 1 |
| 117 | fi |
| 118 | |
| 119 | # Mask the token so it is redacted in all subsequent log output. |
| 120 | echo "::add-mask::${CRATES_TOKEN}" |
| 121 | echo "token=${CRATES_TOKEN}" >> "$GITHUB_OUTPUT" |
| 122 | |
| 123 | - name: Publish |
| 124 | shell: bash |
| 125 | env: |
| 126 | CARGO_REGISTRY_TOKEN: ${{ steps.crates-token.outputs.token }} |
| 127 | run: | |
| 128 | if [[ "${{ github.event.inputs.dry_run }}" == "true" ]]; then |
| 129 | echo "Dry run — package will not be published." |
| 130 | cargo publish --locked --dry-run |
| 131 | else |
| 132 | cargo publish --locked |
| 133 | fi |