Merge pull request #195 from justrach/release/0.4.14 #26
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| permissions: | |
| contents: write | |
| jobs: | |
| # ── Linux: cross-compile both arches on one runner ───────────────────────── | |
| build-linux: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: [x86_64-linux, aarch64-linux] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: mlugg/setup-zig@v2 | |
| with: | |
| version: 0.17.0-dev.813+2153f8143 | |
| - name: Build | |
| run: zig build -Dtarget=${{ matrix.target }} -Doptimize=ReleaseFast -p out | |
| - name: Package | |
| run: | | |
| VERSION="${GITHUB_REF_NAME}" | |
| mkdir -p dist | |
| tar -czf "dist/kuri-${VERSION}-${{ matrix.target }}.tar.gz" -C out/bin . | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: kuri-${{ matrix.target }} | |
| path: dist/*.tar.gz | |
| # ── macOS: both arches on one Apple Silicon runner ───────────────────────── | |
| # Both arches build here on purpose. The previous x86 job pinned `macos-13`, | |
| # a label GitHub has retired: it never got a runner, sat queued until the 24h | |
| # job limit killed it, and took every release down with it (the publish jobs | |
| # `needs:` it, so they were skipped). Zig cross-compiles x86_64-macos from | |
| # arm64 fine, and codesign is architecture-agnostic, so one live runner does | |
| # both. Each matrix leg installs to its own prefix so the tarballs can't pick | |
| # up the other arch's binaries. | |
| build-macos: | |
| runs-on: macos-latest | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: [aarch64-macos, x86_64-macos] | |
| outputs: | |
| signed: ${{ steps.sign.outputs.signed }} | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: mlugg/setup-zig@v2 | |
| with: | |
| version: 0.17.0-dev.813+2153f8143 | |
| - name: Build | |
| run: zig build -Dtarget=${{ matrix.target }} -Doptimize=ReleaseFast -p out | |
| # Records whether signing actually happened, so the release notes and the | |
| # channel manifest can state the truth instead of asserting it. | |
| - name: Sign & notarize | |
| id: sign | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${APPLE_CERTIFICATE}" ]; then | |
| echo "signed=false" >> "$GITHUB_OUTPUT" | |
| echo "::warning::No APPLE_CERTIFICATE secret — shipping UNSIGNED, un-notarized macOS binaries." | |
| exit 0 | |
| fi | |
| KEYCHAIN="build-$(uuidgen).keychain" | |
| KEYCHAIN_PASS="$(uuidgen)" | |
| security create-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN" | |
| security unlock-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN" | |
| echo "$APPLE_CERTIFICATE" | base64 --decode > /tmp/cert.p12 | |
| security import /tmp/cert.p12 -k "$KEYCHAIN" -P "$APPLE_CERTIFICATE_PASSWORD" \ | |
| -T /usr/bin/codesign -T /usr/bin/productsign | |
| security list-keychain -d user -s "$KEYCHAIN" | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASS" "$KEYCHAIN" | |
| for BIN in out/bin/*; do | |
| codesign --sign "Developer ID Application: $APPLE_TEAM_ID" \ | |
| --options runtime --timestamp --force "$BIN" | |
| done | |
| zip -j "/tmp/kuri-${{ matrix.target }}.zip" out/bin/* | |
| # --timeout so a stalled submission fails in minutes, not at the job cap. | |
| xcrun notarytool submit "/tmp/kuri-${{ matrix.target }}.zip" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --wait --timeout 30m | |
| # Bare Mach-O executables cannot be stapled (only bundles/pkgs/dmgs); | |
| # Gatekeeper verifies these online. Best-effort, never fatal. | |
| for BIN in out/bin/*; do xcrun stapler staple "$BIN" || true; done | |
| security delete-keychain "$KEYCHAIN" | |
| echo "signed=true" >> "$GITHUB_OUTPUT" | |
| - name: Package | |
| run: | | |
| VERSION="${GITHUB_REF_NAME}" | |
| mkdir -p dist | |
| tar -czf "dist/kuri-${VERSION}-${{ matrix.target }}.tar.gz" -C out/bin . | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: kuri-${{ matrix.target }} | |
| path: dist/*.tar.gz | |
| # ── Publish self-managed release channel ──────────────────────────────────── | |
| publish-channel: | |
| needs: [build-linux, build-macos] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: release-channel | |
| path: channel | |
| # The tagged source tree. This job used to check out only the channel | |
| # branch, so when publishing install.sh from the repo was added below | |
| # there was no repo to copy it from: `cp install.sh` failed, and with | |
| # `set -e` the whole job died before writing anything at all. v0.4.13 | |
| # published to GitHub Releases but never reached the channel, leaving | |
| # `kuri update` and the curl-installer serving the previous release. | |
| - uses: actions/checkout@v4 | |
| with: | |
| path: repo | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| merge-multiple: true | |
| path: dist | |
| - name: Publish stable channel | |
| env: | |
| MACOS_SIGNED: ${{ needs.build-macos.outputs.signed }} | |
| run: | | |
| set -euo pipefail | |
| VERSION="${GITHUB_REF_NAME}" | |
| CHANNEL_DIR="channel/stable/${VERSION}" | |
| mkdir -p "$CHANNEL_DIR" | |
| cp dist/*.tar.gz "$CHANNEL_DIR/" | |
| # Publish the installer from the repository rather than leaving the | |
| # channel copy to be remembered by hand. These drifted for four | |
| # releases: install.sh in the repo gained kuri-mobile, the copy | |
| # people actually `curl` did not, so every install produced a kuri | |
| # whose `android`/`ios` subcommands could not exec their sibling | |
| # binary. The file users run must be generated from the tagged tree. | |
| cp repo/install.sh channel/stable/install.sh | |
| python3 - <<'PY' | |
| from pathlib import Path | |
| import datetime | |
| import hashlib | |
| import json | |
| import os | |
| version = os.environ["GITHUB_REF_NAME"] | |
| commit = os.environ["GITHUB_SHA"] | |
| notarized = os.environ.get("MACOS_SIGNED") == "true" | |
| root = Path("channel") | |
| version_dir = root / "stable" / version | |
| base = "https://raw.githubusercontent.com/justrach/kuri/release-channel/stable" | |
| assets = {} | |
| for path in sorted(version_dir.glob("kuri-*.tar.gz")): | |
| target = path.name.removeprefix(f"kuri-{version}-").removesuffix(".tar.gz") | |
| assets[target] = { | |
| "url": f"{base}/{version}/{path.name}", | |
| "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), | |
| "size": path.stat().st_size, | |
| "notarized": target.endswith("macos") and notarized, | |
| } | |
| manifest = { | |
| "channel": "stable", | |
| "version": version, | |
| "commit": commit, | |
| "published_at": datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"), | |
| "install_url": f"{base}/install.sh", | |
| "assets": assets, | |
| } | |
| (root / "stable" / "latest.json").write_text(json.dumps(manifest, indent=2) + "\n") | |
| (version_dir / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") | |
| with (version_dir / "sha256sums.txt").open("w") as out: | |
| for target, meta in assets.items(): | |
| out.write(f"{meta['sha256']} kuri-{version}-{target}.tar.gz\n") | |
| PY | |
| cat > channel/README.md <<'EOF' | |
| # Kuri Release Channel | |
| Self-managed release channel for `justrach/kuri`. GitHub Releases mirror the tagged assets, but the installer and stable manifest are still served from this branch. | |
| ## Stable | |
| ```sh | |
| curl -fsSL https://raw.githubusercontent.com/justrach/kuri/release-channel/stable/install.sh | sh | |
| ``` | |
| Manifest: | |
| - https://raw.githubusercontent.com/justrach/kuri/release-channel/stable/latest.json | |
| EOF | |
| cd channel | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add README.md stable | |
| git commit -m "release: publish stable ${VERSION}" || exit 0 | |
| git push origin HEAD:release-channel | |
| publish-github-release: | |
| needs: [build-linux, build-macos] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| merge-multiple: true | |
| path: dist | |
| - name: Build release notes and checksums | |
| run: | | |
| set -euo pipefail | |
| VERSION="${GITHUB_REF_NAME}" | |
| python3 - <<'PY' | |
| from pathlib import Path | |
| import hashlib | |
| import os | |
| import re | |
| version = os.environ["GITHUB_REF_NAME"].removeprefix("v") | |
| changelog = Path("CHANGELOG.md").read_text() | |
| pattern = re.compile(rf"^## \[{re.escape(version)}\] — .*$", re.MULTILINE) | |
| match = pattern.search(changelog) | |
| if not match: | |
| raise SystemExit(f"missing changelog section for {version}") | |
| start = match.start() | |
| next_match = re.search(r"^## \[", changelog[match.end():], re.MULTILINE) | |
| end = len(changelog) if not next_match else match.end() + next_match.start() | |
| section = changelog[start:end].strip() | |
| body = section.split("\n", 1)[1].strip() | |
| if os.environ.get("MACOS_SIGNED") == "true": | |
| footer = "macOS assets in this release are signed and notarized." | |
| else: | |
| footer = ( | |
| "> **Note** — macOS assets in this release are **not** signed or notarized. " | |
| "Gatekeeper will quarantine them; clear it with " | |
| "`xattr -d com.apple.quarantine <binary>`." | |
| ) | |
| notes = "## " + os.environ["GITHUB_REF_NAME"] + "\n\n" + body + "\n\n" + footer + "\n" | |
| Path("dist/release-notes.md").write_text(notes) | |
| with Path(os.environ["SHA_FILE"]).open("w") as out: | |
| for path in sorted(Path("dist").glob("kuri-*.tar.gz")): | |
| out.write(f"{hashlib.sha256(path.read_bytes()).hexdigest()} {path.name}\n") | |
| PY | |
| env: | |
| SHA_FILE: dist/kuri-${{ github.ref_name }}-sha256sums.txt | |
| MACOS_SIGNED: ${{ needs.build-macos.outputs.signed }} | |
| - name: Publish GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| VERSION="${GITHUB_REF_NAME}" | |
| TITLE="${VERSION}" | |
| NOTES="dist/release-notes.md" | |
| ASSETS=(dist/*.tar.gz "dist/kuri-${VERSION}-sha256sums.txt") | |
| if gh release view "$VERSION" >/dev/null 2>&1; then | |
| gh release upload "$VERSION" "${ASSETS[@]}" --clobber | |
| gh release edit "$VERSION" --title "$TITLE" --notes-file "$NOTES" --latest | |
| else | |
| gh release create "$VERSION" "${ASSETS[@]}" --title "$TITLE" --notes-file "$NOTES" --latest | |
| fi |