|
| 1 | +name: release |
| 2 | + |
| 3 | +# Self-host release: bootstrap mcpp from xlings (xim:mcpp), build the |
| 4 | +# musl-static artefact via `mcpp pack --target x86_64-linux-musl -o ...`, |
| 5 | +# inject xlings into the produced tarball for install.sh consumers, |
| 6 | +# smoke-test, upload. |
| 7 | + |
| 8 | +on: |
| 9 | + push: |
| 10 | + tags: [ 'v*' ] |
| 11 | + workflow_dispatch: |
| 12 | + inputs: |
| 13 | + tag: |
| 14 | + description: 'tag to (re)build — leave blank to derive `v<version>` from mcpp.toml and create the tag automatically' |
| 15 | + required: false |
| 16 | + |
| 17 | +jobs: |
| 18 | + build-release: |
| 19 | + name: build + upload (linux / x86_64) |
| 20 | + runs-on: ubuntu-24.04 |
| 21 | + permissions: |
| 22 | + contents: write # required to create releases + push tags |
| 23 | + timeout-minutes: 60 |
| 24 | + env: |
| 25 | + # mcpp resolves MCPP_HOME from the binary's location by default, |
| 26 | + # but here we want to share toolchains with the bootstrap sandbox, |
| 27 | + # so we pin to a known path. |
| 28 | + MCPP_HOME: /home/runner/.mcpp |
| 29 | + steps: |
| 30 | + # fetch-tags so the resolve-tag step can detect existing tags |
| 31 | + # without an extra round-trip; full history isn't needed. |
| 32 | + - uses: actions/checkout@v4 |
| 33 | + with: |
| 34 | + fetch-tags: true |
| 35 | + |
| 36 | + - name: Resolve target tag + commit |
| 37 | + id: resolve |
| 38 | + # Three trigger shapes converge here: |
| 39 | + # 1. push: refs/tags/vX.Y.Z → use that tag, build at its commit |
| 40 | + # 2. workflow_dispatch with `tag` input set: |
| 41 | + # - tag exists on remote → check it out (rebuild scenario) |
| 42 | + # - tag doesn't exist → use current HEAD; gh-release |
| 43 | + # creates the tag at that commit on upload |
| 44 | + # 3. workflow_dispatch with no input → derive `v<version>` from |
| 45 | + # mcpp.toml's [package].version, build at current HEAD; |
| 46 | + # gh-release creates the tag. |
| 47 | + run: | |
| 48 | + if [ "${{ github.event_name }}" = "push" ]; then |
| 49 | + TAG="${{ github.ref_name }}" |
| 50 | + elif [ -n "${{ github.event.inputs.tag }}" ]; then |
| 51 | + TAG="${{ github.event.inputs.tag }}" |
| 52 | + else |
| 53 | + VER=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) |
| 54 | + test -n "$VER" || { echo 'failed to read [package].version from mcpp.toml'; exit 1; } |
| 55 | + TAG="v$VER" |
| 56 | + fi |
| 57 | + echo "tag=$TAG" >> "$GITHUB_OUTPUT" |
| 58 | + echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" |
| 59 | + # If the tag exists on remote AND we're on workflow_dispatch, |
| 60 | + # check it out so we rebuild that exact commit. push-tag runs |
| 61 | + # already start at the tag commit. |
| 62 | + if [ "${{ github.event_name }}" = "workflow_dispatch" ] \ |
| 63 | + && git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then |
| 64 | + git checkout --detach "refs/tags/$TAG" |
| 65 | + fi |
| 66 | + echo "Resolved tag: $TAG (commit $(git rev-parse --short HEAD))" |
| 67 | +
|
| 68 | + # Cache mcpp's sandbox: musl-gcc 15.1 + binutils + glibc + linux-headers |
| 69 | + # + patchelf + ninja is ~800 MB on disk; without this every release |
| 70 | + # rebuilds from cold install. Key on the workspace manifest so a |
| 71 | + # toolchain change in mcpp.toml refreshes the cache. |
| 72 | + - name: Cache mcpp sandbox |
| 73 | + uses: actions/cache@v4 |
| 74 | + with: |
| 75 | + path: ~/.mcpp |
| 76 | + key: mcpp-sandbox-${{ runner.os }}-release-${{ hashFiles('mcpp.toml', '.xlings.json') }} |
| 77 | + restore-keys: | |
| 78 | + mcpp-sandbox-${{ runner.os }}-release- |
| 79 | + mcpp-sandbox-${{ runner.os }}- |
| 80 | +
|
| 81 | + # Cache xlings + xim:mcpp install. |
| 82 | + - name: Cache xlings |
| 83 | + uses: actions/cache@v4 |
| 84 | + with: |
| 85 | + path: ~/.xlings |
| 86 | + key: xlings-${{ runner.os }}-release-${{ hashFiles('.xlings.json') }} |
| 87 | + restore-keys: | |
| 88 | + xlings-${{ runner.os }}-release- |
| 89 | + xlings-${{ runner.os }}- |
| 90 | +
|
| 91 | + - name: Bootstrap mcpp via xlings |
| 92 | + env: |
| 93 | + XLINGS_NON_INTERACTIVE: '1' |
| 94 | + run: | |
| 95 | + if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then |
| 96 | + curl -fsSL https://d2learn.org/xlings-install.sh | bash |
| 97 | + fi |
| 98 | + export PATH="$HOME/.xlings/subos/default/bin:$PATH" |
| 99 | + xlings --version |
| 100 | + xlings install mcpp -y |
| 101 | + MCPP="$HOME/.xlings/subos/default/bin/mcpp" |
| 102 | + test -x "$MCPP" |
| 103 | + "$MCPP" --version |
| 104 | + echo "MCPP=$MCPP" >> "$GITHUB_ENV" |
| 105 | + echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" |
| 106 | +
|
| 107 | + - name: Build + pack release artefact (musl static) |
| 108 | + id: stage |
| 109 | + # Build for the musl-static target, strip the produced ELF, then |
| 110 | + # let `mcpp pack` assemble the tarball (binary + top-level wrapper |
| 111 | + # + README + LICENSE, contents at archive root). Inject xlings |
| 112 | + # afterwards so install.sh consumers get a single self-contained |
| 113 | + # bundle. |
| 114 | + run: | |
| 115 | + TAG="${{ steps.resolve.outputs.tag }}" |
| 116 | + VERSION="${{ steps.resolve.outputs.version }}" |
| 117 | + TARBALL_NAME="mcpp-${VERSION}-linux-x86_64.tar.gz" |
| 118 | +
|
| 119 | + # Build first so we can strip the ELF before pack copies it. |
| 120 | + "$MCPP" build --target x86_64-linux-musl |
| 121 | + ARTIFACT=$(find target/x86_64-linux-musl -type f -name mcpp | head -1) |
| 122 | + test -n "$ARTIFACT" |
| 123 | + file "$ARTIFACT" | grep -q 'statically linked' |
| 124 | + # Strip — debug info on a static ELF balloons it ~7×. |
| 125 | + strip "$ARTIFACT" |
| 126 | +
|
| 127 | + # Pack with the freshly-built mcpp (not the bootstrap) so any |
| 128 | + # fixes to the pack code path are exercised in the same release |
| 129 | + # they ship in. MCPP_HOME is forced so the new binary uses the |
| 130 | + # pinned sandbox instead of resolving relative to its own |
| 131 | + # location under target/. |
| 132 | + MCPP_HOME="$MCPP_HOME" "$ARTIFACT" pack \ |
| 133 | + --target x86_64-linux-musl \ |
| 134 | + --mode static \ |
| 135 | + -o "${TARBALL_NAME}" |
| 136 | +
|
| 137 | + # Inject xlings: extract → add bin/xlings to the wrapper dir → |
| 138 | + # re-tar preserving the wrapper. The wrapper dir name matches |
| 139 | + # the tarball stem (mcpp pack ties the two together). |
| 140 | + TARBALL="target/dist/${TARBALL_NAME}" |
| 141 | + WRAPPER="${TARBALL_NAME%.tar.gz}" |
| 142 | + test -f "$TARBALL" |
| 143 | + INJECT=$(mktemp -d) |
| 144 | + tar -xzf "$TARBALL" -C "$INJECT" |
| 145 | + cp "$XLINGS_BIN" "$INJECT/$WRAPPER/bin/xlings" |
| 146 | + chmod +x "$INJECT/$WRAPPER/bin/xlings" |
| 147 | + (cd "$INJECT" && tar -czf "$GITHUB_WORKSPACE/${TARBALL}" "$WRAPPER") |
| 148 | + rm -rf "$INJECT" |
| 149 | +
|
| 150 | + # Stage final dist/ (tarball + sidecars) for upload. |
| 151 | + mkdir -p dist |
| 152 | + cp "$TARBALL" "dist/${TARBALL_NAME}" |
| 153 | + (cd dist && cp "${TARBALL_NAME}" "mcpp-linux-x86_64.tar.gz") |
| 154 | + (cd dist && sha256sum "${TARBALL_NAME}" "mcpp-linux-x86_64.tar.gz" > SHA256SUMS) |
| 155 | + (cd dist && sha256sum "${TARBALL_NAME}" > "${TARBALL_NAME}.sha256") |
| 156 | + (cd dist && sha256sum "mcpp-linux-x86_64.tar.gz" > "mcpp-linux-x86_64.tar.gz.sha256") |
| 157 | +
|
| 158 | + # Top-level install.sh — fetched by `curl | bash`. |
| 159 | + cp install.sh dist/install.sh |
| 160 | + chmod +x dist/install.sh |
| 161 | +
|
| 162 | + echo "tag=$TAG" >> $GITHUB_OUTPUT |
| 163 | + echo "version=$VERSION" >> $GITHUB_OUTPUT |
| 164 | + echo "tarball=${TARBALL_NAME}" >> $GITHUB_OUTPUT |
| 165 | + ls -la dist/ |
| 166 | +
|
| 167 | + - name: Smoke-test the bundled tarball |
| 168 | + # Extract to a scratch dir and run mcpp from there with MCPP_HOME |
| 169 | + # unset — proves the release artefact is genuinely self-contained. |
| 170 | + run: | |
| 171 | + VERSION="${{ steps.stage.outputs.version }}" |
| 172 | + TARBALL_NAME="${{ steps.stage.outputs.tarball }}" |
| 173 | + # Wrapper dir inside the tarball matches its stem (mcpp pack |
| 174 | + # ties the two together). |
| 175 | + WRAPPER="${TARBALL_NAME%.tar.gz}" |
| 176 | + SMOKE=$(mktemp -d) |
| 177 | + tar -xzf "dist/${TARBALL_NAME}" -C "$SMOKE" |
| 178 | + ROOT="$SMOKE/$WRAPPER" |
| 179 | + test -x "$ROOT/bin/mcpp" |
| 180 | + test -x "$ROOT/bin/xlings" |
| 181 | + test -x "$ROOT/mcpp" |
| 182 | + file "$ROOT/bin/mcpp" | grep -q 'statically linked' |
| 183 | + env -u MCPP_HOME "$ROOT/bin/mcpp" --version |
| 184 | + env -u MCPP_HOME "$ROOT/bin/mcpp" --help | head -10 |
| 185 | + # Top-level wrapper reports the same version we're shipping. |
| 186 | + env -u MCPP_HOME "$ROOT/mcpp" --version | grep -q "$VERSION" |
| 187 | + # MCPP_HOME should auto-resolve to the extracted root. |
| 188 | + out=$(env -u MCPP_HOME "$ROOT/bin/mcpp" self env) |
| 189 | + echo "$out" | grep -q "MCPP_HOME *= *$ROOT" |
| 190 | +
|
| 191 | + - name: Generate source tarball + xpkg.lua via mcpp publish |
| 192 | + # Use the freshly-built mcpp to produce the source tarball + xpkg |
| 193 | + # descriptor for mcpp-index. The release tarball wraps its |
| 194 | + # contents in a `<tarball-stem>/` directory so the extract path |
| 195 | + # is $PUB/$WRAPPER/bin/mcpp. |
| 196 | + run: | |
| 197 | + VERSION="${{ steps.stage.outputs.version }}" |
| 198 | + TARBALL_NAME="${{ steps.stage.outputs.tarball }}" |
| 199 | + WRAPPER="${TARBALL_NAME%.tar.gz}" |
| 200 | + PUB=$(mktemp -d) |
| 201 | + tar -xzf "dist/${TARBALL_NAME}" -C "$PUB" |
| 202 | + MCPP_BIN="$PUB/$WRAPPER/bin/mcpp" |
| 203 | + env -u MCPP_HOME "$MCPP_BIN" publish --dry-run --allow-dirty |
| 204 | + test -f "target/dist/mcpp-${VERSION}.tar.gz" |
| 205 | + test -f "target/dist/mcpp.lua" |
| 206 | + cp "target/dist/mcpp-${VERSION}.tar.gz" dist/ |
| 207 | + cp "target/dist/mcpp.lua" dist/ |
| 208 | + ls -la dist/ |
| 209 | +
|
| 210 | + - name: Extract release notes from CHANGELOG |
| 211 | + id: notes |
| 212 | + run: | |
| 213 | + TAG="${{ steps.stage.outputs.tag }}" |
| 214 | + VERSION="${{ steps.stage.outputs.version }}" |
| 215 | + awk -v v="$VERSION" ' |
| 216 | + /^## \[/ { |
| 217 | + if (in_section) exit |
| 218 | + if ($0 ~ "\\[" v "\\]") { in_section=1; next } |
| 219 | + } |
| 220 | + in_section { print } |
| 221 | + ' CHANGELOG.md > dist/RELEASE_NOTES.md || true |
| 222 | + if [ ! -s dist/RELEASE_NOTES.md ]; then |
| 223 | + echo "(no CHANGELOG entry found for $VERSION)" > dist/RELEASE_NOTES.md |
| 224 | + fi |
| 225 | + echo "--- RELEASE_NOTES.md ---" |
| 226 | + cat dist/RELEASE_NOTES.md |
| 227 | +
|
| 228 | + - name: Create GitHub Release |
| 229 | + uses: softprops/action-gh-release@v2 |
| 230 | + with: |
| 231 | + tag_name: ${{ steps.stage.outputs.tag }} |
| 232 | + name: ${{ steps.stage.outputs.tag }} |
| 233 | + body_path: dist/RELEASE_NOTES.md |
| 234 | + draft: false |
| 235 | + prerelease: false |
| 236 | + files: | |
| 237 | + dist/mcpp-${{ steps.stage.outputs.version }}-linux-x86_64.tar.gz |
| 238 | + dist/mcpp-${{ steps.stage.outputs.version }}-linux-x86_64.tar.gz.sha256 |
| 239 | + dist/mcpp-linux-x86_64.tar.gz |
| 240 | + dist/mcpp-linux-x86_64.tar.gz.sha256 |
| 241 | + dist/install.sh |
| 242 | + dist/SHA256SUMS |
| 243 | + dist/mcpp-${{ steps.stage.outputs.version }}.tar.gz |
| 244 | + dist/mcpp.lua |
0 commit comments