Skip to content

Commit 2f0bec3

Browse files
committed
Require asset-backed releases
1 parent 4f4911f commit 2f0bec3

3 files changed

Lines changed: 327 additions & 39 deletions

File tree

.github/actions/build-binary/action.yml

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,29 +112,66 @@ runs:
112112
set -euo pipefail
113113
dist_dir="dist"
114114
base_name="${{ inputs.artifact-name }}"
115+
package_dir="${dist_dir}/${base_name}"
115116
bin_dir="target/${{ inputs.target }}/release"
116117
bin_suffix=""
117118
if [[ "${RUNNER_OS}" == "Windows" ]]; then
118119
bin_suffix=".exe"
119120
fi
120-
mkdir -p "${dist_dir}"
121-
cp "${bin_dir}/slipstream-client${bin_suffix}" "${dist_dir}/"
122-
cp "${bin_dir}/slipstream-server${bin_suffix}" "${dist_dir}/"
123-
if command -v sha256sum >/dev/null 2>&1; then
124-
(cd "${dist_dir}" && sha256sum "slipstream-client${bin_suffix}" "slipstream-server${bin_suffix}" > "${base_name}.sha256")
125-
else
126-
(cd "${dist_dir}" && shasum -a 256 "slipstream-client${bin_suffix}" "slipstream-server${bin_suffix}" > "${base_name}.sha256")
127-
fi
121+
rm -rf "${package_dir}"
122+
mkdir -p "${package_dir}"
123+
cp "${bin_dir}/slipstream-client${bin_suffix}" "${package_dir}/"
124+
cp "${bin_dir}/slipstream-server${bin_suffix}" "${package_dir}/"
128125
129126
- name: Verify Windows artifact dependencies
130127
if: runner.os == 'Windows'
131128
shell: pwsh
132-
run: ./scripts/verify_windows_artifact_deps.ps1 -DistDir dist -TargetPlatform "${{ inputs.windows-platform }}"
129+
run: ./scripts/verify_windows_artifact_deps.ps1 -DistDir "dist/${{ inputs.artifact-name }}" -TargetPlatform "${{ inputs.windows-platform }}"
130+
131+
- name: Create binary archive (Unix)
132+
if: runner.os != 'Windows'
133+
shell: bash
134+
run: |
135+
set -euo pipefail
136+
tar -C dist -czf "dist/${{ inputs.artifact-name }}.tar.gz" "${{ inputs.artifact-name }}"
137+
138+
- name: Create binary archive (Windows)
139+
if: runner.os == 'Windows'
140+
shell: pwsh
141+
run: |
142+
$baseName = "${{ inputs.artifact-name }}"
143+
$packageDir = Join-Path "dist" $baseName
144+
$archivePath = Join-Path "dist" "$baseName.zip"
145+
Compress-Archive -Path (Join-Path $packageDir "*") -DestinationPath $archivePath -Force
146+
147+
- name: Checksum binary archive
148+
shell: bash
149+
run: |
150+
set -euo pipefail
151+
dist_dir="dist"
152+
base_name="${{ inputs.artifact-name }}"
153+
found=()
154+
for archive in "${dist_dir}/${base_name}.tar.gz" "${dist_dir}/${base_name}.zip"; do
155+
if [[ -f "${archive}" ]]; then
156+
found+=("${archive}")
157+
fi
158+
done
159+
if [[ "${#found[@]}" -ne 1 ]]; then
160+
echo "Expected exactly one archive for ${base_name}; found ${#found[@]}" >&2
161+
exit 1
162+
fi
163+
archive_name="$(basename "${found[0]}")"
164+
if command -v sha256sum >/dev/null 2>&1; then
165+
(cd "${dist_dir}" && sha256sum "${archive_name}" > "${base_name}.sha256")
166+
else
167+
(cd "${dist_dir}" && shasum -a 256 "${archive_name}" > "${base_name}.sha256")
168+
fi
169+
rm -rf "${dist_dir}/${base_name}"
133170
134171
- name: Upload binaries
135172
uses: actions/upload-artifact@v7
136173
with:
137174
name: ${{ inputs.artifact-name }}
138175
path: |
139-
dist/*
176+
dist/${{ inputs.artifact-name }}.*
140177
if-no-files-found: error

.github/workflows/release.yml

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: Release tag to create, for example v0.1.0.
8+
required: true
9+
type: string
10+
target:
11+
description: Exact 40-character commit SHA to release.
12+
required: true
13+
type: string
14+
15+
permissions:
16+
contents: read
17+
18+
jobs:
19+
validate:
20+
name: Validate release inputs
21+
runs-on: ubuntu-24.04
22+
timeout-minutes: 10
23+
outputs:
24+
version: ${{ steps.validate.outputs.version }}
25+
target: ${{ steps.validate.outputs.target }}
26+
steps:
27+
- name: Check out release target
28+
uses: actions/checkout@v6
29+
with:
30+
ref: ${{ inputs.target }}
31+
submodules: recursive
32+
33+
- name: Validate release inputs
34+
id: validate
35+
shell: bash
36+
env:
37+
GH_TOKEN: ${{ github.token }}
38+
run: |
39+
set -euo pipefail
40+
41+
version="${{ inputs.version }}"
42+
target="${{ inputs.target }}"
43+
44+
if [[ ! "${version}" =~ ^v[0-9]+[.][0-9]+[.][0-9]+([-.][0-9A-Za-z.-]+)?$ ]]; then
45+
echo "Release version must look like vX.Y.Z: ${version}" >&2
46+
exit 1
47+
fi
48+
if [[ ! "${target}" =~ ^[0-9a-fA-F]{40}$ ]]; then
49+
echo "Release target must be a 40-character commit SHA: ${target}" >&2
50+
exit 1
51+
fi
52+
53+
checked_out="$(git rev-parse HEAD)"
54+
if [[ "${checked_out}" != "${target}" ]]; then
55+
echo "Checked out ${checked_out}, expected ${target}" >&2
56+
exit 1
57+
fi
58+
if git ls-remote --exit-code --tags origin "refs/tags/${version}" >/dev/null 2>&1; then
59+
echo "Release tag already exists on origin: ${version}" >&2
60+
exit 1
61+
fi
62+
if gh release view "${version}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
63+
echo "Release already exists: ${version}" >&2
64+
exit 1
65+
fi
66+
67+
awk -v heading="## ${version} " '
68+
index($0, heading) == 1 { found = 1; next }
69+
found && /^## / { exit }
70+
found { print }
71+
END { if (!found) exit 1 }
72+
' CHANGELOG.md > release-notes.md
73+
if [[ ! -s release-notes.md ]]; then
74+
echo "CHANGELOG.md does not contain non-empty notes for ${version}" >&2
75+
exit 1
76+
fi
77+
78+
{
79+
echo "version=${version}"
80+
echo "target=${target}"
81+
} >> "${GITHUB_OUTPUT}"
82+
83+
- name: Upload release notes
84+
uses: actions/upload-artifact@v7
85+
with:
86+
name: release-notes
87+
path: release-notes.md
88+
if-no-files-found: error
89+
90+
build-binaries:
91+
name: Build Binaries (${{ matrix.name }})
92+
needs: validate
93+
runs-on: ${{ matrix.runner }}
94+
timeout-minutes: 40
95+
strategy:
96+
fail-fast: false
97+
matrix:
98+
include:
99+
- name: macos-arm64
100+
runner: macos-26
101+
target: aarch64-apple-darwin
102+
artifact: slipstream-macos-arm64
103+
windows_platform: x64
104+
vcpkg_triplet: ""
105+
- name: macos-x86_64
106+
runner: macos-15-intel
107+
target: x86_64-apple-darwin
108+
artifact: slipstream-macos-x86_64
109+
windows_platform: x64
110+
vcpkg_triplet: ""
111+
- name: linux-x86_64
112+
runner: ubuntu-24.04
113+
target: x86_64-unknown-linux-gnu
114+
artifact: slipstream-linux-x86_64
115+
windows_platform: x64
116+
vcpkg_triplet: ""
117+
- name: linux-arm64
118+
runner: ubuntu-24.04-arm
119+
target: aarch64-unknown-linux-gnu
120+
artifact: slipstream-linux-arm64
121+
windows_platform: x64
122+
vcpkg_triplet: ""
123+
- name: windows-x86_64
124+
runner: windows-2025
125+
target: x86_64-pc-windows-msvc
126+
artifact: slipstream-windows-x86_64
127+
windows_platform: x64
128+
vcpkg_triplet: x64-windows-static-md
129+
- name: windows-arm64
130+
runner: windows-11-arm
131+
target: aarch64-pc-windows-msvc
132+
artifact: slipstream-windows-arm64
133+
windows_platform: ARM64
134+
vcpkg_triplet: arm64-windows-static-md
135+
steps:
136+
- name: Check out release target
137+
uses: actions/checkout@v6
138+
with:
139+
ref: ${{ needs.validate.outputs.target }}
140+
submodules: recursive
141+
142+
- name: Build binary artifact
143+
uses: ./.github/actions/build-binary
144+
with:
145+
target: ${{ matrix.target }}
146+
artifact-name: ${{ matrix.artifact }}
147+
windows-platform: ${{ matrix.windows_platform }}
148+
vcpkg-triplet: ${{ matrix.vcpkg_triplet }}
149+
150+
publish:
151+
name: Publish immutable release
152+
needs:
153+
- validate
154+
- build-binaries
155+
runs-on: ubuntu-24.04
156+
timeout-minutes: 20
157+
permissions:
158+
contents: write
159+
steps:
160+
- name: Download release artifacts
161+
uses: actions/download-artifact@v7
162+
with:
163+
path: release-assets
164+
165+
- name: Create draft, attach assets, and publish
166+
shell: bash
167+
env:
168+
GH_TOKEN: ${{ github.token }}
169+
run: |
170+
set -euo pipefail
171+
172+
version="${{ needs.validate.outputs.version }}"
173+
target="${{ needs.validate.outputs.target }}"
174+
artifacts=(
175+
slipstream-macos-arm64
176+
slipstream-macos-x86_64
177+
slipstream-linux-x86_64
178+
slipstream-linux-arm64
179+
slipstream-windows-x86_64
180+
slipstream-windows-arm64
181+
)
182+
asset_paths=()
183+
expected_assets=()
184+
185+
for artifact in "${artifacts[@]}"; do
186+
extension="tar.gz"
187+
if [[ "${artifact}" == slipstream-windows-* ]]; then
188+
extension="zip"
189+
fi
190+
archive="${artifact}.${extension}"
191+
checksum="${artifact}.sha256"
192+
for asset in "${archive}" "${checksum}"; do
193+
path="release-assets/${artifact}/${asset}"
194+
if [[ ! -f "${path}" ]]; then
195+
echo "Missing required release asset: ${path}" >&2
196+
exit 1
197+
fi
198+
asset_paths+=("${path}")
199+
expected_assets+=("${asset}")
200+
done
201+
done
202+
203+
notes_file="release-assets/release-notes/release-notes.md"
204+
if [[ ! -s "${notes_file}" ]]; then
205+
echo "Missing release notes artifact: ${notes_file}" >&2
206+
exit 1
207+
fi
208+
209+
immutable_enabled="$(gh api "repos/${GITHUB_REPOSITORY}/immutable-releases" -H X-GitHub-Api-Version:2026-03-10 --jq '.enabled')"
210+
if [[ "${immutable_enabled}" != "true" ]]; then
211+
echo "Repository immutable releases are not enabled." >&2
212+
exit 1
213+
fi
214+
215+
gh release create "${version}" \
216+
--repo "${GITHUB_REPOSITORY}" \
217+
--target "${target}" \
218+
--title "${version}" \
219+
--notes-file "${notes_file}" \
220+
--draft
221+
gh release upload "${version}" "${asset_paths[@]}" --repo "${GITHUB_REPOSITORY}"
222+
223+
remote_assets="$(gh release view "${version}" --repo "${GITHUB_REPOSITORY}" --json assets --jq '.assets[].name')"
224+
for asset in "${expected_assets[@]}"; do
225+
if ! grep -qxF "${asset}" <<< "${remote_assets}"; then
226+
echo "Draft release is missing uploaded asset: ${asset}" >&2
227+
exit 1
228+
fi
229+
done
230+
231+
gh release edit "${version}" --repo "${GITHUB_REPOSITORY}" --draft=false
232+
233+
immutable="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${version}" -H X-GitHub-Api-Version:2026-03-10 --jq '.immutable')"
234+
if [[ "${immutable}" != "true" ]]; then
235+
echo "Published release is not immutable." >&2
236+
exit 1
237+
fi

0 commit comments

Comments
 (0)