Skip to content

Commit cc08ed1

Browse files
donislawdevclaude
andauthored
ci: binaries on demand, from whatever branch you pick (#52)
Somebody reports a bug, the fix lands on a branch, and they want to try it before there is a release. Clicking Run workflow builds that branch and leaves the binaries on the run page for fourteen days. Three owner decisions shape it. A choice input, defaulting to the command line binaries only: those cross compile to five platforms on one runner in about two minutes, while the window needs three real runners and CGO. No test gate, because the whole point is speed and the branch has its own CI on its own pull request. And fourteen days rather than the default ninety, because an unsigned binary should not sit for a quarter of a year behind a link somebody can pass on as if it were official. It is deliberately not shaped like a release, so an archive from here cannot be mistaken for one. The name carries the COMMIT rather than the version, and that is not a style choice: internal/version is a const and cannot be stamped at link time, so a build from a fix branch reports whatever version that branch inherited. The file name is the only place that can tell the truth about which code this is. Every archive also carries UNOFFICIAL-BUILD.txt saying the same in words, for whoever unpacks it a month later with no memory of where it came from - not signed, no attestation, and what the version string inside does and does not mean. The note is a script rather than a heredoc because two jobs on four runners write it, and a note that says one thing in one archive and something else in the other is worse than no note. Three guards, five mutations, all caught. The platform list is the one fact this shares with the release, so it is read out of both workflows and compared. A workflow that quietly built four of five platforms would leave somebody's machine unserved, and nothing would say so, because a missing platform looks like a build that did not run. The other two are about the failure mode that matters here, which is a person trusting a file they should not: nothing in this workflow may be granted write, no step may mention a way to publish, and both packaging jobs must write the note - counted rather than found, because one job losing its call would leave the other one proving nothing about it. Measured rather than assumed: the command line loop was run locally against this tree and packaged all five targets in 26 seconds, and the archive holds the binary, the licence, the notices, the readme and the note. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 08ac79d commit cc08ed1

3 files changed

Lines changed: 493 additions & 0 deletions

File tree

‎.github/scripts/unofficial_note.sh‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env bash
2+
# Write the note that travels inside every build-on-demand archive.
3+
#
4+
# Why a script rather than a heredoc in the workflow. The same sentences go into
5+
# the command line archives and the window archives, built by two different jobs
6+
# on four different runners, and a note that says one thing in one archive and
7+
# something else in the other is worse than no note. One file, called twice.
8+
#
9+
# It is also the only thing in the archive that can be honest about which code
10+
# this is. internal/version is a Go const, so it cannot be stamped at link time
11+
# and a build from a fix branch reports whatever version that branch inherited.
12+
# The commit below is the fact - the version string inside the binary is not.
13+
#
14+
# Usage: unofficial_note.sh <output path> <short commit>
15+
set -euo pipefail
16+
17+
out="${1:?first argument is the file to write}"
18+
commit="${2:?second argument is the short commit}"
19+
20+
repo="${GITHUB_REPOSITORY:-donislawdev/TestingFilesGenerator}"
21+
ref="${GITHUB_REF_NAME:-unknown branch}"
22+
run="${GITHUB_RUN_ID:-}"
23+
built="$(date -u '+%Y-%m-%d %H:%M UTC')"
24+
25+
{
26+
echo "UNOFFICIAL BUILD - this is not a release"
27+
echo "========================================"
28+
echo
29+
echo "Built on demand from commit ${commit} of ${ref}, on ${built}."
30+
if [ -n "${run}" ]; then
31+
echo "Run: https://github.com/${repo}/actions/runs/${run}"
32+
fi
33+
echo
34+
echo "What this is. Somebody asked for a build of work that has not been"
35+
echo "released yet - usually a fix for something they reported. It is the code"
36+
echo "at the commit above and nothing more."
37+
echo
38+
echo "What it is NOT."
39+
echo
40+
echo " - It is NOT signed. There is no Windows code signing signature and no"
41+
echo " Apple notarisation. Windows SmartScreen and macOS Gatekeeper will"
42+
echo " both object to it, and they are right to."
43+
echo " - It carries NO provenance attestation and NO bill of materials."
44+
echo " A real release carries both and you can verify them."
45+
echo " - The version it reports is NOT a claim to be that release. The"
46+
echo " version is compiled in as a constant, so a build from a branch"
47+
echo " reports the version that branch started from. The commit above is"
48+
echo " the only thing that identifies this build."
49+
echo
50+
echo "Do not pass this on as a release, and do not keep it once the fix ships."
51+
echo "Releases live at https://github.com/${repo}/releases - they are signed,"
52+
echo "they carry checksums you can check, and they say which version they are."
53+
} > "${out}"

‎.github/workflows/dev-build.yml‎

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# Binaries on demand, built from whatever branch you pick.
2+
#
3+
# What it is for. Somebody reports a bug, the fix lands on a branch, and they
4+
# want to try it before there is a release. Clicking Run workflow here builds
5+
# that branch and leaves the binaries on the run page for fourteen days.
6+
#
7+
# What it is NOT. Not a release and it must never be mistaken for one. These
8+
# binaries are UNSIGNED - no code signing certificate on Windows, no Apple
9+
# notarisation, no provenance attestation, no bill of materials. Windows
10+
# SmartScreen and macOS Gatekeeper will both object, and that is correct
11+
# behaviour rather than a fault to work around. Releases are made by release.yml
12+
# from a tag, signed on two machines, and published by a person.
13+
#
14+
# Three things are deliberately different from a release, so that an archive
15+
# from here cannot be passed off as one:
16+
#
17+
# - The name carries the COMMIT, not the version. internal/version is a const
18+
# and cannot be stamped at link time, so a build from a fix branch says
19+
# 0.3.0-rc1 inside whatever it really is. The file name is the only place
20+
# that can tell the truth about which code this is, so it says the commit.
21+
# - Every archive carries UNOFFICIAL-BUILD.txt, which says the same in words
22+
# for whoever unpacks it a month later with no memory of where it came from.
23+
# - It has read only permissions and no publishing step at all, so it cannot
24+
# put anything on a release page even by accident.
25+
#
26+
# The test suite is deliberately NOT run first, decided by the owner: the whole
27+
# point is a binary in two minutes, the branch has its own CI on its own pull
28+
# request, and the note inside names the commit so anybody can go and read what
29+
# CI said about it.
30+
name: Build on demand
31+
32+
run-name: "dev build (${{ inputs.what }}) from ${{ github.ref_name }}"
33+
34+
on:
35+
workflow_dispatch:
36+
inputs:
37+
what:
38+
description: "Which binaries to build"
39+
type: choice
40+
default: cli
41+
options:
42+
- cli
43+
- gui
44+
- both
45+
46+
permissions:
47+
contents: read
48+
49+
concurrency:
50+
group: dev-build-${{ github.ref }}
51+
cancel-in-progress: true
52+
53+
env:
54+
GO_VERSION: "1.27.0"
55+
# Fourteen days rather than the default ninety. These are throwaway builds
56+
# handed to one person, and an unsigned binary should not sit for a quarter of
57+
# a year behind a link somebody can pass on as if it were official.
58+
KEEP_DAYS: "14"
59+
60+
jobs:
61+
cli:
62+
name: command line binaries
63+
if: inputs.what == 'cli' || inputs.what == 'both'
64+
runs-on: ubuntu-latest
65+
timeout-minutes: 30
66+
env:
67+
# Same as the release: no C and no toolkit in the command line binary, so
68+
# one runner cross compiles every target.
69+
CGO_ENABLED: "0"
70+
steps:
71+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
72+
73+
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
74+
with:
75+
go-version: ${{ env.GO_VERSION }}
76+
77+
- name: build and package every target
78+
run: |
79+
set -euo pipefail
80+
short="$(git rev-parse --short HEAD)"
81+
mkdir -p dist
82+
83+
# darwin is what the compiler is told, macos is what a person reading
84+
# a download recognises. Same rename as the release makes.
85+
friendly() {
86+
case "$1" in
87+
darwin) echo "macos" ;;
88+
*) echo "$1" ;;
89+
esac
90+
}
91+
92+
# The same platforms the release builds, and a guard holds the two
93+
# lists together - a fix nobody can get for their machine is not a fix.
94+
for target in \
95+
windows/amd64 windows/arm64 \
96+
linux/amd64 linux/arm64 \
97+
darwin/arm64
98+
do
99+
os="${target%/*}"
100+
arch="${target#*/}"
101+
label="$(friendly "$os")"
102+
103+
work="$(mktemp -d)"
104+
binary="tfg"
105+
if [ "$os" = "windows" ]; then
106+
binary="tfg.exe"
107+
fi
108+
109+
GOOS="$os" GOARCH="$arch" go build -tags "$(cat .github/build-tags)" -trimpath -o "${work}/${binary}" ./cmd/tfg
110+
111+
cp LICENSE THIRD-PARTY-NOTICES.md README.md "${work}/"
112+
.github/scripts/unofficial_note.sh "${work}/UNOFFICIAL-BUILD.txt" "${short}"
113+
114+
base="tfg_dev-${short}_${label}_${arch}"
115+
if [ "$os" = "windows" ]; then
116+
(cd "${work}" && zip -q -r "${GITHUB_WORKSPACE}/dist/${base}.zip" .)
117+
else
118+
tar -czf "dist/${base}.tar.gz" -C "${work}" .
119+
fi
120+
echo "packaged ${base}"
121+
done
122+
123+
ls -l dist
124+
125+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
126+
with:
127+
name: unofficial-cli
128+
path: dist/*
129+
if-no-files-found: error
130+
retention-days: 14
131+
132+
gui:
133+
name: window binary on ${{ matrix.os }}
134+
if: inputs.what == 'gui' || inputs.what == 'both'
135+
runs-on: ${{ matrix.os }}
136+
timeout-minutes: 60
137+
strategy:
138+
# One system failing should not throw away the binaries that did build.
139+
# Somebody waiting for a Windows build does not care that the Mac runner
140+
# was busy.
141+
fail-fast: false
142+
matrix:
143+
os:
144+
- windows-latest
145+
- ubuntu-latest
146+
- macos-latest
147+
env:
148+
# The window reaches OpenGL through C, so this one cannot be cross
149+
# compiled the way the command line binary is.
150+
CGO_ENABLED: "1"
151+
defaults:
152+
run:
153+
shell: bash
154+
steps:
155+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
156+
157+
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
158+
with:
159+
go-version: ${{ env.GO_VERSION }}
160+
161+
- name: graphics and windowing headers
162+
if: runner.os == 'Linux'
163+
# Taken from the toolkit's own CI. No GitHub runner carries these by
164+
# default, and without them the toolkit's app package does not compile.
165+
run: |
166+
set -euo pipefail
167+
sudo apt-get update
168+
sudo apt-get install -y --no-install-recommends \
169+
libgl1-mesa-dev \
170+
libwayland-dev \
171+
libx11-dev \
172+
libxkbcommon-dev \
173+
xorg-dev
174+
175+
- name: build and package
176+
run: |
177+
set -euo pipefail
178+
short="$(git rev-parse --short HEAD)"
179+
os="$(go env GOOS)"
180+
arch="$(go env GOARCH)"
181+
label="$os"
182+
if [ "$os" = "darwin" ]; then
183+
label="macos"
184+
fi
185+
work="$(mktemp -d)"
186+
mkdir -p dist
187+
188+
if [ "$os" = "windows" ]; then
189+
# The linker flags come from the file and nowhere else, so a build
190+
# from here and a release cannot drift. Without them Windows hangs a
191+
# black console window behind the program.
192+
go build -tags "$(cat .github/build-tags)" -trimpath -ldflags="$(cat .github/gui-ldflags)" \
193+
-o "${work}/tfg-gui.exe" ./cmd/tfg-gui
194+
else
195+
go build -tags "$(cat .github/build-tags)" -trimpath -o "${work}/tfg-gui" ./cmd/tfg-gui
196+
fi
197+
198+
# A bundle on macOS even though nothing here is signed. Without one
199+
# the Finder has no icon to draw and the program behaves like a
200+
# terminal tool, which makes it useless for the person most likely to
201+
# be reporting a window bug in the first place.
202+
if [ "$os" = "darwin" ]; then
203+
.github/scripts/make_app_bundle.sh \
204+
"${work}" "tfg-gui" "com.donislawdev.tfg-gui" "dev-${short}"
205+
fi
206+
207+
cp LICENSE THIRD-PARTY-NOTICES.md README.md "${work}/"
208+
.github/scripts/unofficial_note.sh "${work}/UNOFFICIAL-BUILD.txt" "${short}"
209+
210+
base="tfg-gui_dev-${short}_${label}_${arch}"
211+
if [ "$os" = "windows" ]; then
212+
(cd "${work}" && 7z a -tzip -bso0 "${GITHUB_WORKSPACE}/dist/${base}.zip" .)
213+
else
214+
tar -czf "dist/${base}.tar.gz" -C "${work}" .
215+
fi
216+
echo "packaged ${base}"
217+
ls -l dist
218+
219+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
220+
with:
221+
name: unofficial-gui-${{ matrix.os }}
222+
path: dist/*
223+
if-no-files-found: error
224+
retention-days: 14

0 commit comments

Comments
 (0)