main
yml 233 lines 7.69 KB
Raw
1 name: Release crates.io
2
3 # This is the only workflow that reacts to a tag push. It verifies the tag,
4 # publishes every workspace crate to crates.io in dependency order, and then
5 # dispatches the GitHub release and the MCP Registry release with the same
6 # tag. Neither runs for a crate release that failed: the MCP Registry reads
7 # the published crate, so it has to exist first.
8
9 on:
10 push:
11 tags:
12 - "v*.*.*"
13 workflow_dispatch:
14 inputs:
15 tag:
16 description: "Release tag (e.g. v0.2.0)"
17 required: true
18
19 permissions:
20 contents: read
21 actions: write
22
23 concurrency:
24 group: release-crates-${{ github.ref }}
25 cancel-in-progress: false
26
27 env:
28 CARGO_TERM_COLOR: always
29
30 jobs:
31 verify:
32 name: Verify release
33 runs-on: ubuntu-latest
34 outputs:
35 version: ${{ steps.release.outputs.version }}
36 tag: ${{ steps.release.outputs.tag }}
37 steps:
38 - name: Checkout
39 uses: actions/checkout@v6
40 with:
41 ref: ${{ github.event.inputs.tag || github.ref }}
42
43 - name: Install Rust toolchain
44 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
45 with:
46 toolchain: stable
47 components: clippy, rustfmt
48
49 - name: Cache cargo registry and build artifacts
50 uses: Swatinem/rust-cache@v2
51
52 - name: Set the release version
53 id: release
54 shell: bash
55 run: |
56 if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
57 release_tag="${GITHUB_REF_NAME}"
58 else
59 release_tag="${{ github.event.inputs.tag }}"
60 release_tag="${release_tag#refs/tags/}"
61 fi
62 release_version="${release_tag#v}"
63
64 echo "tag=${release_tag}" >> "$GITHUB_OUTPUT"
65 echo "version=${release_version}" >> "$GITHUB_OUTPUT"
66
67 - name: Ensure tag matches the workspace version
68 shell: bash
69 run: |
70 version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)"
71 if [ -z "$version" ]; then
72 echo "::error::Failed to read the workspace version from Cargo.toml"
73 exit 1
74 fi
75 if [ "$version" != "${{ steps.release.outputs.version }}" ]; then
76 echo "::error::Cargo.toml is ${version} but the tag is ${{ steps.release.outputs.version }}"
77 exit 1
78 fi
79
80 - name: Check formatting
81 run: cargo fmt --all -- --check
82
83 - name: Run clippy
84 run: cargo clippy --workspace --all-targets --locked -- -D warnings
85
86 - name: Run tests
87 run: cargo test --workspace --locked
88
89 # Each lower crate has to build alone: a crate that only compiles
90 # because a sibling's features leak in through the workspace would fail
91 # for every consumer of the published crate.
92 - name: Check the lower crates build alone
93 shell: bash
94 run: |
95 for crate in core aso pricing signing reports play; do
96 cargo check -p "smbcloud-ascapi-${crate}" --locked
97 done
98
99 publish:
100 name: Publish to crates.io
101 runs-on: ubuntu-latest
102 needs: verify
103 environment:
104 name: crates-io
105 # OIDC is how we authenticate to crates.io (trusted publishing): each
106 # crate trusts a GitHub Actions token issued for this repository and
107 # workflow, so no long-lived registry token is stored.
108 permissions:
109 contents: read
110 id-token: write
111 steps:
112 - name: Checkout
113 uses: actions/checkout@v6
114 with:
115 ref: ${{ github.event.inputs.tag || github.ref }}
116
117 - name: Install Rust toolchain
118 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
119 with:
120 toolchain: stable
121
122 - name: Cache cargo registry and build artifacts
123 uses: Swatinem/rust-cache@v2
124
125 - name: Authenticate to crates.io
126 id: auth
127 uses: rust-lang/crates-io-auth-action@v1
128
129 - name: Publish crates
130 env:
131 CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
132 shell: bash
133 run: |
134 version="${{ needs.verify.outputs.version }}"
135
136 # Dependency order: a crate can only be published once everything it
137 # depends on is on crates.io. core first, then the domain crates
138 # that only depend on core, then play (which depends on neither),
139 # then frontend, mcp, and the CLI last.
140 crates=(
141 core
142 aso
143 pricing
144 signing
145 reports
146 play
147 frontend
148 mcp
149 cli
150 )
151
152 for crate in "${crates[@]}"; do
153 name="smbcloud-ascapi-${crate}"
154
155 # Skipping versions that are already up makes a re-run after a
156 # partial failure pick up where it stopped: crates.io versions
157 # are immutable, so a second publish would only error.
158 if curl -sSf -H "User-Agent: smbcloudXYZ/smbcloud-ascapi release-workflow" \
159 "https://crates.io/api/v1/crates/${name}/${version}" >/dev/null 2>&1; then
160 echo "${name} ${version} is already on crates.io, skipping"
161 continue
162 fi
163
164 echo "Publishing ${name} ${version}"
165 # cargo waits for the crate to show up in the index before it
166 # returns, so the next crate can resolve this one.
167 cargo publish -p "${name}" --locked
168 done
169
170 dispatch-distributions:
171 name: Trigger distribution workflows
172 needs: [verify, publish]
173 runs-on: ubuntu-latest
174
175 steps:
176 # If the GitHub release already exists for this tag, the downstream
177 # workflows have already run for this version — either through a
178 # prior dispatch from this workflow, or because the tag push trigger
179 # fired twice (or after a manual run). Skip dispatch in that case so
180 # we don't re-build the six binaries and re-publish the MCP server
181 # for a release that's already out.
182 - name: Skip if distributions are already up
183 id: check
184 uses: actions/github-script@v7
185 with:
186 script: |
187 const tag = '${{ needs.verify.outputs.tag }}';
188 try {
189 await github.rest.repos.getReleaseByTag({
190 owner: context.repo.owner,
191 repo: context.repo.repo,
192 tag: tag,
193 });
194 core.info(`Release ${tag} already exists, distributions are up.`);
195 core.setOutput('already_published', 'true');
196 } catch (err) {
197 if (err.status === 404) {
198 core.info(`Release ${tag} not found, dispatching distributions.`);
199 core.setOutput('already_published', 'false');
200 } else {
201 throw err;
202 }
203 }
204
205 - name: Trigger GitHub release
206 if: steps.check.outputs.already_published != 'true'
207 uses: actions/github-script@v7
208 with:
209 script: |
210 await github.rest.actions.createWorkflowDispatch({
211 owner: context.repo.owner,
212 repo: context.repo.repo,
213 workflow_id: 'release-github.yml',
214 ref: '${{ needs.verify.outputs.tag }}',
215 inputs: {
216 tag: '${{ needs.verify.outputs.tag }}'
217 }
218 })
219
220 - name: Trigger MCP Registry release
221 if: steps.check.outputs.already_published != 'true'
222 uses: actions/github-script@v7
223 with:
224 script: |
225 await github.rest.actions.createWorkflowDispatch({
226 owner: context.repo.owner,
227 repo: context.repo.repo,
228 workflow_id: 'release-mcp-registry.yml',
229 ref: '${{ needs.verify.outputs.tag }}',
230 inputs: {
231 tag: '${{ needs.verify.outputs.tag }}'
232 }
233 })