1 name: Crates.io Release
2
3 on:
4 push:
5 tags:
6 - "v*.*.*"
7 workflow_dispatch:
8 inputs:
9 tag:
10 description: "Release tag (e.g. v1.0.2)"
11 required: true
12
13 concurrency:
14 # A re-pushed or re-dispatched tag fires this a second time. Queue the
15 # duplicate behind the original instead of cancelling it: cancelling a run
16 # mid-publish can leave a channel half-uploaded, whereas a queued duplicate
17 # just hits the "already published" check and exits clean.
18 group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }}
19 cancel-in-progress: false
20
21 permissions:
22 contents: read
23
24 env:
25 CARGO_TERM_COLOR: always
26
27 jobs:
28 publish:
29 name: Publish to crates.io
30 runs-on: ubuntu-latest
31
32 steps:
33 - name: Checkout
34 uses: actions/checkout@v6
35 with:
36 ref: ${{ github.event.inputs.tag || github.ref }}
37
38 - name: Read Rust toolchain
39 shell: bash
40 run: |
41 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
42 if [ -z "$rust_toolchain" ]; then
43 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
44 exit 1
45 fi
46 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
47
48 - name: Install Rust toolchain
49 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
50 with:
51 toolchain: ${{ env.RUST_TOOLCHAIN }}
52
53 - name: Setup Rust cache
54 uses: Swatinem/rust-cache@v2
55 with:
56 key: crates-publish
57
58 - name: Check whether version already exists on crates.io
59 id: crates-check
60 shell: bash
61 run: |
62 VERSION=$(cargo metadata --no-deps --format-version 1 | \
63 python3 -c "import sys,json; pkgs=json.load(sys.stdin)['packages']; \
64 print(next(p['version'] for p in pkgs if p['name']=='sigit'))")
65 echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
66
67 # crates.io answers 403 to requests without a descriptive
68 # User-Agent, so curl's default one makes this look like a 404 and
69 # the publish below runs into "crate already exists on the index".
70 # Branch on the status code rather than on curl's exit status, so an
71 # answer we do not understand stops the job instead of quietly
72 # degrading into that same failure.
73 # Retry first, so a timeout or a 5xx costs a few seconds instead of
74 # the whole release.
75 STATUS=$(curl -sS -o /dev/null -w '%{http_code}' \
76 --retry 3 --retry-delay 2 --retry-all-errors \
77 -H "User-Agent: ${GITHUB_REPOSITORY} release workflow (https://github.com/${GITHUB_REPOSITORY})" \
78 "https://crates.io/api/v1/crates/sigit/${VERSION}")
79
80 case "$STATUS" in
81 200)
82 echo "sigit ${VERSION} already exists on crates.io, skipping publish"
83 echo "exists=true" >> "$GITHUB_OUTPUT"
84 ;;
85 404)
86 echo "exists=false" >> "$GITHUB_OUTPUT"
87 ;;
88 *)
89 echo "Unexpected ${STATUS} from crates.io while checking whether sigit ${VERSION} is published" >&2
90 exit 1
91 ;;
92 esac
93
94 - name: Publish to crates.io
95 if: steps.crates-check.outputs.exists != 'true'
96 env:
97 CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
98 run: cargo publish --locked