Skip to content

Commit c67e311

Browse files
committed
Refactor release workflows for orchestrator model
- Make `release-crate.yml` the only workflow triggered by tag push - All downstream release workflows now run only via `workflow_dispatch` - Add outputs to orchestrator for downstream triggers - Update documentation in `.agents/skills/ci-cd/SKILL.md` to clarify orchestration - Add retry logic for Homebrew asset download
1 parent af6eeaa commit c67e311

16 files changed

Lines changed: 502 additions & 142 deletions

File tree

.agents/skills/ci-cd/SKILL.md

Lines changed: 184 additions & 74 deletions
Large diffs are not rendered by default.

.github/workflows/release-crate.yml

Lines changed: 207 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,47 @@ on:
1212

1313
permissions:
1414
contents: read
15+
actions: write
1516

1617
env:
1718
CARGO_TERM_COLOR: always
1819
CLI_CLIENT_SECRET: ${{ secrets.CLI_CLIENT_SECRET }}
1920

2021
jobs:
2122
publish:
22-
name: Publish to crates.io
23+
name: Publish workspace crates to crates.io
2324
runs-on: ubuntu-latest
25+
outputs:
26+
tag: ${{ steps.release.outputs.tag }}
27+
published_count: ${{ steps.plan.outputs.count }}
28+
publish_cli_sdk_pypi: ${{ steps.downstream.outputs.publish_cli_sdk_pypi }}
29+
publish_cli_sdk_npm: ${{ steps.downstream.outputs.publish_cli_sdk_npm }}
30+
publish_cli_sdk_gem: ${{ steps.downstream.outputs.publish_cli_sdk_gem }}
2431

2532
steps:
2633
- name: Checkout
2734
uses: actions/checkout@v6
2835
with:
36+
fetch-depth: 0
2937
ref: ${{ github.event.inputs.tag || github.ref }}
3038

3139
- name: Set the release version
40+
id: release
3241
shell: bash
3342
run: |
3443
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
44+
release_tag="${GITHUB_REF_NAME}"
3545
release_version="${GITHUB_REF_NAME#v}"
3646
else
37-
release_version="${{ github.event.inputs.tag }}"
38-
release_version="${release_version#v}"
47+
release_tag="${{ github.event.inputs.tag }}"
48+
release_tag="${release_tag#refs/tags/}"
49+
release_version="${release_tag#v}"
3950
fi
4051
52+
echo "RELEASE_TAG=${release_tag}" >> "$GITHUB_ENV"
4153
echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
54+
echo "tag=${release_tag}" >> "$GITHUB_OUTPUT"
55+
echo "version=${release_version}" >> "$GITHUB_OUTPUT"
4256
4357
- name: Read Rust toolchain
4458
shell: bash
@@ -61,37 +75,207 @@ jobs:
6175
with:
6276
key: publish
6377

64-
- name: Verify Cargo.toml version matches tag
78+
- name: Install cargo-workspaces
79+
shell: bash
80+
run: cargo install cargo-workspaces --version 0.4.2 --locked
81+
82+
- name: Verify workspace crate versions match tag
6583
shell: bash
6684
run: |
67-
CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 \
68-
| jq -r '.packages[] | select(.name == "smbcloud-cli") | .version')
85+
cargo metadata --no-deps --format-version 1 > metadata.json
86+
87+
crate_count="$(jq '[.packages[] | select(.manifest_path | contains("/crates/"))] | length' metadata.json)"
88+
if [[ "$crate_count" == "0" ]]; then
89+
echo "::error::No workspace crates found under /crates."
90+
exit 1
91+
fi
6992
70-
echo "Cargo.toml version : ${CARGO_VERSION}"
71-
echo "Release tag version: ${RELEASE_VERSION}"
93+
mismatches="$({
94+
jq -r --arg release_version "${RELEASE_VERSION}" '
95+
.packages[]
96+
| select(.manifest_path | contains("/crates/"))
97+
| select(.version != $release_version)
98+
| "\(.name) \(.version)"' metadata.json
99+
} || true)"
72100
73-
if [[ "${CARGO_VERSION}" != "${RELEASE_VERSION}" ]]; then
74-
echo "::error::Version mismatch — bump version in Cargo.toml before releasing."
101+
if [[ -n "$mismatches" ]]; then
102+
echo "::error::These crates do not match tag version ${RELEASE_VERSION}:"
103+
echo "$mismatches"
75104
exit 1
76105
fi
77106
78-
- name: Check whether release already exists on crates.io
79-
id: crates-check
107+
- name: Build unpublished crate plan
108+
id: plan
80109
shell: bash
110+
env:
111+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
81112
run: |
82-
if curl -fsS \
83-
-H "User-Agent: smbcloud-cli-release-workflow" \
84-
"https://crates.io/api/v1/crates/smbcloud-cli/${RELEASE_VERSION}" \
85-
>/dev/null 2>&1; then
86-
echo "smbcloud-cli ${RELEASE_VERSION} already exists on crates.io, skipping publish"
87-
echo "exists=true" >> "$GITHUB_OUTPUT"
88-
else
89-
echo "exists=false" >> "$GITHUB_OUTPUT"
113+
if [[ -z "${CARGO_REGISTRY_TOKEN}" ]]; then
114+
echo "::error::CARGO_REGISTRY_TOKEN is required."
115+
exit 1
90116
fi
91117
92-
- name: Publish to crates.io
93-
if: steps.crates-check.outputs.exists != 'true'
118+
cargo workspaces plan --skip-published --json --token "${CARGO_REGISTRY_TOKEN}" > plan.json
119+
cat plan.json
120+
121+
plan_count="$(jq 'length' plan.json)"
122+
echo "count=${plan_count}" >> "$GITHUB_OUTPUT"
123+
124+
- name: Derive downstream workflow triggers
125+
id: downstream
126+
shell: bash
127+
run: |
128+
publish_cli_sdk_pypi="$(jq -r 'map(.name) | any(. == "smbcloud-auth-sdk-py" or . == "smbcloud-auth-sdk" or . == "smbcloud-model" or . == "smbcloud-network")' plan.json)"
129+
publish_cli_sdk_npm="$(jq -r 'map(.name) | any(. == "smbcloud-auth-sdk-wasm" or . == "smbcloud-auth-sdk" or . == "smbcloud-network" or . == "smbcloud-networking")' plan.json)"
130+
publish_cli_sdk_gem="$(jq -r 'map(.name) | any(. == "smbcloud-auth-sdk" or . == "smbcloud-model" or . == "smbcloud-network")' plan.json)"
131+
132+
echo "publish_cli_sdk_pypi=${publish_cli_sdk_pypi}" >> "$GITHUB_OUTPUT"
133+
echo "publish_cli_sdk_npm=${publish_cli_sdk_npm}" >> "$GITHUB_OUTPUT"
134+
echo "publish_cli_sdk_gem=${publish_cli_sdk_gem}" >> "$GITHUB_OUTPUT"
135+
136+
echo "SDK PyPI trigger: ${publish_cli_sdk_pypi}"
137+
echo "SDK npm trigger: ${publish_cli_sdk_npm}"
138+
echo "SDK gem trigger: ${publish_cli_sdk_gem}"
139+
140+
- name: Publish workspace crates to crates.io
141+
if: steps.plan.outputs.count != '0'
94142
shell: bash
95143
env:
96144
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
97-
run: cargo publish --locked --package smbcloud-cli
145+
run: |
146+
cargo workspaces publish \
147+
--publish-as-is \
148+
--yes \
149+
--locked \
150+
--allow-branch "*" \
151+
--publish-interval 15 \
152+
--token "${CARGO_REGISTRY_TOKEN}"
153+
154+
- name: Nothing to publish
155+
if: steps.plan.outputs.count == '0'
156+
run: echo "All workspace crate versions in this tag are already published."
157+
158+
dispatch-distributions:
159+
name: Trigger CLI distribution workflows
160+
needs: publish
161+
runs-on: ubuntu-latest
162+
163+
steps:
164+
- name: Trigger GitHub CLI release
165+
uses: actions/github-script@v7
166+
with:
167+
script: |
168+
await github.rest.actions.createWorkflowDispatch({
169+
owner: context.repo.owner,
170+
repo: context.repo.repo,
171+
workflow_id: 'release-github.yml',
172+
ref: context.payload.repository.default_branch,
173+
inputs: {
174+
tag: '${{ needs.publish.outputs.tag }}'
175+
}
176+
})
177+
178+
- name: Trigger npm CLI release
179+
uses: actions/github-script@v7
180+
with:
181+
script: |
182+
await github.rest.actions.createWorkflowDispatch({
183+
owner: context.repo.owner,
184+
repo: context.repo.repo,
185+
workflow_id: 'release-npm.yml',
186+
ref: context.payload.repository.default_branch,
187+
inputs: {
188+
tag: '${{ needs.publish.outputs.tag }}'
189+
}
190+
})
191+
192+
- name: Trigger NuGet CLI release
193+
uses: actions/github-script@v7
194+
with:
195+
script: |
196+
await github.rest.actions.createWorkflowDispatch({
197+
owner: context.repo.owner,
198+
repo: context.repo.repo,
199+
workflow_id: 'release-nuget.yml',
200+
ref: context.payload.repository.default_branch,
201+
inputs: {
202+
tag: '${{ needs.publish.outputs.tag }}'
203+
}
204+
})
205+
206+
- name: Trigger PyPI CLI release
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-pypi.yml',
214+
ref: context.payload.repository.default_branch,
215+
inputs: {
216+
tag: '${{ needs.publish.outputs.tag }}'
217+
}
218+
})
219+
220+
dispatch-sdk-pypi:
221+
name: Trigger SDK PyPI release
222+
needs: publish
223+
if: needs.publish.outputs.publish_cli_sdk_pypi == 'true'
224+
runs-on: ubuntu-latest
225+
226+
steps:
227+
- name: Trigger SDK PyPI release workflow
228+
uses: actions/github-script@v7
229+
with:
230+
script: |
231+
await github.rest.actions.createWorkflowDispatch({
232+
owner: context.repo.owner,
233+
repo: context.repo.repo,
234+
workflow_id: 'release-sdk-pypi.yml',
235+
ref: context.payload.repository.default_branch,
236+
inputs: {
237+
tag: '${{ needs.publish.outputs.tag }}'
238+
}
239+
})
240+
241+
dispatch-sdk-npm:
242+
name: Trigger SDK npm release
243+
needs: publish
244+
if: needs.publish.outputs.publish_cli_sdk_npm == 'true'
245+
runs-on: ubuntu-latest
246+
247+
steps:
248+
- name: Trigger SDK npm release workflow
249+
uses: actions/github-script@v7
250+
with:
251+
script: |
252+
await github.rest.actions.createWorkflowDispatch({
253+
owner: context.repo.owner,
254+
repo: context.repo.repo,
255+
workflow_id: 'release-sdk-npm.yml',
256+
ref: context.payload.repository.default_branch,
257+
inputs: {
258+
tag: '${{ needs.publish.outputs.tag }}'
259+
}
260+
})
261+
262+
dispatch-sdk-gem:
263+
name: Trigger SDK Ruby gem release
264+
needs: publish
265+
if: needs.publish.outputs.publish_cli_sdk_gem == 'true'
266+
runs-on: ubuntu-latest
267+
268+
steps:
269+
- name: Trigger SDK Ruby gem release workflow
270+
uses: actions/github-script@v7
271+
with:
272+
script: |
273+
await github.rest.actions.createWorkflowDispatch({
274+
owner: context.repo.owner,
275+
repo: context.repo.repo,
276+
workflow_id: 'release-sdk-gem.yml',
277+
ref: context.payload.repository.default_branch,
278+
inputs: {
279+
tag: '${{ needs.publish.outputs.tag }}'
280+
}
281+
})

.github/workflows/release-github.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
name: GitHub CLI Release
22

33
on:
4-
push:
5-
tags:
6-
- "v*.*.*"
74
workflow_dispatch:
85
inputs:
96
tag:
@@ -182,7 +179,7 @@ jobs:
182179
owner: context.repo.owner,
183180
repo: context.repo.repo,
184181
workflow_id: 'release-homebrew.yml',
185-
ref: 'main',
182+
ref: context.payload.repository.default_branch,
186183
inputs: {
187184
tag: '${{ steps.tag.outputs.tag }}'
188185
}

.github/workflows/release-homebrew.yml

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,22 @@ jobs:
3737
run: |
3838
mkdir -p artifacts
3939
40-
gh release download "${{ steps.release.outputs.tag }}" \
41-
--repo "${{ env.REPO }}" \
42-
--pattern "smb-macos-*.tar.gz.sha256" \
43-
--dir artifacts/
40+
for attempt in 1 2 3 4 5; do
41+
if gh release download "${{ steps.release.outputs.tag }}" \
42+
--repo "${{ env.REPO }}" \
43+
--pattern "smb-macos-*.tar.gz.sha256" \
44+
--dir artifacts/; then
45+
break
46+
fi
47+
48+
if [ "$attempt" -eq 5 ]; then
49+
echo "Failed to download Homebrew checksum artifacts from the GitHub release." >&2
50+
exit 1
51+
fi
52+
53+
echo "Release assets not visible yet. Retrying in 10 seconds..."
54+
sleep 10
55+
done
4456
4557
ARM64_SHA=$(cat artifacts/smb-macos-arm64.tar.gz.sha256)
4658
AMD64_SHA=$(cat artifacts/smb-macos-amd64.tar.gz.sha256)

.github/workflows/release-npm.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
name: NPM CLI Release
22

33
on:
4-
push:
5-
tags:
6-
- "v*.*.*"
74
workflow_dispatch:
85
inputs:
96
tag:

.github/workflows/release-nuget.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
name: NuGet CLI Release
22

33
on:
4-
push:
5-
tags:
6-
- "v*.*.*"
74
workflow_dispatch:
85
inputs:
96
tag:

.github/workflows/release-pypi.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
name: PyPI CLI Release
22

33
on:
4-
push:
5-
tags:
6-
- "v*.*.*"
74
workflow_dispatch:
85
inputs:
96
tag:

0 commit comments

Comments
 (0)