The default GITHUB_TOKEN cannot trigger downstream workflows (GitHub prevents the chain to avoid infinite loops). publish-tag's `git push origin "$TAG"` therefore never fires release.yml's `push: tags:` listener — observed on v0.1.0-dev.50, where the tag landed but the Release object was missing until manually created. Fold the Release creation into the publish-tag job using `gh release create` with GH_TOKEN=secrets.GITHUB_TOKEN. Direct API calls work fine with the default token; only the push→workflow chain is blocked. - Idempotent: skips if release already exists. - Notes: auto-generated from `git log $prev..$TAG` plus a Verification block linking back to the CI run. - Prerelease flag respects steps.version.outputs.prerelease (true for dev/rc tags, false for vX.Y.Z final tags on main). release.yml stays untouched as a fallback for manually-pushed tags (humans pushing tags directly CAN trigger workflows). Closes #186
482 lines
21 KiB
YAML
482 lines
21 KiB
YAML
# Main CI: build + test + boundary gates for every PR and push to long-lived branches.
|
|
#
|
|
# Branch strategy (defaults — override by editing the branches: lists):
|
|
# - feature/*, fix/* PR-only checks, no tagging
|
|
# - develop full CI + pre-release tag v0.X.Y-dev.<run_number>
|
|
# - rc/*, release/* full CI + tag v0.X.Y-rc.N (N auto-increments)
|
|
# - main full CI + tag v0.X.Y + GitHub Release (release.yml)
|
|
#
|
|
# Versioning: read commits since previous tag, bump per Conventional Commits
|
|
# (feat -> minor, fix -> patch, feat! / BREAKING CHANGE -> major). Defaults to
|
|
# v0.1.0 when no tags exist.
|
|
#
|
|
# Untrusted input handling: this workflow does NOT consume PR titles, bodies,
|
|
# issue text, or commit-author fields directly in run: blocks. The only
|
|
# external inputs are github.ref (controlled) and the constrained
|
|
# workflow_dispatch choice. github.run_number is GitHub-provided.
|
|
|
|
name: CI
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [develop, main, "rc/*", "release/*"]
|
|
push:
|
|
branches: [develop, main, "rc/*", "release/*"]
|
|
workflow_dispatch:
|
|
inputs:
|
|
run_real_hardware:
|
|
description: "Run JARVIS_REAL_MODELS / JARVIS_REAL_CAMERA suites (self-hosted only)"
|
|
required: false
|
|
default: "false"
|
|
type: choice
|
|
options:
|
|
- "false"
|
|
- "true"
|
|
|
|
concurrency:
|
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: write
|
|
pull-requests: read
|
|
checks: write
|
|
|
|
jobs:
|
|
boundary-gates:
|
|
name: Boundary gates
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Run all scripts/check-*.sh (excluding app-builds)
|
|
run: |
|
|
fail=0
|
|
for s in scripts/check-*.sh; do
|
|
name="$(basename "$s")"
|
|
if [ "$name" = "check-app-builds.sh" ]; then
|
|
echo "::group::SKIP $name (needs xcodebuild)"
|
|
echo "::endgroup::"
|
|
continue
|
|
fi
|
|
echo "::group::$name"
|
|
if bash "$s"; then
|
|
echo "PASS: $name"
|
|
else
|
|
echo "::error::FAIL: $name"
|
|
fail=1
|
|
fi
|
|
echo "::endgroup::"
|
|
done
|
|
exit "$fail"
|
|
|
|
webview:
|
|
name: Webview (pnpm test + build)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v4
|
|
with:
|
|
version: 10.30.2
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "20"
|
|
cache: "pnpm"
|
|
cache-dependency-path: webview/pnpm-lock.yaml
|
|
- name: pnpm install
|
|
working-directory: webview
|
|
run: pnpm install --frozen-lockfile
|
|
# Build before typecheck: @jarvis/bus emits its dist/index.d.ts here,
|
|
# which packages/hud's TS imports require. scripts/build-webview.sh
|
|
# also rsyncs the production bundle to App/Resources/webview/ so the
|
|
# downstream SPM jobs (via the artifact below) and app-build (xcodebuild)
|
|
# both find a working HUD bundle.
|
|
- name: Build HUD bundle (produces @jarvis/bus dist + App/Resources/webview/)
|
|
run: bash scripts/build-webview.sh
|
|
# Typecheck is blocking. The historical ~30 errors reported in CI run
|
|
# 25797533005 were almost entirely downstream of `TS2307: Cannot find
|
|
# module '@jarvis/bus'` and disappear once `build-webview.sh` runs above
|
|
# (which produces `packages/bus/dist/index.d.ts`). The one real strict-
|
|
# undefined error in SegmentedRing.tsx was fixed in PR #165.
|
|
- name: Typecheck
|
|
working-directory: webview
|
|
run: pnpm -r typecheck
|
|
- name: Test (vitest)
|
|
working-directory: webview
|
|
run: pnpm -r test
|
|
# Artifact is the production-stamped App/Resources/webview/ tree.
|
|
# Downstream SPM + app-build jobs depend on this artifact name.
|
|
- name: Upload App/Resources/webview artifact
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: webview-resources
|
|
path: App/Resources/webview/
|
|
retention-days: 7
|
|
if-no-files-found: error
|
|
|
|
swift-packages:
|
|
name: SPM ${{ matrix.package }}
|
|
# Self-hosted on Grogu (labels: self-hosted, macOS, ARM64, jarvis) per #180.
|
|
# The github-hosted macos-15 pool was the rate-limiting step for v0.1:
|
|
# 13 matrix jobs + 1 app-build job all needed macOS slots, GitHub
|
|
# scheduled them 1-3 at a time, and total wall-clock was ~1-2h per run
|
|
# versus actual work of <10m. Self-hosted eliminates the queue.
|
|
runs-on: [self-hosted, macOS, jarvis]
|
|
# Security gate: self-hosted runners + public repo would let a fork PR
|
|
# execute arbitrary code on this machine. Push events only ever come from
|
|
# maintainers (branch protection blocks unauthorized writes). PRs are
|
|
# only allowed onto the self-hosted runner when they originate from a
|
|
# branch on this same repo (i.e., not a fork). Also gates manual dispatch.
|
|
if: |
|
|
github.event_name == 'push' ||
|
|
github.event_name == 'workflow_dispatch' ||
|
|
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
|
|
needs: [webview]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
# VoiceLog removed: no `packages/VoiceLog` directory exists in the
|
|
# repo. The matrix entry was a phantom that failed with `chdir error:
|
|
# No such file or directory`. Voice-log work tracked under v1.1.
|
|
package:
|
|
- AgentCore
|
|
- Bus
|
|
- Config
|
|
- DevOverlay
|
|
- Harness
|
|
- Keychain
|
|
- Logging
|
|
- MCP
|
|
- Memory
|
|
- Replay
|
|
- Shell
|
|
- Vision
|
|
- Voice
|
|
steps:
|
|
# Grogu is a shared workstation, not an ephemeral VM. Interactive
|
|
# Xcode test sessions, debugging, or aborted `swift test` runs can
|
|
# leave orphan xctest/swift-test/swift-driver processes around for
|
|
# hours, holding Mach services + sockets + ports that a new
|
|
# WKWebView mount or test-target launch then blocks on (observed:
|
|
# AgentCore hung 34min on develop CI 25824607025 with 6 zombie
|
|
# xctests aged 22-24h from prior interactive use).
|
|
#
|
|
# Age filter (>= 1 hour): Grogu now has SIBLING runners
|
|
# (Grogu-jarvis, -2, -3) so concurrent CI jobs can be in flight
|
|
# simultaneously. CI jobs complete in <10 min, so a 1-hour cutoff
|
|
# safely reaps interactive-use orphans without killing concurrent
|
|
# CI work on sibling runners. `ps -o etime` formats: `MM:SS`
|
|
# (<1h, skip), `HH:MM:SS` (>=1h, kill), `DD-HH:MM:SS` (days, kill).
|
|
# github-hosted VMs start clean, so this step is gated to self-hosted.
|
|
- name: Reap orphan xctest/swift-test processes older than 1h (self-hosted only)
|
|
if: runner.environment == 'self-hosted'
|
|
run: |
|
|
age_filter='/(xctest|swift-test|swift-driver)$/ && !/awk/ && (match($2, /-/) || match($2, /[0-9]+:[0-9]+:[0-9]+/)) {print $1}'
|
|
/bin/ps -A -o pid,etime,comm \
|
|
| awk "$age_filter" \
|
|
| xargs -r -I{} kill -TERM {} 2>/dev/null || true
|
|
sleep 2
|
|
/bin/ps -A -o pid,etime,comm \
|
|
| awk "$age_filter" \
|
|
| xargs -r -I{} kill -9 {} 2>/dev/null || true
|
|
echo "Surviving xctest/swift-test processes (any age — includes concurrent CI work on sibling runners):"
|
|
/bin/ps -A -o pid,etime,comm | grep -E '(xctest|swift-test|swift-driver)$' || echo " (none)"
|
|
- uses: actions/checkout@v4
|
|
# Project targets macOS 26 Tahoe + Swift 6.2/6.3. Xcode 16 (Swift 6.0)
|
|
# cannot parse `mlx-audio-swift` v0.1.2's `swift-tools-version:6.2`,
|
|
# lacks the `withThrowingTaskGroup` overload used by MCP `swift-sdk`
|
|
# 0.12.0, and rejects `@MainActor` XCTestCase `super.setUp()` patterns
|
|
# that Swift 6.1+ accepts. The first two paths cover github-hosted
|
|
# macos-15 (which ships Xcode 26.3); the bare `Xcode.app` fallback
|
|
# covers self-hosted runners with whatever local Xcode 26.x is the
|
|
# default (Grogu currently has 26.5). Using DEVELOPER_DIR (per-process)
|
|
# rather than `sudo xcode-select -s` (system-wide, needs passwordless
|
|
# sudo on self-hosted) — both paths respect DEVELOPER_DIR identically.
|
|
- name: Select Xcode
|
|
run: |
|
|
for path in \
|
|
/Applications/Xcode_26.3.app/Contents/Developer \
|
|
/Applications/Xcode_26.app/Contents/Developer \
|
|
/Applications/Xcode.app/Contents/Developer; do
|
|
if [ -d "$path" ]; then
|
|
echo "DEVELOPER_DIR=$path" >> "$GITHUB_ENV"
|
|
echo "Using Xcode at: $path"
|
|
exit 0
|
|
fi
|
|
done
|
|
echo "No Xcode 26.x found in any expected path" >&2
|
|
exit 1
|
|
- name: Show Swift toolchain
|
|
run: |
|
|
swift --version
|
|
xcodebuild -version
|
|
# Download the production webview bundle so Bus + downstream packages
|
|
# (AgentCore / Harness / MCP / Voice) can mount a real WKWebView with
|
|
# a working JS bridge. Without this, BusTests fail with `Error: bus
|
|
# not mounted` because index.html + the JS bundle are missing from
|
|
# App/Resources/webview/.
|
|
- name: Download webview resources
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: webview-resources
|
|
path: App/Resources/webview/
|
|
# actions/cache@v4 restores the package's `.build/` and SwiftPM's
|
|
# global cache. On github-hosted this is a real perf win; on self-hosted
|
|
# it's both unnecessary (the _work/ dir persists between jobs naturally)
|
|
# and actively harmful (caches saved on github-hosted have absolute
|
|
# paths like /Users/runner/work/Jarvis/.../onnxruntime.xcframework
|
|
# baked into .build/artifacts, which don't resolve on Grogu and break
|
|
# Voice + Harness with "XCFramework Info.plist not found"). Restrict
|
|
# the cache action to github-hosted only.
|
|
- name: Cache SPM build artifacts (github-hosted only)
|
|
if: runner.environment == 'github-hosted'
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
packages/${{ matrix.package }}/.build
|
|
~/Library/Caches/org.swift.swiftpm
|
|
key: spm-${{ matrix.package }}-${{ runner.os }}-${{ hashFiles(format('packages/{0}/Package.resolved', matrix.package), format('packages/{0}/Package.swift', matrix.package)) }}
|
|
restore-keys: |
|
|
spm-${{ matrix.package }}-${{ runner.os }}-
|
|
# On self-hosted (Grogu) the runner is a LaunchAgent with no UI session,
|
|
# so SecItemAdd against the locked login keychain returns
|
|
# errSecInteractionNotAllowed (-25308). Use a pre-created dedicated
|
|
# keychain `jarvis-runner.keychain-db` (one-time setup on Grogu) and
|
|
# prepend it to the user search list for this run so test SecItemAdd
|
|
# calls without an explicit keychain target write there. github-hosted
|
|
# bakes its own unlocked keychain into the runner image and skips this.
|
|
- name: Unlock runner keychain (self-hosted only)
|
|
if: runner.environment == 'self-hosted'
|
|
env:
|
|
KEYCHAIN_PASSWORD: ${{ secrets.JARVIS_RUNNER_KEYCHAIN_PASSWORD }}
|
|
run: |
|
|
KEYCHAIN_PATH="$HOME/Library/Keychains/jarvis-runner.keychain-db"
|
|
if [ ! -f "$KEYCHAIN_PATH" ]; then
|
|
echo "FATAL: runner keychain missing at $KEYCHAIN_PATH" >&2
|
|
echo " Create with: security create-keychain -p \"\$KEYCHAIN_PASSWORD\" \"$KEYCHAIN_PATH\"" >&2
|
|
exit 1
|
|
fi
|
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
|
# NOTE: do NOT re-run `set-keychain-settings` here — the Security
|
|
# framework requires user interaction for that operation even with
|
|
# the keychain unlocked, and the LaunchAgent has no UI. The
|
|
# settings (no-timeout, no-lock-on-sleep) were applied once at
|
|
# keychain-creation time and persist; we just need to unlock.
|
|
security list-keychains -d user -s "$KEYCHAIN_PATH" "$HOME/Library/Keychains/login.keychain-db"
|
|
# On the legacy file-based macOS keychain, SecItemAdd writes to the
|
|
# user's DEFAULT keychain — the search list only determines read
|
|
# order. Changing list-keychains alone leaves SecItemAdd targeting
|
|
# login.keychain-db (locked, returns -25308). Also redirect the
|
|
# default. SIDE EFFECT documented in CLAUDE.md: this is persistent
|
|
# for james.maes's user prefs.
|
|
security default-keychain -d user -s "$KEYCHAIN_PATH"
|
|
- name: swift test
|
|
run: swift test --package-path packages/${{ matrix.package }}
|
|
|
|
app-build:
|
|
name: App target (xcodebuild)
|
|
runs-on: macos-15
|
|
needs: [boundary-gates, webview]
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
# Same Xcode 26.x pin as swift-packages — Xcode 16 cannot resolve
|
|
# mlx-audio-swift's swift-tools-version:6.2 manifest.
|
|
- name: Select Xcode
|
|
run: sudo xcode-select -s /Applications/Xcode_26.3.app/Contents/Developer || sudo xcode-select -s /Applications/Xcode_26.app/Contents/Developer
|
|
- name: Install xcodegen
|
|
run: brew install xcodegen
|
|
# pnpm + node need to be on PATH because Xcode.app's project has a
|
|
# build-phase script (Script-4B80D79B2A0254274258618C.sh) that calls
|
|
# scripts/build-webview.sh during the build itself. Even though we
|
|
# download the prebuilt artifact below, the build phase still runs
|
|
# and re-stamps the bundle — and would error with
|
|
# "[build-webview] FATAL: pnpm not found on PATH" without these.
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v4
|
|
with:
|
|
version: 10.30.2
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "20"
|
|
cache: "pnpm"
|
|
cache-dependency-path: webview/pnpm-lock.yaml
|
|
# Reuse the webview build from the upstream job rather than rebuilding.
|
|
# The Xcode build phase will re-stamp these on top.
|
|
- name: Download webview resources
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: webview-resources
|
|
path: App/Resources/webview/
|
|
- name: check-app-builds.sh
|
|
run: bash scripts/check-app-builds.sh
|
|
- name: verify-codesign-settings.sh
|
|
run: bash scripts/verify-codesign-settings.sh
|
|
- name: Upload Jarvis.app artifact
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: Jarvis-app-debug
|
|
path: build/Build/Products/Debug/Jarvis.app
|
|
retention-days: 7
|
|
if-no-files-found: warn
|
|
|
|
real-hardware:
|
|
name: Real-hardware tests (opt-in)
|
|
runs-on: [self-hosted, macOS]
|
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.run_real_hardware == 'true'
|
|
needs: [swift-packages]
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: JARVIS_REAL_MODELS Memory regression corpus
|
|
env:
|
|
JARVIS_REAL_MODELS: "1"
|
|
run: swift test --package-path packages/Memory --filter MemoryRegressionCorpusTests
|
|
- name: JARVIS_REAL_CAMERA capture suite
|
|
env:
|
|
JARVIS_REAL_CAMERA: "1"
|
|
run: swift test --package-path packages/Vision --filter CameraCaptureRealHardwareTests
|
|
- name: JARVIS_REAL_MODELS Orpheus TTFA
|
|
env:
|
|
JARVIS_REAL_MODELS: "1"
|
|
run: swift test --package-path packages/Voice --filter OrpheusTTFATests
|
|
|
|
publish-tag:
|
|
name: Publish tag
|
|
runs-on: ubuntu-latest
|
|
needs: [boundary-gates, webview, swift-packages, app-build]
|
|
if: |
|
|
github.event_name == 'push' && (
|
|
github.ref == 'refs/heads/develop' ||
|
|
github.ref == 'refs/heads/main' ||
|
|
startsWith(github.ref, 'refs/heads/rc/') ||
|
|
startsWith(github.ref, 'refs/heads/release/')
|
|
)
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
- name: Compute next version
|
|
id: version
|
|
env:
|
|
GH_REF: ${{ github.ref }}
|
|
RUN_NUMBER: ${{ github.run_number }}
|
|
run: |
|
|
set -euo pipefail
|
|
git fetch --tags --force --quiet
|
|
last_tag=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' \
|
|
| grep -Ev -- '-(dev|rc|alpha|beta)' \
|
|
| sort -V | tail -n 1 || true)
|
|
if [ -z "$last_tag" ]; then
|
|
base="v0.1.0"
|
|
from="$(git rev-list --max-parents=0 HEAD | tail -n 1)"
|
|
echo "No prior tag; defaulting base=$base, scanning all history."
|
|
else
|
|
base="$last_tag"
|
|
from="$last_tag"
|
|
echo "Last final tag: $last_tag"
|
|
fi
|
|
version="${base#v}"
|
|
IFS='.' read -r major minor patch <<< "$version"
|
|
bump="patch"
|
|
if git log "$from"..HEAD --pretty=%B 2>/dev/null | grep -qE '(^|\n)BREAKING CHANGE:|^[a-zA-Z]+(\([^)]+\))?!:'; then
|
|
bump="major"
|
|
elif git log "$from"..HEAD --pretty=%B 2>/dev/null | grep -qE '^feat(\([^)]+\))?:'; then
|
|
bump="minor"
|
|
elif git log "$from"..HEAD --pretty=%B 2>/dev/null | grep -qE '^fix(\([^)]+\))?:'; then
|
|
bump="patch"
|
|
fi
|
|
if [ -n "$last_tag" ]; then
|
|
case "$bump" in
|
|
major) major=$((major+1)); minor=0; patch=0 ;;
|
|
minor) minor=$((minor+1)); patch=0 ;;
|
|
patch) patch=$((patch+1)) ;;
|
|
esac
|
|
fi
|
|
next="${major}.${minor}.${patch}"
|
|
case "$GH_REF" in
|
|
refs/heads/develop)
|
|
tag="v${next}-dev.${RUN_NUMBER}"
|
|
prerelease=true
|
|
;;
|
|
refs/heads/rc/*|refs/heads/release/*)
|
|
existing_rc=$(git tag --list "v${next}-rc.*" | sed -E "s/.*-rc\.([0-9]+)/\1/" | sort -n | tail -n 1)
|
|
n=$(( ${existing_rc:-0} + 1 ))
|
|
tag="v${next}-rc.${n}"
|
|
prerelease=true
|
|
;;
|
|
refs/heads/main)
|
|
tag="v${next}"
|
|
prerelease=false
|
|
;;
|
|
*)
|
|
echo "Unexpected ref: $GH_REF" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
|
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
|
|
echo "Next tag: $tag (prerelease=$prerelease)"
|
|
- name: Create + push tag
|
|
env:
|
|
TAG: ${{ steps.version.outputs.tag }}
|
|
GH_REF: ${{ github.ref }}
|
|
run: |
|
|
set -euo pipefail
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
if git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then
|
|
echo "Tag $TAG already exists; skipping."
|
|
exit 0
|
|
fi
|
|
git tag -a "$TAG" -m "Automated tag from CI for $GH_REF"
|
|
git push origin "$TAG"
|
|
# GitHub Actions GITHUB_TOKEN cannot trigger downstream workflows. The
|
|
# separate `release.yml` workflow listens for `push: tags:` events but
|
|
# never sees a CI-pushed tag — observed on v0.1.0-dev.50 (tag created,
|
|
# Release object missing, had to be manually created). Create the
|
|
# Release object directly here in the same job so the Releases page
|
|
# reflects CI-pushed tags without a manual step. `release.yml` stays
|
|
# as a fallback for manually-pushed tags (which CAN trigger workflows).
|
|
- name: Create GitHub Release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG: ${{ steps.version.outputs.tag }}
|
|
PRERELEASE: ${{ steps.version.outputs.prerelease }}
|
|
RUN_URL: ${{ format('https://github.com/{0}/actions/runs/{1}', github.repository, github.run_id) }}
|
|
run: |
|
|
set -euo pipefail
|
|
if ! git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then
|
|
echo "Tag $TAG not present locally; nothing to release." >&2
|
|
exit 0
|
|
fi
|
|
if gh release view "$TAG" >/dev/null 2>&1; then
|
|
echo "Release $TAG already exists — skipping create (idempotent)."
|
|
exit 0
|
|
fi
|
|
prev=$(git tag --list 'v*' --sort=-v:refname | grep -v "^${TAG}$" | head -n 1 || true)
|
|
notes_file=$(mktemp)
|
|
{
|
|
echo "## Changes"
|
|
echo
|
|
if [ -n "$prev" ]; then
|
|
git log --pretty='format:- %s (%h)' "${prev}..${TAG}"
|
|
else
|
|
git log --pretty='format:- %s (%h)' -20
|
|
fi
|
|
echo
|
|
echo
|
|
echo "## Verification"
|
|
echo
|
|
echo "- CI build: $RUN_URL"
|
|
echo "- Boundary gates, webview, app-build, 13/13 SPM packages: all green on this commit"
|
|
} > "$notes_file"
|
|
flag=()
|
|
if [ "$PRERELEASE" = "true" ]; then
|
|
flag+=("--prerelease")
|
|
fi
|
|
gh release create "$TAG" \
|
|
--title "$TAG" \
|
|
--notes-file "$notes_file" \
|
|
"${flag[@]}"
|