diff --git a/.github/workflows/release-canary.yml b/.github/workflows/release-canary.yml new file mode 100644 index 00000000..2714021f --- /dev/null +++ b/.github/workflows/release-canary.yml @@ -0,0 +1,286 @@ +name: Release (Canary) + +on: + push: + branches: + - canary + +concurrency: + group: release-canary-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ───────────────────────────────────────────────────────────── + # Phase 1: Compute the canary version string + # ───────────────────────────────────────────────────────────── + version: + name: Compute version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.ver.outputs.version }} + tag: ${{ steps.ver.outputs.tag }} + steps: + - id: ver + run: | + VERSION="0.$(date -u +%Y%m%d).${GITHUB_RUN_NUMBER}-canary" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + echo "### Canary version: \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY" + + # ───────────────────────────────────────────────────────────── + # Phase 2: Build each architecture in parallel + # ───────────────────────────────────────────────────────────── + build: + name: Build (${{ matrix.arch }}) + needs: version + runs-on: macos-15 + strategy: + fail-fast: true + matrix: + include: + - arch: aarch64 + target: aarch64-apple-darwin + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - name: Setup Bun (required for bundle-qmd.sh) + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + src-tauri/target + key: ${{ runner.os }}-release-cargo-${{ hashFiles('src-tauri/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-release-cargo- + + - name: Install frontend dependencies + run: pnpm install --frozen-lockfile + + - name: Set version + run: | + VERSION="${{ needs.version.outputs.version }}" + jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json + sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml + + - name: Import Apple Developer certificate into keychain + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + CERT_PATH="$RUNNER_TEMP/apple_cert.p12" + KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db" + KEYCHAIN_PASSWORD="$(uuidgen)" + echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH" + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" + security list-keychain -d user -s "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" + + - name: Build Tauri app (with signing + notarization) + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + pnpm tauri build --target ${{ matrix.target }} + + - name: Upload .dmg + uses: actions/upload-artifact@v4 + with: + name: dmg-${{ matrix.arch }} + path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg + retention-days: 1 + + - name: Upload updater artifacts (.tar.gz + .sig) + uses: actions/upload-artifact@v4 + with: + name: updater-${{ matrix.arch }} + path: | + src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz + src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig + retention-days: 1 + + # ───────────────────────────────────────────────────────────── + # Phase 3: Publish GitHub Release (prerelease) + # ───────────────────────────────────────────────────────────── + release: + name: GitHub Release (canary) + needs: [version, build] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Generate release notes + run: | + PREV_TAG=$(git tag --sort=-version:refname | grep canary | head -n 1 || echo "") + if [ -z "$PREV_TAG" ]; then + NOTES=$(git log --oneline --no-merges -20) + else + NOTES=$(git log --oneline --no-merges "${PREV_TAG}..HEAD") + fi + { + echo "## What's Changed (Canary)" + echo "" + echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done + echo "" + echo "---" + echo "**Canary build — pre-release, may be unstable**" + echo "" + echo "**Requires Apple Silicon (M1/M2/M3)**" + echo "" + echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*" + } > release_notes.md + + - name: Build latest-canary.json + run: | + VERSION="${{ needs.version.outputs.version }}" + TAG="${{ needs.version.outputs.tag }}" + REPO="refactoringhq/laputa-app" + + ARM_SIG=$(cat updater-aarch64/*.app.tar.gz.sig) + ARM_TARBALL=$(ls updater-aarch64/*.app.tar.gz | xargs basename) + + cat > latest-canary.json << EOF + { + "version": "${VERSION}", + "notes": "Canary build. See https://refactoringhq.github.io/laputa-app/ for release notes.", + "pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "platforms": { + "darwin-aarch64": { + "signature": "${ARM_SIG}", + "url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}" + } + } + } + EOF + echo "latest-canary.json:"; cat latest-canary.json + + - name: Publish GitHub Release (prerelease) + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.version.outputs.tag }} + name: Laputa ${{ needs.version.outputs.version }} (Canary) + body_path: release_notes.md + draft: false + prerelease: true + files: | + dmg-aarch64/*.dmg + updater-aarch64/*.app.tar.gz + updater-aarch64/*.app.tar.gz.sig + latest-canary.json + + # ───────────────────────────────────────────────────────────── + # Phase 4: Update GitHub Pages (preserve stable latest.json) + # ───────────────────────────────────────────────────────────── + pages: + name: Update release history page + needs: [version, release] + runs-on: ubuntu-latest + permissions: + contents: write + concurrency: + group: github-pages + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + + - name: Build release history page + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mkdir -p _site + gh api repos/${{ github.repository }}/releases --paginate > _site/releases.json + # Download stable latest.json from existing GH Pages (preserve it) + curl -fsSL "https://refactoringhq.github.io/laputa-app/latest.json" -o _site/latest.json || echo '{}' > _site/latest.json + # Copy canary latest.json from this release + gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "latest-canary.json" --output _site/latest-canary.json || true + cat > _site/index.html << 'HTMLEOF' + + +
+ + +Auto-updated on every release
+ + + + + HTMLEOF + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./_site + commit_message: "Update release history for canary ${{ needs.version.outputs.tag }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70515410..c87a8433 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -216,6 +216,9 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + concurrency: + group: github-pages + cancel-in-progress: false steps: - uses: actions/checkout@v4 @@ -227,6 +230,8 @@ jobs: gh api repos/${{ github.repository }}/releases --paginate > _site/releases.json # Copy latest.json to GitHub Pages for auto-updater endpoint gh release download --repo ${{ github.repository }} --pattern "latest.json" --output _site/latest.json || true + # Preserve canary latest.json from existing GH Pages + curl -fsSL "https://refactoringhq.github.io/laputa-app/latest-canary.json" -o _site/latest-canary.json || true cat > _site/index.html << 'HTMLEOF' diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index ed0a49c8..6b271770 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -605,3 +605,18 @@ Managed by `useSettings` hook and `SettingsPanel` component. ### Tauri Commands - **`reinit_telemetry`** — Re-reads settings and toggles Rust Sentry on/off. Called from frontend when user changes crash reporting setting. + +--- + +## Update Channels & Feature Flags + +### Settings +- **`update_channel`** — `"stable"` (default/null) or `"canary"`. Stored in `Settings` struct, configurable in Settings panel under "Updates" section. + +### Hooks +- **`useUpdater(channel?)`** — Checks for updates. For stable: uses Tauri updater plugin. For canary: fetches `latest-canary.json` and opens release page for download. +- **`useFeatureFlag(flag)`** — Returns boolean for a named feature flag. Checks `localStorage` override (`ff_