Skip to content

Release v0.0.149

Release v0.0.149 #104

Workflow file for this run

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