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.
1730permissions :
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+
2041jobs :
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"
0 commit comments