Release v0.0.149 #104
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 | |
| # Build the signed dmg + OTA binary on a real macOS runner and attach them to | |
| # the GitHub release. Triggered by pushing a version tag (e.g. `git tag v0.0.49 | |
| # && git push --tags`) — no more flaky local uploads. | |
| # | |
| # Struktur 3 job (wall-clock ±16m → ±7-10m): `gates` (fmt/clippy/test) dan | |
| # `build` (dmg + OTA) berjalan PARALEL; `publish` menunggu keduanya lalu | |
| # menempelkan artefak ke GitHub Release. Rilis tetap tidak terbit bila gate | |
| # gagal — hanya urutannya yang tidak lagi serial. | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Tag to build/release (e.g. v0.0.49)" | |
| required: true | |
| permissions: | |
| contents: write | |
| jobs: | |
| gates: | |
| runs-on: macos-14 # Apple silicon (arm64) | |
| steps: | |
| - name: Resolve tag | |
| id: tag | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| tag="${{ inputs.tag }}" | |
| else | |
| tag="$GITHUB_REF_NAME" | |
| fi | |
| case "$tag" in | |
| v*) ;; | |
| *) echo "Release tag must start with v: $tag" >&2; exit 1 ;; | |
| esac | |
| echo "tag=$tag" >> "$GITHUB_OUTPUT" | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ steps.tag.outputs.tag }} | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt, clippy | |
| - name: Cache cargo | |
| uses: Swatinem/rust-cache@v2 | |
| - name: Validate release tag | |
| run: | | |
| version="$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')" | |
| expected="v$version" | |
| actual="${{ steps.tag.outputs.tag }}" | |
| if [ "$actual" != "$expected" ]; then | |
| echo "Tag/version mismatch: tag=$actual Cargo.toml=$version" >&2 | |
| exit 1 | |
| fi | |
| - name: Check formatting | |
| run: cargo fmt --check | |
| - name: Run GUI visual QA contracts | |
| run: python3 scripts/gui-visual-qa.py | |
| # clippy = cargo check + lint dengan cakupan flag yang sama, jadi tidak | |
| # ada step `cargo check` terpisah (dulu redundan). | |
| - name: Run clippy | |
| run: cargo clippy --workspace --all-targets -- -D warnings | |
| # oxide-desktop (egui, dorman — tidak ada command yang me-route ke sana) | |
| # dikecualikan: clippy di atas sudah menjaga crate itu tetap compile, | |
| # membangun test-harness-nya hanya membakar menit runner. | |
| - name: Run tests | |
| run: cargo test --workspace --exclude oxide-desktop | |
| build: | |
| runs-on: macos-14 # Apple silicon (arm64) | |
| steps: | |
| - name: Resolve tag | |
| id: tag | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| tag="${{ inputs.tag }}" | |
| else | |
| tag="$GITHUB_REF_NAME" | |
| fi | |
| case "$tag" in | |
| v*) ;; | |
| *) echo "Release tag must start with v: $tag" >&2; exit 1 ;; | |
| esac | |
| echo "tag=$tag" >> "$GITHUB_OUTPUT" | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ steps.tag.outputs.tag }} | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache cargo | |
| uses: Swatinem/rust-cache@v2 | |
| - name: Validate release tag | |
| run: | | |
| version="$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')" | |
| expected="v$version" | |
| actual="${{ steps.tag.outputs.tag }}" | |
| if [ "$actual" != "$expected" ]; then | |
| echo "Tag/version mismatch: tag=$actual Cargo.toml=$version" >&2 | |
| exit 1 | |
| fi | |
| # Optional: import the stable "Oxide Dev" signing identity so OTA updates | |
| # don't re-trigger macOS Allow prompts. Set repo secrets OXIDE_CERT_P12 | |
| # (base64 of the .p12) and OXIDE_CERT_PASSWORD. Without them the build is | |
| # ad-hoc signed (still works; just prompts on each update). | |
| - name: Import signing certificate | |
| id: signing | |
| timeout-minutes: 3 | |
| env: | |
| OXIDE_CERT_P12: ${{ secrets.OXIDE_CERT_P12 }} | |
| OXIDE_CERT_PASSWORD: ${{ secrets.OXIDE_CERT_PASSWORD }} | |
| run: | | |
| if [ -z "$OXIDE_CERT_P12" ]; then | |
| echo "::warning title=Unsigned release::Missing OXIDE_CERT_P12; this build will be ad-hoc signed and macOS may re-ask volume permissions after updates." | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| KEYCHAIN="$RUNNER_TEMP/build.keychain" | |
| PW="ci-$RANDOM" | |
| security create-keychain -p "$PW" "$KEYCHAIN" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN" | |
| security unlock-keychain -p "$PW" "$KEYCHAIN" | |
| echo "$OXIDE_CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12" | |
| security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$OXIDE_CERT_PASSWORD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$PW" "$KEYCHAIN" >/dev/null | |
| security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | sed s/\"//g) | |
| CERT_PEM="$RUNNER_TEMP/signing-cert.pem" | |
| security find-certificate -c "Oxide Dev" -p "$KEYCHAIN" > "$CERT_PEM" || true | |
| if [ ! -s "$CERT_PEM" ]; then | |
| security find-certificate -a -p "$KEYCHAIN" > "$CERT_PEM" || true | |
| fi | |
| if [ -s "$CERT_PEM" ]; then | |
| python3 - "$KEYCHAIN" "$CERT_PEM" <<'PY' | |
| import subprocess | |
| import sys | |
| commands = [ | |
| [ | |
| "sudo", | |
| "-n", | |
| "security", | |
| "add-trusted-cert", | |
| "-d", | |
| "-r", | |
| "trustRoot", | |
| "-p", | |
| "codeSign", | |
| "-k", | |
| "/Library/Keychains/System.keychain", | |
| sys.argv[2], | |
| ], | |
| [ | |
| "security", | |
| "add-trusted-cert", | |
| "-r", | |
| "trustRoot", | |
| "-p", | |
| "codeSign", | |
| "-k", | |
| sys.argv[1], | |
| sys.argv[2], | |
| ], | |
| ] | |
| trusted = False | |
| timed_out = False | |
| for command in commands: | |
| try: | |
| result = subprocess.run(command, check=False, timeout=30) | |
| except subprocess.TimeoutExpired: | |
| timed_out = True | |
| continue | |
| if result.returncode == 0: | |
| trusted = True | |
| break | |
| if not trusted and timed_out: | |
| print("::warning title=Signing trust timed out::Timed out while trusting the imported signing certificate; release will fall back to ad-hoc signing if no valid identity is available.") | |
| elif not trusted: | |
| print("::warning title=Signing trust failed::Unable to trust the imported signing certificate; release will fall back to ad-hoc signing if no valid identity is available.") | |
| PY | |
| fi | |
| security find-identity -v -p codesigning "$KEYCHAIN" | |
| identity="$(security find-identity -v -p codesigning "$KEYCHAIN" | awk -F '\"' '/\"/ { print $2; exit }')" | |
| if [ -z "$identity" ]; then | |
| identity="$(security find-identity -v -p codesigning | awk -F '\"' '/\"/ { print $2; exit }')" | |
| fi | |
| rm -f "$RUNNER_TEMP/cert.p12" | |
| if [ -z "$identity" ]; then | |
| echo "::warning title=Unsigned release::Signing certificate was imported but no valid codesigning identity was found; this build will be ad-hoc signed." | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "identity=$identity" >> "$GITHUB_OUTPUT" | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| - name: Install dmg background deps (Pillow) | |
| # Optional — dmg-bg.py skips the styled background gracefully if absent. | |
| # Newer macOS runner images dropped Pillow from the default python3, which | |
| # is why v0.0.104's dmg step failed; install it (best-effort) to restore | |
| # the styled install window. | |
| run: pip3 install --quiet Pillow || pip3 install --quiet --break-system-packages Pillow || true | |
| - name: Build dmg | |
| run: | | |
| if [ "${{ steps.signing.outputs.available }}" = "true" ]; then | |
| export OXIDE_REQUIRE_SIGNING=1 | |
| export OXIDE_SIGN_IDENTITY="${{ steps.signing.outputs.identity }}" | |
| fi | |
| bash scripts/make-dmg.sh | |
| - name: Package OTA binaries (gzip) | |
| run: | | |
| gzip -9 -c target/release/oxide > oxide-macos-arm64.gz | |
| gzip -9 -c target/release/oxide-term > oxide-term-macos-arm64.gz | |
| - name: Generate asset checksums | |
| run: | | |
| shasum -a 256 dist/Oxide.dmg > dist/Oxide.dmg.sha256 | |
| shasum -a 256 oxide-macos-arm64.gz > oxide-macos-arm64.gz.sha256 | |
| shasum -a 256 oxide-term-macos-arm64.gz > oxide-term-macos-arm64.gz.sha256 | |
| - name: Upload release assets artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: release-assets | |
| if-no-files-found: error | |
| retention-days: 3 | |
| path: | | |
| dist/Oxide.dmg | |
| dist/Oxide.dmg.sha256 | |
| oxide-macos-arm64.gz | |
| oxide-macos-arm64.gz.sha256 | |
| oxide-term-macos-arm64.gz | |
| oxide-term-macos-arm64.gz.sha256 | |
| publish: | |
| needs: [gates, build] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Resolve tag | |
| id: tag | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| tag="${{ inputs.tag }}" | |
| else | |
| tag="$GITHUB_REF_NAME" | |
| fi | |
| case "$tag" in | |
| v*) ;; | |
| *) echo "Release tag must start with v: $tag" >&2; exit 1 ;; | |
| esac | |
| echo "tag=$tag" >> "$GITHUB_OUTPUT" | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ steps.tag.outputs.tag }} | |
| fetch-depth: 0 | |
| - name: Resolve previous release tag | |
| id: previous_tag | |
| run: | | |
| tag="${{ steps.tag.outputs.tag }}" | |
| previous="$(git describe --tags --abbrev=0 "$tag^" 2>/dev/null || true)" | |
| if [ -n "$previous" ]; then | |
| echo "tag=$previous" >> "$GITHUB_OUTPUT" | |
| echo "Previous release tag: $previous" | |
| else | |
| echo "No previous release tag found; GitHub will choose the release-notes base." | |
| fi | |
| - name: Download release assets artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: release-assets | |
| - name: Generate release notes | |
| env: | |
| TAG: ${{ steps.tag.outputs.tag }} | |
| PREVIOUS_TAG: ${{ steps.previous_tag.outputs.tag }} | |
| run: | | |
| notes="$RUNNER_TEMP/release-notes.md" | |
| release_utc="$(date -u '+%Y-%m-%d %H:%M:%S UTC')" | |
| release_local="$(TZ=Asia/Makassar date '+%Y-%m-%d %H:%M:%S WITA')" | |
| run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" | |
| short_sha="$(git rev-parse --short "$TAG")" | |
| full_sha="$(git rev-parse "$TAG")" | |
| { | |
| echo "Built by GitHub Actions on ${release_local} (${release_utc})." | |
| echo | |
| echo "## Build" | |
| echo "- Workflow run: [${GITHUB_RUN_ID}](${run_url})" | |
| echo "- Commit: \`${short_sha}\`" | |
| echo "- Full SHA: \`${full_sha}\`" | |
| echo | |
| echo "## Changes" | |
| if [ -n "$PREVIOUS_TAG" ]; then | |
| changes="$(git log --pretty=format:'- %s (`%h`)' --no-merges "${PREVIOUS_TAG}..${TAG}" | sed '/^- Release v[0-9][0-9.]* (`[0-9a-f]*`)$/d' || true)" | |
| if [ -n "$changes" ]; then | |
| echo "$changes" | |
| else | |
| echo "- Release packaging and version metadata for ${TAG}." | |
| fi | |
| echo | |
| echo "Changed files:" | |
| git diff --name-only "${PREVIOUS_TAG}..${TAG}" | sed 's/^/- `/' | sed 's/$/`/' | |
| echo | |
| echo "**Full Changelog**: https://github.com/${GITHUB_REPOSITORY}/compare/${PREVIOUS_TAG}...${TAG}" | |
| else | |
| echo "- Initial release notes base was not available." | |
| fi | |
| echo | |
| echo "## Install" | |
| echo "Open the dmg, drag Oxide to Applications, then run:" | |
| echo | |
| echo '```sh' | |
| echo 'xattr -dr com.apple.quarantine /Applications/Oxide.app' | |
| echo '```' | |
| } > "$notes" | |
| cat "$notes" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Attach assets to the release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.tag.outputs.tag }} | |
| name: ${{ steps.tag.outputs.tag }} | |
| body_path: ${{ runner.temp }}/release-notes.md | |
| files: | | |
| dist/Oxide.dmg | |
| dist/Oxide.dmg.sha256 | |
| oxide-macos-arm64.gz | |
| oxide-macos-arm64.gz.sha256 | |
| oxide-term-macos-arm64.gz | |
| oxide-term-macos-arm64.gz.sha256 | |
| fail_on_unmatched_files: true |