Skip to content

Commit 26714cb

Browse files
hsyl20claude
andcommitted
Cut releases from CI and publish the ghcup channel from a branch
Set up how releases are made. Nothing has been released yet; the repo has no tags. A release is cut from Actions -> CI -> Run workflow with release_version set to the version to release and `publish` ticked. CI builds and tests every platform and only then tags the revision, publishes the bindists as a GitHub Release and updates the ghcup channel. Leaving `publish` off rehearses all of it without tagging or publishing anything. Tags are produced by CI, never consumed by it, so a v* tag only ever exists for a revision that built and passed everywhere, and an abandoned attempt leaves nothing behind but a red run. The v* push trigger is dropped accordingly. - plinth-build.sh: add RELEASE_VERSION, which rewrites configure.ac's base version to the full four-component version and passes RELEASE=YES to configure, so the compiler reports exactly the released version instead of <base>.<date>. See Note [Release versioning]. The counter lives in the release request rather than in configure.ac: a committed four-component base would make every later snapshot build five components (9.6.166.1.20260803), one more than the tools accept. The value handed to configure is always spelled out, so this script's own RELEASE (release flavour) can never be mistaken for configure's RELEASE (version stamping). The build aborts if the compiler it produced does not report the requested version. - ci.yml: add the release-precheck job, which rejects an empty version, a version that does not extend configure.ac, or one already released -- in seconds, rather than after a full matrix build. Verify every bindist tarball is named for the released version before generating the channel. - ci.yml: gate the release job on the plinth-test-* suites as well as the ghcup install tests. It depended only on the latter, so a release could have shipped a compiler whose test suite had failed. - ci.yml: publish the ghcup channel to a dedicated orphan `ghcup-channel` branch rather than to gh-pages, served from raw.githubusercontent.com (where ghcup fetches its own metadata too). Publishing is then a plain git push, with no build or deploy step behind it, and the branch is CI's alone, so a release cannot conflict with anything else being pushed. The branch is created on the first release, so there is nothing to set up by hand. - configure.ac: document that the version here stays at three components. - README.md: the new channel URL, the RELEASE_VERSION knob, and how to cut a release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent f6ca7db commit 26714cb

4 files changed

Lines changed: 277 additions & 50 deletions

File tree

.github/workflows/ci.yml

Lines changed: 178 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,80 @@ on:
55
types: [opened, synchronize]
66
push:
77
branches: [master, ghc-9.6-plinth]
8-
# Pushing a v* tag builds every platform (as on a branch push) and then the
9-
# `release` job publishes a GitHub Release + updates the ghcup channel. See
10-
# Note [ghcup release channel] on the release job below.
11-
tags: ['v*']
8+
# Releases are cut from here: run the workflow with a release_version to build
9+
# and test that version, and tick `publish` to have the release job tag it and
10+
# publish it once everything has passed. See Note [ghcup release channel].
1211
workflow_dispatch:
12+
inputs:
13+
release_version:
14+
description: >-
15+
Build as a release stamped with this version (e.g. 9.6.166.1) instead
16+
of the usual dated snapshot version. Leave empty for an ordinary build.
17+
required: false
18+
default: ''
19+
publish:
20+
description: >-
21+
Tag and publish that version once every build and test job has passed.
22+
Leave off to rehearse a release without publishing anything.
23+
type: boolean
24+
required: false
25+
default: false
1326

1427
# Least-privilege GITHUB_TOKEN: jobs only check out code (contents: read).
1528
# Submodules use a dedicated SSH key and artifacts use the Actions runtime
1629
# token, so no write scopes are needed.
1730
permissions:
1831
contents: read
1932

33+
env:
34+
# The version being released ("9.6.166.1", or "v9.6.166.1" -- the steps below
35+
# strip the "v"), empty on ordinary pushes and PRs. It is passed to
36+
# plinth-build.sh as RELEASE_VERSION, which stamps the compiler with exactly
37+
# that version instead of the usual <base>.<date>. See Note [Release
38+
# versioning] in plinth-build.sh.
39+
REQUESTED_VERSION: ${{ github.event.inputs.release_version || '' }}
40+
2041
jobs:
42+
# Reject an unreleasable request in seconds instead of after a full matrix
43+
# build. The release job repeats none of this: it inherits the verdict by
44+
# depending on this job. See Note [ghcup release channel].
45+
release-precheck:
46+
name: release-precheck
47+
if: ${{ inputs.publish }}
48+
runs-on: ubuntu-latest
49+
steps:
50+
51+
- uses: actions/checkout@v4
52+
with:
53+
submodules: false
54+
55+
- name: Check the requested release version
56+
run: |
57+
set -eu
58+
VERSION="${REQUESTED_VERSION#v}"
59+
if [ -z "$VERSION" ]; then
60+
echo "error: publish was requested without a release_version"
61+
exit 1
62+
fi
63+
# The same rule plinth-build.sh applies when stamping the compiler, so a
64+
# version it would reject never reaches the build. See Note [Release
65+
# versioning] there.
66+
BASE_VERSION=$(sed -n 's/^AC_INIT(\[[^]]*\], *\[\([^]]*\)\].*/\1/p' configure.ac)
67+
case "$VERSION" in
68+
"$BASE_VERSION"|"$BASE_VERSION".*) ;;
69+
*)
70+
echo "error: $VERSION does not extend the version in configure.ac ($BASE_VERSION)"
71+
exit 1
72+
;;
73+
esac
74+
# Tags are created by the release job, so an existing one means this
75+
# version was already released.
76+
if git ls-remote --exit-code --tags origin "refs/tags/v$VERSION" >/dev/null 2>&1; then
77+
echo "error: v$VERSION is already released"
78+
exit 1
79+
fi
80+
echo "releasing $VERSION from $GITHUB_REF_NAME ($GITHUB_SHA)"
81+
2182
plinth-build-linux:
2283
name: plinth-build / ${{ matrix.os }} / ghc ${{ matrix.ghc }}
2384
runs-on: ${{ matrix.os }}
@@ -41,8 +102,10 @@ jobs:
41102

42103
# BINDIST=1 produces the fixed-up uplc-ghc bindist (+ .tar.xz archive) while
43104
# keeping the lean dev flavour. The plinth-test job consumes that archive.
105+
# RELEASE_VERSION is empty except on a release build, where it pins the
106+
# version; see Note [Release versioning] in plinth-build.sh.
44107
- name: Build uplc-ghc bindist
45-
run: BINDIST=1 ./plinth-build.sh
108+
run: RELEASE_VERSION="${REQUESTED_VERSION#v}" BINDIST=1 ./plinth-build.sh
46109

47110
- name: Upload uplc-ghc bindist
48111
uses: actions/upload-artifact@v4
@@ -195,6 +258,7 @@ jobs:
195258
docker run --rm \
196259
-v "$GITHUB_WORKSPACE:/workspace" -w /workspace \
197260
-e BINDIST=1 -e LANG=C.UTF-8 -e LC_ALL=C.UTF-8 \
261+
-e RELEASE_VERSION="${REQUESTED_VERSION#v}" \
198262
alpine:3.20 sh -c '
199263
set -eux
200264
. ./.github/alpine-setup.sh
@@ -364,7 +428,7 @@ jobs:
364428
# BINDIST=1 produces the fixed-up uplc-ghc bindist (+ .tar.xz archive); the
365429
# plinth-test-windows job consumes that archive instead of rebuilding.
366430
- name: Build uplc-ghc bindist
367-
run: BINDIST=1 ./plinth-build.sh
431+
run: RELEASE_VERSION="${REQUESTED_VERSION#v}" BINDIST=1 ./plinth-build.sh
368432

369433
- name: Upload uplc-ghc bindist
370434
uses: actions/upload-artifact@v4
@@ -523,7 +587,7 @@ jobs:
523587
# BINDIST=1 produces the fixed-up uplc-ghc bindist (+ .tar.xz archive); the
524588
# plinth-test-macos job consumes that archive instead of rebuilding.
525589
- name: Build uplc-ghc bindist
526-
run: BINDIST=1 ./plinth-build.sh
590+
run: RELEASE_VERSION="${REQUESTED_VERSION#v}" BINDIST=1 ./plinth-build.sh
527591

528592
- name: Upload uplc-ghc bindist
529593
uses: actions/upload-artifact@v4
@@ -798,37 +862,50 @@ jobs:
798862

799863
# Note [ghcup release channel]
800864
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
801-
# On a v* tag, publish the per-platform bindists as a GitHub Release and
802-
# (re)generate the ghcup channel metadata on the gh-pages branch. uplc-ghc is
803-
# distributed as a custom third-party ghcup tool named `plinth`, installable
804-
# with a stock ghcup >= 0.2.1.0 (installer DSL). Users then run:
865+
# Once every build and test job has passed, tag the revision, publish the
866+
# per-platform bindists as a GitHub Release and (re)generate the ghcup channel
867+
# metadata on the ghcup-channel branch (see Note [Tagging a release] below).
868+
# uplc-ghc is distributed as a custom third-party ghcup tool named `plinth`,
869+
# installable with a stock ghcup >= 0.2.1.0 (installer DSL). Users then run:
805870
# ghcup config add-release-channel \
806-
# https://input-output-hk.github.io/ghc-plinth/ghcup-plinth.yaml
871+
# https://raw.githubusercontent.com/input-output-hk/ghc-plinth/ghcup-channel/ghcup-plinth.yaml
807872
# ghcup install plinth <version>
808873
#
809-
# The generator (generate-ghcup-metadata.py) accumulates every released
810-
# version into gh-pages/ghcup-plinth.versions.json and re-emits the channel
811-
# YAML, so old versions stay installable. Before publishing, we validate the
812-
# metadata end-to-end by installing plinth from a file:// copy of it (real
813-
# download + configure + make + symlink), decoupled from the not-yet-public
814-
# release URLs.
874+
# The channel lives on its own orphan branch, served straight from
875+
# raw.githubusercontent.com (which is also where ghcup fetches its own
876+
# metadata). Publishing is then a plain git push, and the branch is CI's alone,
877+
# so a release can never conflict with anything else being pushed.
878+
# raw.githubusercontent.com caches for 5 minutes (max-age=300), so a fresh
879+
# release can take that long to become visible to ghcup.
880+
#
881+
# The generator (generate-ghcup-metadata.py) accumulates every released version
882+
# into ghcup-plinth.versions.json on that branch and re-emits the channel YAML,
883+
# so old versions stay installable. Before publishing, we validate the metadata
884+
# end-to-end by installing plinth from a file:// copy of it (real download +
885+
# configure + make + symlink), decoupled from the not-yet-public release URLs.
815886
release:
816887
name: release / ghcup channel
817-
if: startsWith(github.ref, 'refs/tags/')
818-
# Gate publishing on the per-platform ghcup install tests (which transitively
819-
# require the build jobs and their uploaded bindist artifacts).
888+
if: ${{ inputs.publish }}
889+
# Gate publishing on everything that exercises the bindists: the ghcup
890+
# install tests and the Plinth test suites, on every platform. Both sets
891+
# transitively require the build jobs and their uploaded artifacts, so the
892+
# builds don't need listing here. Without the plinth-test-* jobs a release
893+
# could ship a compiler whose test suite failed.
820894
needs:
895+
- release-precheck
821896
- plinth-ghcup-test-linux
822897
- plinth-ghcup-test-linux-musl
823898
- plinth-ghcup-test-windows
824899
- plinth-ghcup-test-macos
900+
- plinth-test-linux
901+
- plinth-test-linux-musl
902+
- plinth-test-windows
903+
- plinth-test-macos
825904
runs-on: ubuntu-latest
826-
# Overrides the top-level `contents: read`: this job creates a Release and
827-
# pushes the regenerated channel to gh-pages.
905+
# Overrides the top-level `contents: read`: this job creates the tag and the
906+
# Release, and pushes the regenerated channel to the ghcup-channel branch.
828907
permissions:
829908
contents: write
830-
env:
831-
TAG: ${{ github.ref_name }}
832909
steps:
833910

834911
- uses: actions/checkout@v4
@@ -849,12 +926,39 @@ jobs:
849926
run: |
850927
set -eux
851928
ls -l dist/*.tar.xz
852-
# ghcup tool version: the tag without the leading 'v'.
853-
echo "VERSION=${TAG#v}" >> "$GITHUB_ENV"
929+
# The version released, and the ghcup tool version. The build jobs
930+
# stamped the compiler with the same string (RELEASE_VERSION), so every
931+
# tarball must be named for it; if not, the channel would advertise a
932+
# version no bindist provides. See Note [Release versioning] in
933+
# plinth-build.sh.
934+
VERSION="${REQUESTED_VERSION#v}"
935+
for f in dist/*.tar.xz; do
936+
case "$(basename "$f")" in
937+
"ghc-$VERSION-"*) ;;
938+
*) echo "error: $f is not a bindist for version $VERSION"; exit 1 ;;
939+
esac
940+
done
941+
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
942+
echo "TAG=v$VERSION" >> "$GITHUB_ENV"
854943
855944
# The install is already validated end-to-end on every platform by the
856945
# plinth-ghcup-test-* jobs this job `needs`, so no re-validation here.
857946

947+
# Note [Tagging a release]
948+
# ~~~~~~~~~~~~~~~~~~~~~~~~
949+
# The tag is created here, at the end, rather than being what starts the
950+
# release: everything above has already built and tested this exact commit,
951+
# so a v* tag exists only for a revision that passed. A failed or abandoned
952+
# release attempt leaves no trace in the tag list, and the checked-out commit
953+
# is $GITHUB_SHA -- the revision the run was dispatched on.
954+
- name: Tag the release
955+
run: |
956+
set -eux
957+
git config user.name "github-actions[bot]"
958+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
959+
git tag -a "$TAG" -m "uplc-ghc $VERSION" "$GITHUB_SHA"
960+
git push origin "$TAG"
961+
858962
# Publish the Release with all tarballs. Create it if missing, otherwise
859963
# (re-)upload the assets.
860964
- name: Create GitHub Release
@@ -871,29 +975,57 @@ jobs:
871975
gh release upload "$TAG" dist/*.tar.xz --clobber
872976
fi
873977
874-
# Regenerate the channel on gh-pages with the real Release download URLs and
875-
# push it. The generator merges into the existing versions DB so previously
876-
# released versions remain in the channel.
877-
- name: Check out gh-pages
878-
uses: actions/checkout@v4
879-
with:
880-
ref: gh-pages
881-
path: gh-pages-out
882-
883-
- name: Regenerate and publish ghcup channel
978+
# Regenerate the channel with the real Release download URLs and push it to
979+
# the ghcup-channel branch, which is what users point ghcup at. The generator
980+
# merges into the existing versions DB so previously released versions remain
981+
# in the channel. See Note [ghcup release channel].
982+
#
983+
# The branch holds nothing but the channel files, so it is built up in a
984+
# standalone repo rather than checked out: cloning it into a working tree of
985+
# this (very large) repo would mean deleting the whole GHC tree first. It is
986+
# created as an orphan on the first release, so no manual setup is needed.
987+
- name: Publish the ghcup channel
988+
env:
989+
GH_TOKEN: ${{ github.token }}
990+
CHANNEL_BRANCH: ghcup-channel
991+
# no -x: the push URL carries the token (GitHub masks it, but don't print it)
884992
run: |
885-
set -eux
993+
set -eu
994+
mkdir channel-out
995+
(
996+
cd channel-out
997+
git init -q -b "$CHANNEL_BRANCH"
998+
git config user.name "github-actions[bot]"
999+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
1000+
git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}"
1001+
if git fetch -q --depth 1 origin "$CHANNEL_BRANCH" 2>/dev/null; then
1002+
git reset -q --hard FETCH_HEAD
1003+
else
1004+
echo "first release: creating the $CHANNEL_BRANCH branch"
1005+
fi
1006+
)
8861007
python3 generate-ghcup-metadata.py \
8871008
--version "$VERSION" \
8881009
--base-url "https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}" \
889-
--db gh-pages-out/ghcup-plinth.versions.json \
890-
--output gh-pages-out/ghcup-plinth.yaml \
1010+
--db channel-out/ghcup-plinth.versions.json \
1011+
--output channel-out/ghcup-plinth.yaml \
8911012
--release-day "$(date -u +%Y-%m-%d)" \
8921013
--set-latest \
8931014
dist/*.tar.xz
894-
cd gh-pages-out
895-
git config user.name "github-actions[bot]"
896-
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
897-
git add ghcup-plinth.yaml ghcup-plinth.versions.json
898-
git commit -m "ghcup channel: publish plinth ${VERSION} (${TAG})" || echo "no changes to commit"
899-
git push
1015+
cat > channel-out/README.md <<EOF
1016+
# ghcup channel for the Plinth compiler
1017+
1018+
This branch only carries the ghcup metadata for \`uplc-ghc\`; it is not
1019+
part of the source tree. Both files are generated by CI on a \`v*\` tag
1020+
(see the \`release\` job in \`.github/workflows/ci.yml\`) -- do not edit
1021+
them by hand.
1022+
1023+
ghcup config add-release-channel https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${CHANNEL_BRANCH}/ghcup-plinth.yaml
1024+
ghcup install plinth latest
1025+
EOF
1026+
cd channel-out
1027+
git add -A
1028+
git commit -q -m "ghcup channel: publish plinth ${VERSION} (${TAG})" \
1029+
|| echo "no changes to commit"
1030+
git push -q origin "HEAD:$CHANNEL_BRANCH"
1031+
echo "published https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${CHANNEL_BRANCH}/ghcup-plinth.yaml"

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ third-party tools); upgrade with `ghcup upgrade` if needed.
2525

2626
Add the release channel once, then install and select a version:
2727

28-
$ ghcup config add-release-channel https://input-output-hk.github.io/ghc-plinth/ghcup-plinth.yaml
28+
$ ghcup config add-release-channel https://raw.githubusercontent.com/input-output-hk/ghc-plinth/ghcup-channel/ghcup-plinth.yaml
2929
$ ghcup install plinth latest
3030
$ ghcup set plinth latest # puts uplc-ghc on PATH
3131

@@ -41,7 +41,14 @@ compiler to build a Plinth project:
4141
published as GitHub Releases for x86_64/aarch64 Linux (glibc and musl), macOS
4242
(Apple Silicon), and Windows; ghcup picks the right one for your platform.
4343

44+
The channel metadata lives on the [`ghcup-channel`][ghcup-channel] branch of this
45+
repository and is regenerated by CI on every release. Note that
46+
`raw.githubusercontent.com` caches for five minutes, so a just-published version
47+
can take that long to show up.
48+
4449
[ghcup]: https://www.haskell.org/ghcup/ "ghcup"
50+
[ghcup-channel]: https://github.com/input-output-hk/ghc-plinth/tree/ghcup-channel
51+
"the ghcup-channel branch"
4552

4653
The text below is the upstream GHC README.
4754

@@ -75,11 +82,43 @@ The script bootstraps GHC and then builds the Plinth-enabled compiler,
7582

7683
- `REBUILD=1` forces a full rebuild.
7784
- `RELEASE=1` builds a release flavour including documentation.
85+
- `RELEASE_VERSION=9.6.166.1` stamps the compiler with exactly that version
86+
instead of the usual `<version>.<date>` snapshot version. Used by CI when
87+
building a release; see the next section.
7888

7989
A binary distribution archive (`ghc-<version>-<platform>.tar.xz`) is produced
8090
under `_build/bindist/`.
8191

8292

93+
Making a release
94+
================
95+
96+
Releases are cut from the Actions tab, not by pushing a tag: go to
97+
**Actions -> CI -> Run workflow**, pick the branch to release, and fill in
98+
99+
- `release_version`: the version to release, e.g. `9.6.166.1`;
100+
- `publish`: tick it.
101+
102+
`configure.ac` holds the three-component base version (`9.6.166`, bumped
103+
whenever the `plutus` submodule moves) and `release_version` adds the release
104+
counter, so `9.6.166.1` produces a compiler reporting exactly `9.6.166.1`. A
105+
version that does not extend the base, or one that has already been released, is
106+
rejected within seconds by the `release-precheck` job.
107+
108+
CI then builds every platform and runs the test and ghcup-install suites. Only
109+
if all of them pass does it tag the revision `v<release_version>`, publish the
110+
bindists as a GitHub Release, and push the regenerated ghcup channel to the
111+
`ghcup-channel` branch, where previously released versions are kept and the new
112+
one becomes `latest`. That branch is created on the first release; nothing needs
113+
setting up by hand.
114+
115+
Tagging last means a `v*` tag only ever exists for a revision that built and
116+
passed everywhere: an abandoned or failed release attempt leaves nothing behind
117+
but a red workflow run. To rehearse one, run the workflow the same way with
118+
`publish` left off -- identical builds and tests, but nothing is tagged or
119+
published.
120+
121+
83122
How to use it?
84123
==============
85124

configure.ac

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ AC_INIT([The Glorious Glasgow Haskell Compilation System], [9.6.166], [glasgow-h
3030
# would tell a user which one that is.
3131
#
3232
# The fourth component distinguishes builds of the same Plinth version:
33-
# - 9.6.166.DATE for RELEASE=NO (version in this file is [9.6.166])
34-
# - 9.6.166.X for RELEASE=YES, X counting re-releases from 1
33+
# - 9.6.166.DATE for a snapshot build (RELEASE=NO, the default)
34+
# - 9.6.166.X for a released build, X counting releases from 1
35+
# Keep the version in this file at three components: plinth-build.sh rewrites
36+
# it to the full four-component version and passes RELEASE=YES when building a
37+
# release. See Note [Release versioning] there.
3538
#
3639
# 166 is numerically below the previous 1501, which encoded a release counter
3740
# in this component; nothing was ever published as 1501, so no public version

0 commit comments

Comments
 (0)