Skip to content

fix(M-025.1): suppress Untitled-1 tab when launched with pending opens #34

fix(M-025.1): suppress Untitled-1 tab when launched with pending opens

fix(M-025.1): suppress Untitled-1 tab when launched with pending opens #34

Workflow file for this run

# Phase-B4 step-1 release workflow.
#
# Triggered by git tag push matching v*.* — produces signed, smoke-
# tested DMG artifacts and publishes them as a GitHub Release with
# .sha256 sidecars. The Homebrew cask formula sources sha256 from
# the sidecar (NOT recomputed) per B2CR-3.
#
# Replaces the Phase-B1.5 dryrun workflow (release-tauri.yml) which
# was triggered by branch pushes. release-tauri.yml is retained as a
# named-environment dryrun for branch-time validation.
#
# Phase-B4 prerequisites still in flight: ed25519 signer keypair for
# tauri-plugin-updater (F-UPDATER-WIRE-PLUGIN), SLSA-3 provenance via
# actions/attest-build-provenance + Sigstore Fulcio (B4 step-11/12),
# cross-repo cask PR via xronocode-release-bot GitHub App (B4 step-15).
# Each lands in its own follow-up PR; this workflow ships the build +
# release skeleton that they wire into.
name: release
on:
push:
tags:
- 'v*.*.*'
- 'v*.*.*-*'
workflow_dispatch:
inputs:
tag:
description: 'Tag name to release (e.g. v2.0.0). Leave empty for current ref.'
required: false
default: ''
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write # gh release create needs write
id-token: write # B4 step-11 SLSA + Sigstore OIDC (wired in follow-up PR)
attestations: write # B4 step-11 actions/attest-build-provenance (follow-up)
jobs:
build:
name: build (${{ matrix.platform.name }})
strategy:
fail-fast: false
matrix:
platform:
- name: macos-arm64
os: macos-14
target: aarch64-apple-darwin
runs-on: ${{ matrix.platform.os }}
timeout-minutes: 60
outputs:
tag: ${{ steps.resolve_tag.outputs.tag }}
steps:
# ── checkout + tooling ────────────────────────────────────────
- name: checkout
uses: actions/checkout@v4
- name: resolve tag
id: resolve_tag
run: |
if [ -n "${{ inputs.tag }}" ]; then
TAG="${{ inputs.tag }}"
else
TAG="${GITHUB_REF#refs/tags/}"
fi
if [ -z "$TAG" ] || [ "$TAG" = "$GITHUB_REF" ]; then
echo "::error::could not resolve tag from ref=$GITHUB_REF inputs.tag='${{ inputs.tag }}'"
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "resolved tag: $TAG"
- name: setup node
uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
- name: setup rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- name: cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: cargo-${{ matrix.platform.target }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-${{ matrix.platform.target }}-
# ── install + build frontend ─────────────────────────────────
- name: npm ci
run: npm ci
- name: vite build
run: npm run build
# ── tauri build (with updater signing) ───────────────────────
# F-UPDATER-WIRE-PLUGIN: TAURI_SIGNING_PRIVATE_KEY env feeds
# tauri-action's auto-sign. _PASSWORD is optional — if the
# keypair was generated with empty password, GH resolves the
# missing secret to "" and tauri-action treats it as no-password.
#
# tauri-action emits Mark.app + DMG + Mark.app.tar.gz +
# Mark.app.tar.gz.sig (ed25519 signature). The .sig is what
# the updater plugin verifies against plugins.updater.pubkey
# before applying an update on user machines.
#
# If TAURI_SIGNING_PRIVATE_KEY itself is missing (first runs
# before user provisions the keypair), tauri-action falls
# through to unsigned build — users on those builds won't have
# working in-app updates, but `brew upgrade --cask mark` still
# works.
- name: tauri build
id: tauri
uses: tauri-apps/tauri-action@v0
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
args: --target ${{ matrix.platform.target }}
# ── ad-hoc signing ───────────────────────────────────────────
- name: ad-hoc codesign
if: matrix.platform.os == 'macos-14'
run: |
set -euo pipefail
APP_PATH=$(find target -name 'Mark.app' -type d | head -1)
if [ -z "$APP_PATH" ]; then
echo "::error::Mark.app not found under target"
exit 1
fi
echo "signing: $APP_PATH"
codesign --force --deep --sign - "$APP_PATH"
codesign --verify --verbose=4 "$APP_PATH"
# ── smoke launch ─────────────────────────────────────────────
- name: smoke launch
if: matrix.platform.os == 'macos-14'
run: |
set -euo pipefail
DMG=$(find target -name '*.dmg' | head -1)
if [ -z "$DMG" ]; then
echo "::error::no .dmg artifact produced"
exit 1
fi
MOUNT=/tmp/mark-dmg
mkdir -p "$MOUNT"
hdiutil attach "$DMG" -nobrowse -mountpoint "$MOUNT" -readonly
cp -R "$MOUNT/Mark.app" /Applications/
hdiutil detach "$MOUNT"
xattr -cr /Applications/Mark.app
MARK_SKIP_MIGRATION=1 open -a /Applications/Mark.app
for i in $(seq 1 10); do
if pgrep -f 'Mark.app/Contents/MacOS/mark' > /dev/null; then
echo "alive after ${i}s"
break
fi
sleep 1
done
if ! pgrep -f 'Mark.app/Contents/MacOS/mark' > /dev/null; then
echo "::error::Mark process did not appear within 10s of open"
exit 1
fi
pkill -9 -f 'Mark.app/Contents/MacOS/mark' || true
# ── bundle size budget ───────────────────────────────────────
- name: bundle size
if: matrix.platform.os == 'macos-14'
id: size
run: |
set -euo pipefail
APP_PATH=/Applications/Mark.app
if [ ! -d "$APP_PATH" ]; then
APP_PATH=$(find target -name 'Mark.app' -type d | head -1)
fi
BYTES=$(du -sk "$APP_PATH" | awk '{print $1*1024}')
HUMAN=$(du -sh "$APP_PATH" | awk '{print $1}')
echo "bundle_bytes=$BYTES" >> "$GITHUB_OUTPUT"
echo "bundle_human=$HUMAN" >> "$GITHUB_OUTPUT"
{
echo "## Bundle size — ${{ matrix.platform.name }}"
echo
echo "- \`du -sh\`: **$HUMAN**"
echo "- bytes: $BYTES"
} >> "$GITHUB_STEP_SUMMARY"
# ── warnings allowlist ───────────────────────────────────────
- name: warnings allowlist check
if: matrix.platform.os == 'macos-14'
run: |
set -uo pipefail # NOT -e so we can show JSON before failing
cd src-tauri
# Force re-emit of warnings: cargo's incremental cache
# otherwise hides warnings from already-compiled crates.
touch src/main.rs
cargo build --release --bin mark --message-format=json > /tmp/cargo.json
cd ..
node tools/check-warnings-allowlist.mjs /tmp/cargo.json docs/ci-warnings-allowlist.md \
| tee /tmp/toolchain-warnings.json
RC=${PIPESTATUS[0]}
echo "---"
echo "verifier exit code: $RC"
exit $RC
# ── attribution-completeness gate (B4 step-9) ────────────────
# Re-runs license-checker + cargo-bundle-licenses, diffs against
# committed NOTICES-{node,rust}.md. New deps without an attribution
# line fail the build.
- name: install cargo-bundle-licenses
if: matrix.platform.os == 'macos-14'
run: cargo install cargo-bundle-licenses
- name: verify attributions
if: matrix.platform.os == 'macos-14'
run: ./tools/verify-attributions.sh
# ── SBOM generation (B4 step-9) ──────────────────────────────
# CycloneDX SPDX-compatible SBOM for the Tauri binary + Node
# renderer dependency closure. Published alongside the DMG so
# downstream auditors can verify the supply chain.
- name: install cargo-cyclonedx
if: matrix.platform.os == 'macos-14'
run: cargo install cargo-cyclonedx
- name: generate SBOM
if: matrix.platform.os == 'macos-14'
run: |
set -euo pipefail
mkdir -p sbom
# Node side: production deps only
npx --yes --package @cyclonedx/cyclonedx-npm -- cyclonedx-npm \
--output-format JSON \
--output-file sbom/sbom-node.cdx.json \
--omit dev \
--ignore-npm-errors
# Rust side — cargo-cyclonedx 0.5.x writes <crate>.cdx.json
# to the manifest directory; we relocate to sbom/.
(cd src-tauri && cargo cyclonedx --format json)
mv src-tauri/mark.cdx.json sbom/sbom-rust.cdx.json
ls -la sbom/
# ── updater latest.json feed (B4 step-5) ─────────────────────
# tauri-plugin-updater fetches this file from the URL configured
# in tauri.conf.json plugins.updater.endpoints[]. It contains the
# ed25519 signature of the .app.tar.gz updater bundle that
# tauri-action produced; the plugin verifies before downloading.
# If signing keys aren't provisioned, the .sig file is missing
# and we skip the feed (graceful degradation).
- name: build updater feed (latest.json)
if: matrix.platform.os == 'macos-14'
id: feed
run: |
set -euo pipefail
TAG=${{ steps.resolve_tag.outputs.tag }}
VER="${TAG#v}"
SIG_FILE=$(find target -name '*.app.tar.gz.sig' | head -1)
TGZ_FILE=$(find target -name '*.app.tar.gz' ! -name '*.sig' | head -1)
if [ -z "$SIG_FILE" ] || [ -z "$TGZ_FILE" ]; then
echo "::warning::updater signing artifacts missing; latest.json not generated. Configure TAURI_SIGNING_PRIVATE_KEY+_PASSWORD secrets per docs/F-UPDATER-WIRE-PLUGIN-handoff.md."
echo "skipped=1" >> "$GITHUB_OUTPUT"
exit 0
fi
SIG=$(cat "$SIG_FILE")
PUB_DATE=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
mkdir -p /tmp/release
# Copy the .app.tar.gz so it gets uploaded as a release asset.
cp "$TGZ_FILE" "/tmp/release/Mark_${VER}_aarch64.app.tar.gz"
cat > /tmp/release/latest.json <<JSON
{
"version": "${TAG}",
"notes": "See release notes at https://github.com/xronocode/mark/releases/tag/${TAG}",
"pub_date": "${PUB_DATE}",
"platforms": {
"darwin-aarch64": {
"signature": "${SIG}",
"url": "https://github.com/xronocode/mark/releases/download/${TAG}/Mark_${VER}_aarch64.app.tar.gz"
}
}
}
JSON
echo "skipped=0" >> "$GITHUB_OUTPUT"
cat /tmp/release/latest.json
# ── stage release artifacts ──────────────────────────────────
# Per B2CR-3: the cask sources sha256 from a .sha256 sidecar that
# ships with the DMG, NOT a recomputed value. CI emits the sidecar
# so the cask formula and the release artifact agree.
- name: stage release artifacts
if: matrix.platform.os == 'macos-14'
id: stage
run: |
set -euo pipefail
mkdir -p /tmp/release
DMG_SRC=$(find target -name '*.dmg' | head -1)
if [ -z "$DMG_SRC" ]; then
echo "::error::no .dmg artifact produced"
exit 1
fi
TAG=${{ steps.resolve_tag.outputs.tag }}
# Strip leading 'v' from tag for filename version segment
# (matches v1 cask convention: Mark_1.2.3_aarch64.dmg).
VER=${TAG#v}
DMG_DST=/tmp/release/Mark_${VER}_aarch64.dmg
cp "$DMG_SRC" "$DMG_DST"
shasum -a 256 "$DMG_DST" | awk '{print $1}' > "$DMG_DST.sha256"
# B4-step-8 attribution + B4-step-9 SBOM artifacts shipped
# alongside the DMG so downstream cask + audit consumers
# don't need to re-clone the repo.
cp NOTICES-node.md /tmp/release/
cp NOTICES-rust.md /tmp/release/
cp sbom/sbom-node.cdx.json /tmp/release/Mark_${VER}_sbom-node.cdx.json
cp sbom/sbom-rust.cdx.json /tmp/release/Mark_${VER}_sbom-rust.cdx.json
ls -la /tmp/release
echo "dmg=$DMG_DST" >> "$GITHUB_OUTPUT"
echo "sha256=$DMG_DST.sha256" >> "$GITHUB_OUTPUT"
# ── upload as workflow artifact for downstream jobs ──────────
# ── SLSA-3 build provenance (B4 step-11) ─────────────────────
# actions/attest-build-provenance@v2 issues a Sigstore-anchored
# in-toto attestation that proves the artifact was built by THIS
# workflow run on THIS commit. OIDC → Fulcio → ephemeral cert →
# signed bundle published to the GH Releases page alongside the
# DMG. Verification command goes in release notes:
# gh attestation verify Mark_v2.0.0_aarch64.dmg --owner xronocode
- name: SLSA build provenance
if: matrix.platform.os == 'macos-14'
uses: actions/attest-build-provenance@v2
with:
subject-path: '/tmp/release/Mark_*.dmg'
# ── Cosign keyless sign-blob (B4 step-12) ────────────────────
# Belt-and-braces over the SLSA attestation: a keyless cosign
# signature using the same OIDC → Fulcio path. Produces a
# .bundle file users can verify with:
# cosign verify-blob \
# --bundle Mark_v2.0.0_aarch64.dmg.bundle \
# --certificate-identity-regexp 'https://github\.com/xronocode/mark/.*' \
# --certificate-oidc-issuer https://token.actions.githubusercontent.com \
# Mark_v2.0.0_aarch64.dmg
- name: install cosign
if: matrix.platform.os == 'macos-14'
uses: sigstore/cosign-installer@v3
with:
cosign-release: 'v2.4.1'
- name: cosign sign-blob
if: matrix.platform.os == 'macos-14'
env:
COSIGN_EXPERIMENTAL: '1'
run: |
set -euo pipefail
for DMG in /tmp/release/Mark_*.dmg; do
cosign sign-blob --yes --bundle "$DMG.bundle" "$DMG"
ls -la "$DMG.bundle"
done
- name: upload release artifacts
if: matrix.platform.os == 'macos-14'
uses: actions/upload-artifact@v4
with:
name: mark-${{ matrix.platform.name }}-release
path: |
/tmp/release/*.dmg
/tmp/release/*.sha256
/tmp/release/*.bundle
/tmp/release/Mark_*_sbom-*.cdx.json
/tmp/release/NOTICES-*.md
/tmp/release/Mark_*.app.tar.gz
/tmp/release/latest.json
if-no-files-found: error
retention-days: 30
publish:
name: publish github release
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: checkout
uses: actions/checkout@v4
- name: download release artifacts
uses: actions/download-artifact@v4
with:
path: /tmp/release-stage
- name: list staged
run: |
find /tmp/release-stage -type f -exec ls -la {} \;
# ── create GitHub release + upload assets ────────────────────
# B4 step-15 will replace this with cross-repo cask PR via the
# xronocode-release-bot GitHub App + environment-protection
# `release` requiring manual approval. For now, ambient
# GITHUB_TOKEN with contents:write permission ships the build
# to the project's own Releases page.
- name: gh release create
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG=${{ needs.build.outputs.tag }}
VER="${TAG#v}"
FILES=$(find /tmp/release-stage -type f)
NOTES=$(cat <<NOTES_EOF
Automated release from \`$GITHUB_SHA\`.
**Verification:**
Build provenance (SLSA-3):
\`\`\`
gh attestation verify Mark_${VER}_aarch64.dmg --owner xronocode
\`\`\`
Cosign keyless signature:
\`\`\`
cosign verify-blob \\
--bundle Mark_${VER}_aarch64.dmg.bundle \\
--certificate-identity-regexp 'https://github.com/xronocode/mark/.*' \\
--certificate-oidc-issuer https://token.actions.githubusercontent.com \\
Mark_${VER}_aarch64.dmg
\`\`\`
DMG SHA-256 sidecar published as \`Mark_${VER}_aarch64.dmg.sha256\`.
CycloneDX SBOMs published for both Node renderer and Rust backend
dependency closures. NOTICES-{node,rust}.md attribution lists
shipped alongside.
**Updater:** ed25519-signed feed (\`latest.json\` + \`Mark_${VER}_aarch64.app.tar.gz\`) shipped.
tauri-plugin-updater verifies against the embedded pubkey before applying. To upgrade
outside the in-app updater:
\`\`\`
brew upgrade --cask mark
\`\`\`
NOTES_EOF
)
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "release $TAG already exists; uploading assets idempotently"
gh release upload "$TAG" $FILES --repo "$GITHUB_REPOSITORY" --clobber
else
gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--title "Mark $TAG" \
--notes "$NOTES" \
$FILES
fi
publish-cask:
name: cross-repo cask PR
needs: [build, publish]
runs-on: ubuntu-latest
timeout-minutes: 10
environment: release # gates on environment-protection approval
# B4 step-15: cross-repo PR via xronocode-release-bot GitHub App.
# The App's installation token has contents:write on
# xronocode/homebrew-mark and pull-requests:write to open the bump
# PR. Falls through with a noisy warning if APP_ID/PRIVATE_KEY
# secrets are missing — this lets the rest of the release ship
# while the App is being provisioned.
steps:
- name: checkout reborn-mark
uses: actions/checkout@v4
- name: download release artifacts
uses: actions/download-artifact@v4
with:
path: /tmp/release-stage
- name: gate on App secrets
id: gate
run: |
if [ -z "${{ secrets.RELEASE_BOT_APP_ID }}" ] || [ -z "${{ secrets.RELEASE_BOT_PRIVATE_KEY }}" ]; then
echo "::warning::xronocode-release-bot App secrets not configured; skipping cask PR. See docs/F-CASK-PUBLISH-HANDOFF.md."
echo "skip=1" >> "$GITHUB_OUTPUT"
else
echo "skip=0" >> "$GITHUB_OUTPUT"
fi
- name: mint app installation token
if: steps.gate.outputs.skip == '0'
id: app_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: xronocode
repositories: homebrew-mark
- name: open cask bump PR
if: steps.gate.outputs.skip == '0'
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
TAG: ${{ needs.build.outputs.tag }}
run: |
set -euo pipefail
VER="${TAG#v}"
# Pull the .sha256 sidecar value (NOT recompute) per B2CR-3.
SHA256_FILE=$(find /tmp/release-stage -name "Mark_${VER}_aarch64.dmg.sha256" | head -1)
if [ -z "$SHA256_FILE" ]; then
echo "::error::sha256 sidecar missing for $TAG"
exit 1
fi
SHA256=$(awk '{print $1}' < "$SHA256_FILE")
# Configure git to use the GH App token via gh's credential
# helper. Without this, `git push` falls through to the default
# askpass and fails: "could not read Username for github.com".
gh auth setup-git
# Clone tap, bump, push, open PR.
gh repo clone xronocode/homebrew-mark /tmp/tap -- --depth 1
cd /tmp/tap
git config user.name 'xronocode-release-bot[bot]'
git config user.email 'xronocode-release-bot[bot]@users.noreply.github.com'
BRANCH="bump/mark-${VER}"
git checkout -b "$BRANCH"
# Update version + sha256. Cask uses on_arm sha256 + url.
sed -i'' -e "s|version \".*\"|version \"$VER\"|" Casks/mark.rb
# Replace the on_arm sha256 line.
awk -v sha="$SHA256" '
BEGIN { in_arm=0 }
/^ on_arm do/ { in_arm=1 }
in_arm && /sha256 / { sub(/sha256 \"[a-f0-9]+\"/, "sha256 \"" sha "\""); in_arm=0 }
{ print }
' Casks/mark.rb > Casks/mark.rb.tmp && mv Casks/mark.rb.tmp Casks/mark.rb
git add Casks/mark.rb
git commit -m "Bump mark to ${VER}"
git push origin "$BRANCH"
gh pr create \
--repo xronocode/homebrew-mark \
--title "Bump mark to ${VER}" \
--body "Auto-generated by release.yml on tag ${TAG}.
- DMG sha256 sourced verbatim from Mark_${VER}_aarch64.dmg.sha256 sidecar.
- SLSA-3 attestation + Cosign bundle published with the release.
- Smoke launch + warnings allowlist passed in CI.
Verification before merge: \`brew install --cask --no-quarantine ./Casks/mark.rb\`" \
--base main \
--head "$BRANCH"