1 name: GitHub CLI Release
2
3 on:
4 workflow_dispatch:
5 inputs:
6 tag:
7 description: "Release tag (e.g. v0.3.39)"
8 required: true
9
10 permissions:
11 contents: write
12 actions: write
13
14 env:
15 PROJECT_NAME: smb
16 CARGO_TERM_COLOR: always
17 CLI_CLIENT_SECRET: ${{ secrets.CLI_CLIENT_SECRET }}
18
19 jobs:
20 build:
21 name: Build binary (${{ matrix.name }})
22 runs-on: ${{ matrix.runner }}
23
24 strategy:
25 fail-fast: false
26 matrix:
27 include:
28 - name: linux-amd64
29 runner: ubuntu-latest
30 target: x86_64-unknown-linux-gnu
31 - name: linux-arm64
32 runner: ubuntu-24.04-arm
33 target: aarch64-unknown-linux-gnu
34 - name: win-amd64
35 runner: windows-latest
36 target: x86_64-pc-windows-msvc
37 - name: win-arm64
38 runner: windows-latest
39 target: aarch64-pc-windows-msvc
40 - name: macos-amd64
41 runner: macos-15-intel
42 target: x86_64-apple-darwin
43 - name: macos-arm64
44 runner: macos-latest
45 target: aarch64-apple-darwin
46
47 steps:
48 - name: Checkout
49 uses: actions/checkout@v6
50 with:
51 ref: ${{ github.event.inputs.tag || github.ref }}
52
53 - name: Set the release version
54 shell: bash
55 run: |
56 if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
57 release_version="${GITHUB_REF_NAME#v}"
58 else
59 release_version="${{ github.event.inputs.tag }}"
60 release_version="${release_version#v}"
61 fi
62
63 echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
64
65 - name: Read Rust toolchain
66 shell: bash
67 run: |
68 rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
69 if [ -z "$rust_toolchain" ]; then
70 echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
71 exit 1
72 fi
73
74 echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
75
76 - name: Install toolkit
77 uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
78 with:
79 toolchain: ${{ env.RUST_TOOLCHAIN }}
80 targets: ${{ matrix.target }}
81
82 - name: Setup Rust cache
83 uses: Swatinem/rust-cache@v2
84 with:
85 key: github-${{ matrix.target }}
86
87 - name: Build binary
88 shell: bash
89 run: cargo build --locked --release --target ${{ matrix.target }} --package smbcloud-cli
90
91 - name: Check Windows binary has no VC++ runtime dependency
92 if: contains(matrix.target, 'windows')
93 shell: pwsh
94 run: ./packaging/windows/check-static-crt.ps1 -Path target/${{ matrix.target }}/release/${{ env.PROJECT_NAME }}.exe
95
96 - name: Package binary
97 shell: bash
98 run: |
99 BIN_SUFFIX=""
100 if [[ "${{ matrix.runner }}" == "windows-latest" ]]; then
101 BIN_SUFFIX=".exe"
102 fi
103
104 BIN_OUTPUT="target/${{ matrix.target }}/release/${PROJECT_NAME}${BIN_SUFFIX}"
105 BIN_RELEASE="${PROJECT_NAME}-${{ matrix.name }}${BIN_SUFFIX}"
106
107 mkdir -p ./release
108 mv "${BIN_OUTPUT}" "./release/${BIN_RELEASE}"
109
110 - name: Package macOS tarball for Homebrew
111 if: contains(matrix.target, 'apple-darwin')
112 shell: bash
113 run: |
114 ARCHIVE_NAME="${PROJECT_NAME}-${{ matrix.name }}.tar.gz"
115
116 mkdir -p staging-brew
117 cp "./release/${PROJECT_NAME}-${{ matrix.name }}" "staging-brew/${PROJECT_NAME}"
118 strip "staging-brew/${PROJECT_NAME}"
119
120 tar -C staging-brew -czf "${ARCHIVE_NAME}" "${PROJECT_NAME}"
121
122 shasum -a 256 "${ARCHIVE_NAME}" | awk '{print $1}' > "${ARCHIVE_NAME}.sha256"
123
124 echo "Archive: ${ARCHIVE_NAME}"
125 echo "SHA256: $(cat "${ARCHIVE_NAME}.sha256")"
126
127 # Homebrew has always had a checksum because the tap needs one. Scoop
128 # and winget each need one too, and they consume the raw Windows
129 # binaries rather than the macOS tarball, so every asset gets a
130 # sidecar. Windows runners use bash from Git for Windows, which has
131 # sha256sum; macOS only has shasum.
132 - name: Checksum release assets
133 shell: bash
134 run: |
135 cd release
136 for asset in *; do
137 case "${asset}" in *.sha256) continue ;; esac
138
139 if command -v sha256sum >/dev/null 2>&1; then
140 sha256sum "${asset}" | awk '{print $1}' > "${asset}.sha256"
141 else
142 shasum -a 256 "${asset}" | awk '{print $1}' > "${asset}.sha256"
143 fi
144
145 echo "${asset}: $(cat "${asset}.sha256")"
146 done
147
148 - name: Upload binary artifact
149 uses: actions/upload-artifact@v7
150 with:
151 name: binary-${{ matrix.name }}
152 path: release/*
153
154 - name: Upload macOS Homebrew artifacts
155 if: contains(matrix.target, 'apple-darwin')
156 uses: actions/upload-artifact@v7
157 with:
158 name: homebrew-${{ matrix.name }}
159 path: |
160 ${{ env.PROJECT_NAME }}-${{ matrix.name }}.tar.gz
161 ${{ env.PROJECT_NAME }}-${{ matrix.name }}.tar.gz.sha256
162
163 release:
164 name: Create GitHub Release
165 needs: build
166 runs-on: ubuntu-latest
167 if: github.ref_type == 'tag' || github.event_name == 'workflow_dispatch'
168
169 steps:
170 - name: Resolve tag
171 id: tag
172 shell: bash
173 run: |
174 if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
175 echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
176 else
177 echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
178 fi
179
180 - name: Download binary artifacts
181 uses: actions/download-artifact@v7
182 with:
183 pattern: "binary-*"
184 path: release
185 merge-multiple: true
186
187 - name: Download Homebrew artifacts
188 uses: actions/download-artifact@v7
189 with:
190 pattern: "homebrew-*"
191 path: release
192 merge-multiple: true
193
194 - name: Release
195 uses: softprops/action-gh-release@v2
196 with:
197 tag_name: ${{ steps.tag.outputs.tag }}
198 files: release/*
199 # Build the notes from the PRs since the previous tag, the same
200 # format as the "Generate release notes" button on the web UI, so
201 # nobody has to fill the release body in by hand after the fact.
202 generate_release_notes: true
203
204 # These channels all publish somewhere outside this repo (a tap, a
205 # Scoop bucket, microsoft/winget-pkgs) and every one of them reads
206 # checksums off the release created above, so they hang off this job
207 # rather than firing on the tag directly.
208 - name: Trigger OS package manager releases
209 uses: actions/github-script@v8
210 with:
211 script: |
212 const tag = '${{ steps.tag.outputs.tag }}'
213 const workflows = [
214 'release-homebrew.yml',
215 'release-scoop.yml',
216 'release-winget.yml',
217 ]
218
219 for (const workflow_id of workflows) {
220 try {
221 await github.rest.actions.createWorkflowDispatch({
222 owner: context.repo.owner,
223 repo: context.repo.repo,
224 workflow_id,
225 ref: tag,
226 inputs: { tag },
227 })
228 console.log(`Dispatched ${workflow_id} for tag ${tag}`)
229 } catch (error) {
230 // One packaging channel being unconfigured (a missing secret,
231 // a bucket repo that does not exist yet) should not take the
232 // rest of the fan-out down with it.
233 core.warning(`Failed to dispatch ${workflow_id}: ${error.message}`)
234 }
235 }