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: Package binary
92 shell: bash
93 run: |
94 BIN_SUFFIX=""
95 if [[ "${{ matrix.runner }}" == "windows-latest" ]]; then
96 BIN_SUFFIX=".exe"
97 fi
98
99 BIN_OUTPUT="target/${{ matrix.target }}/release/${PROJECT_NAME}${BIN_SUFFIX}"
100 BIN_RELEASE="${PROJECT_NAME}-${{ matrix.name }}${BIN_SUFFIX}"
101
102 mkdir -p ./release
103 mv "${BIN_OUTPUT}" "./release/${BIN_RELEASE}"
104
105 - name: Package macOS tarball for Homebrew
106 if: contains(matrix.target, 'apple-darwin')
107 shell: bash
108 run: |
109 ARCHIVE_NAME="${PROJECT_NAME}-${{ matrix.name }}.tar.gz"
110
111 mkdir -p staging-brew
112 cp "./release/${PROJECT_NAME}-${{ matrix.name }}" "staging-brew/${PROJECT_NAME}"
113 strip "staging-brew/${PROJECT_NAME}"
114
115 tar -C staging-brew -czf "${ARCHIVE_NAME}" "${PROJECT_NAME}"
116
117 shasum -a 256 "${ARCHIVE_NAME}" | awk '{print $1}' > "${ARCHIVE_NAME}.sha256"
118
119 echo "Archive: ${ARCHIVE_NAME}"
120 echo "SHA256: $(cat "${ARCHIVE_NAME}.sha256")"
121
122 # Homebrew has always had a checksum because the tap needs one. Scoop
123 # and winget each need one too, and they consume the raw Windows
124 # binaries rather than the macOS tarball, so every asset gets a
125 # sidecar. Windows runners use bash from Git for Windows, which has
126 # sha256sum; macOS only has shasum.
127 - name: Checksum release assets
128 shell: bash
129 run: |
130 cd release
131 for asset in *; do
132 case "${asset}" in *.sha256) continue ;; esac
133
134 if command -v sha256sum >/dev/null 2>&1; then
135 sha256sum "${asset}" | awk '{print $1}' > "${asset}.sha256"
136 else
137 shasum -a 256 "${asset}" | awk '{print $1}' > "${asset}.sha256"
138 fi
139
140 echo "${asset}: $(cat "${asset}.sha256")"
141 done
142
143 - name: Upload binary artifact
144 uses: actions/upload-artifact@v7
145 with:
146 name: binary-${{ matrix.name }}
147 path: release/*
148
149 - name: Upload macOS Homebrew artifacts
150 if: contains(matrix.target, 'apple-darwin')
151 uses: actions/upload-artifact@v7
152 with:
153 name: homebrew-${{ matrix.name }}
154 path: |
155 ${{ env.PROJECT_NAME }}-${{ matrix.name }}.tar.gz
156 ${{ env.PROJECT_NAME }}-${{ matrix.name }}.tar.gz.sha256
157
158 release:
159 name: Create GitHub Release
160 needs: build
161 runs-on: ubuntu-latest
162 if: github.ref_type == 'tag' || github.event_name == 'workflow_dispatch'
163
164 steps:
165 - name: Resolve tag
166 id: tag
167 shell: bash
168 run: |
169 if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
170 echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
171 else
172 echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
173 fi
174
175 - name: Download binary artifacts
176 uses: actions/download-artifact@v7
177 with:
178 pattern: "binary-*"
179 path: release
180 merge-multiple: true
181
182 - name: Download Homebrew artifacts
183 uses: actions/download-artifact@v7
184 with:
185 pattern: "homebrew-*"
186 path: release
187 merge-multiple: true
188
189 - name: Release
190 uses: softprops/action-gh-release@v2
191 with:
192 tag_name: ${{ steps.tag.outputs.tag }}
193 files: release/*
194 # Build the notes from the PRs since the previous tag, the same
195 # format as the "Generate release notes" button on the web UI, so
196 # nobody has to fill the release body in by hand after the fact.
197 generate_release_notes: true
198
199 # These channels all publish somewhere outside this repo (a tap, a
200 # Scoop bucket, microsoft/winget-pkgs) and every one of them reads
201 # checksums off the release created above, so they hang off this job
202 # rather than firing on the tag directly.
203 - name: Trigger OS package manager releases
204 uses: actions/github-script@v8
205 with:
206 script: |
207 const tag = '${{ steps.tag.outputs.tag }}'
208 const workflows = [
209 'release-homebrew.yml',
210 'release-scoop.yml',
211 'release-winget.yml',
212 ]
213
214 for (const workflow_id of workflows) {
215 try {
216 await github.rest.actions.createWorkflowDispatch({
217 owner: context.repo.owner,
218 repo: context.repo.repo,
219 workflow_id,
220 ref: tag,
221 inputs: { tag },
222 })
223 console.log(`Dispatched ${workflow_id} for tag ${tag}`)
224 } catch (error) {
225 // One packaging channel being unconfigured (a missing secret,
226 // a bucket repo that does not exist yet) should not take the
227 // rest of the fan-out down with it.
228 core.warning(`Failed to dispatch ${workflow_id}: ${error.message}`)
229 }
230 }