1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
name: Release
# Publishing is done by CI, not from a laptop. A local `cargo publish`
# depends on whatever that machine happened to have checked out, and
# crates.io cannot be un-published -- only yanked.
#
# The trigger is a tag. Pushing `v0.0.7` publishes 0.0.7 and nothing
# else: the first job refuses to run if the tag disagrees with
# Cargo.toml.
on:
push:
tags:
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
jobs:
publish:
name: Publish to crates.io
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# A tag that disagrees with Cargo.toml publishes the wrong
# version under the right name, and that cannot be taken back.
- name: The tag must match the version in Cargo.toml
run: |
tag="${GITHUB_REF_NAME#v}"
have="$(cargo metadata --no-deps --format-version 1 \
| jq -r '.packages[] | select(.name=="oxml-cli") | .version')"
if [ "$tag" != "$have" ]; then
echo "::error::tag $tag does not match Cargo.toml $have"
exit 1
fi
echo "publishing oxml-cli $tag"
- run: cargo test --all-features
# The suite ships one version across six crates, and the order is
# load-bearing: `oxml-cli` and `oxml-mcp` depend on `xmlschema`,
# which depends on `oxml`. Publishing out of order leaves a tree
# resolving two incompatible `oxml` versions, which fails with
# `expected oxml::tree::Document, found Document`.
#
# doc/RELEASING.md got this order wrong once. Waiting here makes
# the order a property of the pipeline rather than of whoever
# pushes the tags: publish the tags in any order and each crate
# holds until what it needs is live.
- name: Wait for the crates this one depends on
run: |
tag="${GITHUB_REF_NAME#v}"
for dep in oxml xmlschema; do
echo "waiting for $dep $tag"
ok=
for i in $(seq 1 90); do
if curl -sf "https://crates.io/api/v1/crates/$dep/$tag" >/dev/null; then
ok=1; echo " $dep $tag is live"; break
fi
sleep 20
done
if [ -z "$ok" ]; then
echo "::error::$dep $tag did not appear within 30 minutes"
exit 1
fi
done
# Until CARGO_REGISTRY_TOKEN exists, this crate is published
# from a laptop with `scripts/publish.sh`. Say so plainly rather
# than letting `cargo publish` fail on an empty token, which
# reports as an authentication error and reads like a revoked
# credential.
- name: The publish token must be set
env:
TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
if [ -z "$TOKEN" ]; then
echo "::error::CARGO_REGISTRY_TOKEN is not set on this repository."
echo "Either add it, or publish with scripts/publish.sh and do"
echo "not push release tags."
exit 1
fi
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish
# crates.io indexes asynchronously, so a publish that returned
# success is not yet a publish anyone can depend on. The next
# crate in the suite waits on exactly this.
- name: Confirm it is on crates.io
run: |
tag="${GITHUB_REF_NAME#v}"
for i in $(seq 1 30); do
if curl -sf "https://crates.io/api/v1/crates/oxml-cli/$tag" >/dev/null; then
echo "oxml-cli $tag is live"; exit 0
fi
echo "not indexed yet ($i/30)"; sleep 10
done
echo "::error::oxml-cli $tag did not appear within five minutes"
exit 1