mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
[comment audit] Complete repository-wide comment cleanup (#33249)
* [comment audit] Align legacy failure-read counts with cached display * [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci] * [comment audit] Condense shared generation, path and preview comments [skip ci] * [comment audit] Condense host, file and Git contract rationale [skip ci] * [comment audit] Tighten Git lifecycle and provider contract comments [skip ci] * [comment audit] Clarify shared host authority and launch isolation [skip ci] * [comment audit] simplify local access mobile and native chat contracts [skip ci] * [comment audit] simplify native chat presentation and state contracts [skip ci] * [comment audit] simplify native runtime and provider contracts [skip ci] * [comment audit] simplify migration and orchestration contracts [skip ci] * [comment audit] simplify plugin trust and process capture rationale [skip ci] * [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci] * [comment audit] clarify remote recovery and launch wire rationale [skip ci] * [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci] * [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci] * [comment audit] clarify sidebar, skill and SSH invariants [skip ci] * [comment audit] condense structured chat delivery and timing rationale [skip ci] * [comment audit] condense terminal replay and telemetry invariants [skip ci] * [comment audit] clarify launch permission and terminal delivery rationale [skip ci] * [comment audit] clarify workspace ownership and layout contracts [skip ci] * [comment audit] wait for fitted and painted SSH terminal links * [comment audit] clarify layout persistence and session ownership [skip ci] * [comment audit] clarify host routing and workspace lifecycle contracts [skip ci] * [comment audit] clarify startup persistence and app lifetime comments [skip ci] * [comment audit] clarify recovery and activity boundaries [skip ci] * [comment audit] condense activity and artifact invariants [skip ci] * [comment audit] condense automation authority contracts [skip ci] * [comment audit] simplify automation scheduling and history contracts [skip ci] * [comment audit] clarify browser chrome and annotation lifecycles [skip ci] * [comment audit] clarify browser routing and retained guest ownership [skip ci] * [comment audit] clarify browser recovery and document ownership comments [skip ci] * [comment audit] restore hosted runners required by native IME fixtures * [comment audit] shorten preview, palette and dashboard invariants [skip ci] * [comment audit] clarify terminal preview, dashboard and retention comments [skip ci] * [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci] * [comment audit] shorten rich-text and combined-diff ownership comments [skip ci] * [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci] * [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci] * [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci] * [comment audit] clarify rich Markdown source and selection safeguards [skip ci] * [comment audit] clarify editor loading and emulator stream lifetimes [skip ci] * [comment audit] trim emulator and feature tour narration [skip ci] * [comment audit] clarify floating workspace and GitHub cache ownership [skip ci] * [comment audit] trim provider controls and Linear filter explanations [skip ci] * [comment audit] clarify pairing and chat resume lifetimes [skip ci] * [comment audit] clarify chat delivery and transcript lifetimes [skip ci] * [comment audit] clarify session options and host-owned status [skip ci] * [comment audit] clarify draft durability and live-status precedence [skip ci] * [comment audit] Trim chat send and model ownership explanations [skip ci] * [comment audit] Shorten transcript ownership and send recovery comments [skip ci] * [comment audit] Simplify native chat composer and transcript hook comments [skip ci] * [comment audit] Shorten native chat scroll and structured session explanations [skip ci] * [comment audit] Trim workspace picker and onboarding commentary [skip ci] * [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci] * [comment audit] Trim sidebar and session history explanations [skip ci] * [comment audit] Condense session resume and review checks comments [skip ci] * [comment audit] Shorten checks state and file explorer refresh comments [skip ci] * [comment audit] Trim plugin bridge and source-control action rationale [skip ci] * [comment audit] Trim source-control listing and review rationale [skip ci] * [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci] * [comment audit] Condense Settings account and appearance rationale [skip ci] * [comment audit] Trim pairing and repository Settings explanations [skip ci] * [comment audit] Condense Settings search SSH and terminal safeguards [skip ci] * [comment audit] Shorten Settings voice and provider ownership comments [skip ci] * [comment audit] Condense Settings draft persistence and host-state rationale [skip ci] * [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci] * [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci] * [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci] * [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci] * [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci] * [comment audit] Prune focus-read baseline after comment cleanup * [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci] * [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci] * [comment audit] Clarify viewport geometry and skill update safeguards [skip ci] * [comment audit] Tighten skill inventory, usage and host status comments [skip ci] * [comment audit] Clarify usage ownership and resource kill safeguards [skip ci] * [comment audit] Tighten tab selection, rename and search contracts [skip ci] * [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci] * [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci] * [comment audit] Tighten provider loading and terminal recovery comments [skip ci] * [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci] * [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci] * [comment audit] Tighten PTY startup and replay explanations [skip ci] * [comment audit] Clarify PTY identity and delivery ownership [skip ci] * [comment audit] Shorten remote reconnect and input replay rationale [skip ci] * [comment audit] Condense terminal parking clipboard and file link comments [skip ci] * [comment audit] Clarify IME ownership and trim trace history [skip ci] * [comment audit] Shorten pane recovery and parked watcher explanations [skip ci] * [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci] * [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci] * [comment audit] Trim renderer lifecycle and menu rationale [skip ci] * [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci] * [comment audit] Clarify renderer hook evidence and source ownership [skip ci] * [comment audit] Tighten IPC ownership and reconnect explanations [skip ci] * [comment audit] Trim renderer focus, paste and attention comments [skip ci] * [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci] * [comment audit] Trim agent launch, resume and ownership commentary [skip ci] * [comment audit] Shorten account, browser and editor comments [skip ci] * [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci] * [comment audit] Condense launch recovery and editor tokenization comments [skip ci] * [comment audit] Condense workspace selection and terminal pane rationale [skip ci] * [comment audit] Condense terminal fitting and rendering recovery comments [skip ci] * [comment audit] Condense terminal input and host routing explanations [skip ci] * [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci] * [comment audit] Condense structured launch and terminal link explanations [skip ci] * [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci] * [comment audit] Condense session persistence and workspace activation rationale [skip ci] * [comment audit] Condense workspace routing and browser ownership rationale [skip ci] * [comment audit] Condense mirror recovery and layout authority rationale [skip ci] * [comment audit] Condense runtime transport and file safety rationale [skip ci] * [comment audit] Condense runtime graph and structured session rationale [skip ci] * [comment audit] Condense browser creation and remote tab recovery rationale [skip ci] * [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci] * [comment audit] Condense frame, startup and provider cache rationale [skip ci] * [comment audit] Condense host twin and agent lifecycle rationale [skip ci] * [comment audit] Condense browser authority and editor cleanup rationale [skip ci] * [comment audit] Condense editor buffer identity and review cache rationale [skip ci] * [comment audit] Condense provider lifetime and repository ownership rationale [skip ci] * [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci] * [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci] * [comment audit] Condense cleanup ownership and listing admission rationale [skip ci] * [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci] * [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci] * [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci] * [comment audit] Condense cloud rollout and assignment safety rationale [skip ci] * [comment audit] Condense relay session and database recovery rationale [skip ci] * [comment audit] Condense relay placement and protocol compatibility rationale [skip ci] * [comment audit] Condense build and packaging constraints [skip ci] * [comment audit] Condense CI gates and validation isolation rationale [skip ci] * [comment audit] Condense catalog identity and browser validation rationale [skip ci] * [comment audit] Condense mobile browser and frame-budget rationale [skip ci] * [comment audit] Shorten mobile route and browser test explanations [skip ci] * [comment audit] Condense editor and session seam test rationale [skip ci] * [comment audit] Trim session and terminal closure history [skip ci] * [comment audit] Shorten terminal browser and census explanations [skip ci] * [comment audit] Preserve root-only E2E diagnostic baseline * [comment audit] Condense mobile seam and HTML preview test rationale [skip ci] * [comment audit] Condense preview instrumentation and render harness rationale [skip ci] * [comment audit] Condense bundle contracts and native-build constraints [skip ci] * [comment audit] Shorten native packaging and CI routing rationale [skip ci] * [comment audit] Condense release tooling and benchmark constraints [skip ci] * [comment audit] Condense packaging and platform gate rationale [skip ci] * [comment audit] Shorten mobile route and history rationale [skip ci] * [comment audit] Condense mobile browser and component rationale [skip ci] * [comment audit] Shorten drawer and workspace-form rationale [skip ci] * [comment audit] Trim mobile review and editor-document explanations [skip ci] * [comment audit] Condense editor lifecycle and Markdown explanations [skip ci] * [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci] * [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci] * [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci] * [comment audit] Condense bridge stream, native request and audio rationale [skip ci] * [comment audit] Reduce bridge schema and page-client commentary [skip ci] * [comment audit] Tighten mobile page bridge contracts [skip ci] * [comment audit] Clarify mobile cache and session constraints [skip ci] * [comment audit] Condense mobile route and lifecycle rationale [skip ci] * [comment audit] Tighten mobile shell hook timing rationale [skip ci] * [comment audit] Clarify route handoff and notification compatibility [skip ci] * [comment audit] Clarify push registration and dictation cleanup constraints [skip ci] * [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci] * [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci] * [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci] * [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci] * [comment audit] Tighten stream replay and queued session ownership rationale [skip ci] * [comment audit] Condense mobile composer and reply-schema constraints [skip ci] * [comment audit] Clarify mobile transcript recovery and session ownership [skip ci] * [comment audit] Clarify mobile queue and source-control compatibility [skip ci] * [comment audit] Condense storage and task launch constraints [skip ci] * [comment audit] Clarify task schemas and terminal document ownership [skip ci] * [comment audit] Tighten terminal input and viewport rationale [skip ci] * [comment audit] Shorten terminal and RPC recorder contracts [skip ci] * [comment audit] Condense recorder engine and connection contracts [skip ci] * [comment audit] Shorten host credential and relay recovery comments [skip ci] * [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci] * [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci] * [comment audit] Condense catalog and relay entry contracts [skip ci] * [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci] * [comment audit] Condense WSL relay and E2E regression rationale [skip ci] * [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci] * [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci] * [comment audit] Shorten layout oracle and regression explanations [skip ci] * [comment audit] Condense paired recovery and ownership regression contracts [skip ci] * [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci] * [comment audit] Condense terminal rendering and IME regression rationale [skip ci] * [comment audit] Shorten terminal recovery and benchmark contracts [skip ci] * [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci] * [comment audit] Condense workflow routing and release rationale [skip ci] * [comment audit] Condense mobile security and installer contracts [skip ci] * [comment audit] Shorten native rationale and stylesheet workarounds [skip ci] * [comment audit] Shorten Windows visibility probe rationale [skip ci] * [comment audit] Condense embedded release and signing rationale [skip ci] * [comment audit] Retain current main test behavior in final reconciliation [skip ci] * [comment audit] Complete final comment review across current source inventory * [comment audit] Handle consolidated audit reports before final CI * [comment audit] Preserve credential storage census detail [skip ci] * [comment audit] Scale broad changed E2E selections with verified timing shards * [comment audit] Align E2E assertions with browser close and settled session metadata * [comment audit] Match existing E2Es to current resource and windowing contracts * [comment audit] Observe the shared server outage notice in reconnect coverage * [comment audit] Make existing renderer probes measure settled tab and PTY work * [comment audit] Align palette E2E selector with task search * [comment audit] Refresh remaining palette search selectors * [comment audit] Keep selector correction within selected audit specs * [comment audit] Await emoji fixture settings inside renderer evaluation * [comment audit] Cancel fixture fit retries between terminal tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -1,58 +1,12 @@
|
||||
name: Adhoc macOS + Windows Dev Build
|
||||
|
||||
# Why: lets anyone cut installable macOS and Windows builds of a trusted repo ref
|
||||
# so the team can run an experimental feature instead of reasoning from a diff.
|
||||
# Hourly covers current main; this covers experiments and on-demand validation.
|
||||
#
|
||||
# Deliberately narrow scope:
|
||||
# - macOS and Windows desktop installers. Linux keeps using RC/stable.
|
||||
# - No tests, no lint, no e2e. PR CI and release-cut remain the gates. The one
|
||||
# exception is the orcad template: like release-cut, it is merged from the
|
||||
# node-server lanes that build and qualify each SSH target's slot.
|
||||
# - macOS is signed and notarized so TCC grants survive updates.
|
||||
# - Windows is unsigned; the published release notes explain the one-time
|
||||
# SmartScreen/manual-install requirement.
|
||||
#
|
||||
# Artifacts publish to stablyai/orca-adhoc — separate from both orca and
|
||||
# orca-hourly. Separate from orca because the main repo's releases atom feed
|
||||
# exposes only its 10 newest entries. Separate from orca-hourly because a build
|
||||
# from someone's branch must never be picked up by a developer who only meant to
|
||||
# ride main; the two are different levels of "unvetted".
|
||||
#
|
||||
# From the Actions tab: pick this workflow, "Run workflow", leave "Use workflow
|
||||
# from" on main, and type your branch in the first field. Or from the CLI:
|
||||
#
|
||||
# gh workflow run adhoc-mac-build.yml --ref main -f ref=my-branch -f label=wasm-terminal
|
||||
#
|
||||
# Leaving the ref field empty builds whatever "Use workflow from" is set to, which
|
||||
# is what someone who only touched that picker means. Naming the branch explicitly
|
||||
# is still better: the workflow file is always read from the dispatch ref, so a
|
||||
# branch carrying a stale copy of this file would otherwise run that copy.
|
||||
#
|
||||
# GITHUB_TOKEN is scoped to this repo and cannot publish there, so writes use the
|
||||
# same GitHub App as hourly, additionally installed on orca-adhoc with
|
||||
# Contents: Read and write. The secret names below are historical — one App, one
|
||||
# private key, both dev-channel repos — and rotating it stays a single operation.
|
||||
# Provision with `bash config/scripts/setup-hourly-release-token.sh`.
|
||||
#
|
||||
# The requested ref is vetted before checkout: it must be a branch or tag of this
|
||||
# repo, or a commit reachable from one. PR refs are refused outright — this
|
||||
# workflow runs the checked-out code next to MAC_CERTS and the notary password,
|
||||
# so "just build that community PR" must not become a way to hand fork code the
|
||||
# release identity. A branch here always belongs to someone with write access,
|
||||
# which is the same trust the dispatch button itself already requires.
|
||||
# Build trusted repo refs on demand: signed/notarized macOS and unsigned Windows, with qualified orcad slots; PR/release pipelines own other gates. Publish to orca-adhoc so branch experiments cannot enter main’s hourly channel or crowd stable/RC release feeds. Dispatch main’s workflow with -f ref=my-branch to avoid stale YAML. Writes use the hourly GitHub App installed on orca-adhoc (setup-hourly-release-token.sh); GITHUB_TOKEN cannot publish cross-repo. Ref vetting rejects PR refs and unreachable SHAs before source runs beside signing secrets.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
# Why optional: the Actions UI already shows its own "Use workflow from"
|
||||
# branch picker directly above this field, and picking a branch there is
|
||||
# what most people will read as "build this". Defaulting to that branch
|
||||
# makes the obvious action correct. Fill this in only to build a ref other
|
||||
# than the one the workflow file itself is read from — normally leave the
|
||||
# picker on main and name your branch here, so a stale copy of this
|
||||
# workflow on an old branch is not what runs.
|
||||
# Default empty ref to the dispatch picker; prefer main’s workflow plus explicit branch input to build old branches with current YAML.
|
||||
description: 'Branch, tag, or SHA to build — must live in stablyai/orca; PR refs are refused (default: the branch selected above)'
|
||||
required: false
|
||||
default: ''
|
||||
@@ -67,27 +21,18 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
# Why keyed on the ref rather than global: two people cutting builds from two
|
||||
# different branches at the same time is the ordinary case here, and serialising
|
||||
# them would make each wait out the other's notary queue. Re-dispatching the
|
||||
# *same* branch still queues, so a push mid-build cannot race itself.
|
||||
# Serialize per ref: queue repeat builds while independent branches avoid each other’s notary wait.
|
||||
group: adhoc-mac-build-${{ inputs.ref || github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
ADHOC_REPO: stablyai/orca-adhoc
|
||||
# Why age and not a count like hourly: this channel is low-volume and bursty, so
|
||||
# a count would either hold one week's experiments forever or evict a build
|
||||
# someone is still running after a busy afternoon. A month is well past the "few
|
||||
# days" these exist for, and by then the branch has landed or been abandoned.
|
||||
# Retain by month rather than count so bursts cannot evict recent experiments or sparse periods keep them forever.
|
||||
ADHOC_RETAIN_DAYS: 30
|
||||
|
||||
jobs:
|
||||
# Why: every job below checks out its own copy, and a branch name read per job builds whatever
|
||||
# the branch points at when that job starts. A push mid-run then mixes commits, e.g. slot lanes
|
||||
# from one commit merged by a template step from the next. Resolving once pins the whole run.
|
||||
# Validate reachability before any job executes the requested source.
|
||||
# Resolve once before source execution so concurrent pushes cannot mix slot/template/desktop commits.
|
||||
resolve-ref:
|
||||
if: github.repository == 'stablyai/orca'
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
@@ -111,17 +56,12 @@ jobs:
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
# Bare: a work-tree repo refuses to fetch over its own checked-out
|
||||
# branch. tree:0 keeps the fetch to the commit graph — no trees, no
|
||||
# blobs — so this stays cheap next to the build it fronts. reftable
|
||||
# because this repo has branches that differ only in casing, and the
|
||||
# files backend cannot store both on a case-insensitive runner disk —
|
||||
# it fails the entire fetch, not just the one ref.
|
||||
# Bare avoids fetching over a checked-out branch; tree:0 fetches only the commit graph.
|
||||
# Reftable preserves case-only branch names that make the files backend fail on case-insensitive disks.
|
||||
scratch="$RUNNER_TEMP/vet-requested-ref"
|
||||
git init -q --bare --ref-format=reftable "$scratch"
|
||||
git -C "$scratch" fetch -q --filter=tree:0 "$REPO_URL" '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'
|
||||
# Branch first to keep actions/checkout's old tie-break: bare
|
||||
# rev-parse would prefer the tag when a branch shares its name.
|
||||
# Resolve branches before tags, matching actions/checkout’s tie-break.
|
||||
sha=""
|
||||
case "$REQUESTED_REF" in
|
||||
refs/heads/*|refs/tags/*) candidates=("$REQUESTED_REF") ;;
|
||||
@@ -143,10 +83,7 @@ jobs:
|
||||
echo "::error::'$REQUESTED_REF' does not resolve to a branch, tag, or commit of stablyai/orca."
|
||||
exit 1
|
||||
fi
|
||||
# The object resolving locally is not proof a branch or tag reaches
|
||||
# it: a partial clone can lazily fetch a bare SHA on demand, and
|
||||
# GitHub serves PR-only commits by SHA. Reachability is the actual
|
||||
# trust test.
|
||||
# Lazy-fetchable SHAs can be PR-only; require reachability from a trusted branch/tag, not local object presence.
|
||||
if [[ -z "$(git -C "$scratch" for-each-ref --count=1 --format='%(refname)' --contains "$sha" refs/heads refs/tags)" ]]; then
|
||||
echo "::error::Commit $REQUESTED_REF is not reachable from any branch or tag of stablyai/orca; refusing to build it with release credentials."
|
||||
exit 1
|
||||
@@ -212,20 +149,15 @@ jobs:
|
||||
MAIN_REPO_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Why this check: the version scripts are read from the branch being built,
|
||||
# not from main, so a branch cut before the adhoc channel landed has no
|
||||
# copy of them. Say that plainly instead of failing with a module-not-found.
|
||||
# Old branch refs may predate adhoc version scripts; report that explicitly.
|
||||
for script in adhoc-build-version dev-channel-base-version; do
|
||||
if [[ ! -f "config/scripts/$script.mjs" ]]; then
|
||||
echo "::error::$REF has no config/scripts/$script.mjs; rebase it onto a main that has the adhoc channel."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# Why git tags, not GitHub releases: unpublishing a buggy cut deletes the
|
||||
# GitHub release and leaves the tag, which still owns that number.
|
||||
# Releases-only let adhoc sit on a number already taken, so the updater
|
||||
# would not install it. Empty on failure — the script then falls back
|
||||
# to package.json.
|
||||
# Unpublishing deletes releases but leaves version-owning tags; use tags to prevent updater regression.
|
||||
# On lookup failure the script falls back to package.json.
|
||||
published="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api \
|
||||
"repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" \
|
||||
--jq '.[].ref | sub("^refs/tags/"; "")' || true)"
|
||||
@@ -238,10 +170,7 @@ jobs:
|
||||
fi
|
||||
cat "$RUNNER_TEMP/adhoc-identity.txt" >>"$GITHUB_OUTPUT"
|
||||
|
||||
# Design D2, as release-cut does: every desktop build ships the orcad template (server JS plus
|
||||
# every target's addons), merged from the node-server lanes that qualify each slot at this ref.
|
||||
# Without it an adhoc build cannot deploy managed orcad to an SSH host. No secrets, and the mac
|
||||
# job vets the ref before anything is signed.
|
||||
# Ship managed-SSH orcad template from qualified per-target lanes at the vetted ref, as release-cut does.
|
||||
orcad-template:
|
||||
needs: [resolve-ref, orcad-template-support]
|
||||
if: needs.orcad-template-support.outputs.ships == 'true'
|
||||
@@ -293,22 +222,13 @@ jobs:
|
||||
NAME: ${{ needs.orcad-template-support.outputs.name }}
|
||||
SHA: ${{ needs.resolve-ref.outputs.sha }}
|
||||
REF: ${{ inputs.ref || github.ref_name }}
|
||||
# Via env, not inline `${{ }}`: both land inside a shell string, and an
|
||||
# expression expanded there is substituted before bash parses the line
|
||||
# (zizmor: template-injection).
|
||||
# Pass expressions via env so bash parses data rather than substituted script text (zizmor: template-injection).
|
||||
ACTOR: ${{ github.actor }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
short_sha="${SHA:0:12}"
|
||||
# Why create it up front: electron-builder then uploads into a known tag
|
||||
# rather than inferring one from package.json.
|
||||
#
|
||||
# Why --draft: everything between here and the manifest check is a window
|
||||
# where the release exists but has no installable assets. A draft is
|
||||
# absent from the releases list and from listReleaseBuilds, so a job that
|
||||
# dies in that window — including a hard kill by the job timeout, which
|
||||
# runs no cleanup step at all — leaves something invisible rather than a
|
||||
# tag the picker offers and the download 404s on.
|
||||
# Precreate a known tag as draft so builder cannot infer it from package.json.
|
||||
# Keep incomplete assets invisible until manifest verification, even if timeout kills the job without cleanup.
|
||||
gh release create "$TAG" \
|
||||
--repo "$ADHOC_REPO" \
|
||||
--title "$NAME" \
|
||||
@@ -337,13 +257,7 @@ jobs:
|
||||
needs.orcad-template-support.result == 'success' &&
|
||||
needs.release-javascript.result == 'success' &&
|
||||
needs.prepare-adhoc-mac-release.result == 'success'
|
||||
# Why an environment: it gives the signing/notary/App secrets somewhere to
|
||||
# live that a stale copy of this workflow on an old branch cannot reach.
|
||||
# Referencing it is a no-op until repo settings give it teeth; the intended
|
||||
# follow-up is to move MAC_CERTS, MAC_CERTS_PASSWORD, APPLE_ID,
|
||||
# APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID, HOURLY_RELEASE_APP_ID, and
|
||||
# HOURLY_RELEASE_APP_PRIVATE_KEY into it, then pin its deployment branch
|
||||
# policy to main so only main's copy of this file can read them.
|
||||
# Signing environment must restrict deployments to main after MAC_CERTS/password, APPLE_ID/app password/team and hourly App secrets move there; referencing it alone enforces nothing.
|
||||
environment: adhoc-mac-build
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -353,21 +267,13 @@ jobs:
|
||||
matrix:
|
||||
arch: [x64, arm64]
|
||||
runs-on: blacksmith-12vcpu-macos-15
|
||||
# Why 150: it must exceed the worst case the retry budgets below can produce
|
||||
# (install 3x10 + publish 2x45 = 120, plus ~25 for checkout/build/verify), or
|
||||
# the job is killed mid-retry and no cleanup step runs at all.
|
||||
# Allow 150min for install 3×10 + publish 2×45 + ~25 setup/build/verify; outer timeout would prevent cleanup.
|
||||
timeout-minutes: 150
|
||||
env:
|
||||
JOBS: max
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
steps:
|
||||
# Why vet before checkout: everything after this step runs the checked-out
|
||||
# code with release signing credentials in reach. Branches and tags of this
|
||||
# repo are the intended audience; refs/pull/* would smuggle in fork code,
|
||||
# and a raw SHA is only accepted when some branch or tag of this repo can
|
||||
# actually reach it. Resolving to a pinned SHA here also means the commit
|
||||
# that was vetted is the commit that gets checked out — a push to the
|
||||
# branch between the two steps cannot swap it.
|
||||
# Vet branch/tag reachability and reject refs/pull before checkout can access signing credentials; checkout the resolved SHA to prevent ref-swap races.
|
||||
- name: Vet the requested ref
|
||||
id: vetted
|
||||
shell: bash
|
||||
@@ -380,17 +286,11 @@ jobs:
|
||||
- name: Checkout the requested ref
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Why an input at all rather than just github.ref: the whole point is to
|
||||
# build code that has not landed, and the workflow definition itself
|
||||
# always comes from the dispatch ref — naming the branch here instead
|
||||
# applies main's current copy of this file to an arbitrary branch.
|
||||
# Explicit source input lets main’s workflow build an unmerged branch rather than run that branch’s stale workflow.
|
||||
ref: ${{ steps.vetted.outputs.sha }}
|
||||
# Version helpers only read HEAD; published versions come from the release API.
|
||||
fetch-depth: 1
|
||||
# This job only reads stablyai/orca and never pushes; every write goes
|
||||
# to the adhoc repo through a minted App token passed by env. Not
|
||||
# persisting the checkout credential shrinks the blast radius if a build
|
||||
# step is compromised (zizmor: artipacked).
|
||||
# Read this repo without persisted checkout credentials; all writes use an env-passed App token for orca-adhoc (zizmor: artipacked).
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
@@ -408,8 +308,7 @@ jobs:
|
||||
pnpm-lock.yaml
|
||||
mobile/pnpm-lock.yaml
|
||||
|
||||
# Why: packaging bundles the pinned Node into the macOS terminal host. Keyed on the pin alone
|
||||
# with no restore-keys, so a pin bump starts empty instead of carrying old archives forward.
|
||||
# Cache bundled host Node by exact pin, without restore keys, so bumps cannot restore old archives.
|
||||
- name: Locate the pinned Node runtime cache
|
||||
shell: bash
|
||||
run: echo "ORCA_NODE_RUNTIME_CACHE_DIR=$HOME/Library/Caches/orca-node-runtime" >>"$GITHUB_ENV"
|
||||
@@ -430,8 +329,7 @@ jobs:
|
||||
restore-keys: |
|
||||
electron-builder-mac-
|
||||
|
||||
# Why both CPUs: the mac config packages x64 and arm64 from this arm64
|
||||
# runner, so the install must carry both variants of the native optional deps.
|
||||
# Install both x64/arm64 optional natives because this arm64 runner packages both.
|
||||
- name: Install dependencies
|
||||
uses: nick-fields/retry@v4
|
||||
with:
|
||||
@@ -440,13 +338,11 @@ jobs:
|
||||
retry_wait_seconds: 30
|
||||
command: pnpm install --frozen-lockfile --cpu=current,x64,arm64
|
||||
|
||||
# Why here: electron-builder's beforePack requires out/mobile-web, and the bundle
|
||||
# build resolves React Native and Expo from mobile/node_modules.
|
||||
# Build required out/mobile-web after installing its React Native/Expo dependencies.
|
||||
- uses: ./.github/actions/install-mobile-dependencies
|
||||
if: needs.release-javascript.outputs.supported != 'true'
|
||||
|
||||
# Why: signing is what makes an adhoc build installable over an existing
|
||||
# Orca, so a missing cert must fail here rather than after a 20-minute build.
|
||||
# Require signing cert before expensive build; unsigned macOS cannot update an existing install correctly.
|
||||
- name: Verify macOS signing environment
|
||||
run: node config/scripts/verify-macos-release-env.mjs
|
||||
env:
|
||||
@@ -475,9 +371,7 @@ jobs:
|
||||
ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA: ${{ needs.release-javascript.outputs.supported == 'true' && needs.release-javascript.outputs.source_sha || '' }}
|
||||
ORCA_RELEASE_JAVASCRIPT_BUILD_IDENTITY: dev
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
# Why: adhoc builds are not an official channel — telemetry's transport
|
||||
# gate accepts only 'stable' or 'rc', so leaving this unset keeps them
|
||||
# silent, which is correct for unvetted branch artifacts.
|
||||
# Leave build identity unset: telemetry accepts only official stable/rc, so branch artifacts stay silent.
|
||||
ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token
|
||||
|
||||
- name: Wait for the qualified orcad template
|
||||
@@ -512,10 +406,7 @@ jobs:
|
||||
name: orcad-template
|
||||
path: out/orcad-template
|
||||
|
||||
# Why the token is minted here and not at the top: installation tokens live
|
||||
# one hour, everything before this point writes nothing, and the notary round
|
||||
# trip inside the publish step can be tens of minutes. Minting after the build
|
||||
# starts the clock at the first call that actually uses it.
|
||||
# Mint the one-hour write token after build, just before potentially long notarization/publish.
|
||||
- name: Checkout workflow packaging scripts
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -528,9 +419,7 @@ jobs:
|
||||
- name: Package and notarize this Mac architecture
|
||||
uses: nick-fields/retry@v4
|
||||
with:
|
||||
# Why 45: an attempt is pack + notarize, and the notary queue is
|
||||
# the unbounded part. Two attempts, because a failed adhoc build has a
|
||||
# person waiting on it who can simply dispatch again.
|
||||
# Allow two 45min pack/notarize attempts; an adhoc user can redispatch rather than pay for a third.
|
||||
timeout_minutes: 45
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 30
|
||||
@@ -542,9 +431,7 @@ jobs:
|
||||
ORCA_REQUIRE_ORCAD_TEMPLATE: ${{ needs.orcad-template-support.outputs.ships == 'true' && '1' || '' }}
|
||||
CSC_LINK: ${{ secrets.MAC_CERTS }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
|
||||
# Why all three: electron-builder's notarize step authenticates to the
|
||||
# Apple notary service with the app-specific password, not with the
|
||||
# signing cert. Omitting them fails the build rather than skipping it.
|
||||
# Require Apple id/app-specific password/team: notarization authenticates separately from signing cert.
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
@@ -677,9 +564,7 @@ jobs:
|
||||
}
|
||||
throw new Error('Timed out waiting for the qualified orcad template')
|
||||
|
||||
# Why this is the last mutating step: publishing the draft is what makes the
|
||||
# build visible to listReleaseBuilds. Doing it only after the manifest check
|
||||
# means the picker can never offer a release whose assets are incomplete.
|
||||
# Publish only after manifest validation so the picker never offers incomplete assets.
|
||||
- name: Publish the verified release
|
||||
id: publish_live
|
||||
shell: bash
|
||||
@@ -689,18 +574,11 @@ jobs:
|
||||
NAME: ${{ needs.orcad-template-support.outputs.name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# --title again: electron-builder resolves this draft by tag and may
|
||||
# rewrite its title on upload. Re-asserting here means the name the
|
||||
# picker reads is the one composed above, whatever it did in between.
|
||||
# Reassert the picker title after electron-builder may rewrite the tag’s draft on upload.
|
||||
gh release edit "$TAG" --repo "$ADHOC_REPO" --draft=false --prerelease --title "$NAME"
|
||||
echo "Published $TAG as \"$NAME\""
|
||||
|
||||
# Why: a draft left behind by a failed publish is invisible to users but still
|
||||
# holds its tag name. Gated on publish_live not having succeeded so a later
|
||||
# failure (the prune step) cannot delete a release that already went live and
|
||||
# that people may already be installing. Why cancelled() too: a run stopped
|
||||
# from the Actions UI is not a failure(), so without it a manual cancel
|
||||
# mid-publish would strand the draft.
|
||||
# Delete stranded drafts on failure/cancel, but never after publish_live: later prune failure must not remove an installable release.
|
||||
- name: Discard the draft release on failure
|
||||
if: >-
|
||||
(failure() || cancelled()) && needs.prepare-adhoc-mac-release.outputs.tag != '' &&
|
||||
@@ -717,32 +595,21 @@ jobs:
|
||||
echo "::warning::Could not discard draft $TAG; remove it manually."
|
||||
|
||||
- name: Prune expired adhoc releases
|
||||
# Only after a live publish: $TAG is then a non-draft this step must not
|
||||
# delete, and a run that failed before publishing has nothing to retire.
|
||||
# Retire only after live publication; preserve this non-draft tag and leave failed runs to draft cleanup.
|
||||
if: steps.publish_live.outcome == 'success'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||
# Protect the tag this run just shipped, so no filter mistake can delete
|
||||
# a build minutes after the person who cut it was told it exists.
|
||||
# Exclude the just-published tag from pruning even if ordering/filter logic regresses.
|
||||
TAG: ${{ needs.prepare-adhoc-mac-release.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Why compute the cutoff in bash rather than with jq's `now`: this runs
|
||||
# once per dispatch, and a fixed epoch makes the threshold visible in the
|
||||
# log when someone asks where their build went.
|
||||
# Fix/log the cutoff epoch once per dispatch so retention decisions remain auditable.
|
||||
cutoff=$(( $(date -u +%s) - ADHOC_RETAIN_DAYS * 86400 ))
|
||||
echo "Pruning adhoc releases published before $(date -u -d "@$cutoff" '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
# --cleanup-tag so pruning does not leave orphan tags with no release or
|
||||
# assets attached. Drafts are excluded: a stale draft is the failure
|
||||
# path's business, not the retention window's.
|
||||
#
|
||||
# Age comes from publishedAt, never createdAt. GitHub reports createdAt
|
||||
# as the date of the *commit* a release's tag points at, and every tag
|
||||
# here is cut from this repo's one seed commit — so all of them carry
|
||||
# that same createdAt, and the day the window rolled past it the entire
|
||||
# channel expired at once and a single run deleted it. A release with no
|
||||
# publishedAt is kept rather than aged by guesswork.
|
||||
# --cleanup-tag removes release-less tags; failure cleanup owns drafts, which retention excludes.
|
||||
# Use publishedAt: GitHub’s createdAt is the seed commit’s date for every tag. Aging by it once
|
||||
# expired the entire channel in one run; retain releases with unknown publishedAt.
|
||||
jq_filter='map(select(.isDraft | not))'
|
||||
if [[ -n "${TAG:-}" ]]; then
|
||||
jq_filter+=" | map(select(.tagName != \"${TAG//\"/\\\"}\"))"
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
name: Publish agent state rules
|
||||
|
||||
# The only publisher of agent state rules releases. Merging never publishes: a maintainer runs
|
||||
# this from main, first to `next` (RC and dev builds), then promotes the identical file to
|
||||
# `stable` after a soak. Every job runs on the dispatched commit, so the publish job runs exactly
|
||||
# the script and rules the gate tested.
|
||||
#
|
||||
# Why GITHUB_TOKEN: release-policy.yml deletes an agent-state-rules-* release any other author
|
||||
# publishes. Its releases start no workflow, so release-policy.yml sees them only when someone
|
||||
# edits one by hand, and then accepts them as bot-authored prereleases.
|
||||
# Maintainer-only publisher: dispatch main to next, soak, then promote identical bytes to stable. All jobs use the dispatched commit. Use GITHUB_TOKEN so release-policy accepts bot-authored prereleases; token-created releases do not trigger workflows until manual edits.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
|
||||
@@ -80,8 +80,7 @@ jobs:
|
||||
|
||||
- wait: history-scan
|
||||
|
||||
# Compiles the workspace. No Postgres service: nothing here reaches a
|
||||
# database, and the service container costs ~13s of startup.
|
||||
# Compile without Postgres: no database access occurs here, avoiding ~13s service startup.
|
||||
build:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.draft != true
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
@@ -167,9 +166,7 @@ jobs:
|
||||
path: cloud/security-reports/
|
||||
retention-days: 30
|
||||
|
||||
# Runs in parallel with build. `pnpm test` compiles the workspace
|
||||
# packages it needs through pretest hooks, so it does not depend on
|
||||
# `pnpm build` having run.
|
||||
# Run tests beside build; pretest hooks compile their own workspace dependencies.
|
||||
test:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.draft != true
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
|
||||
@@ -25,9 +25,7 @@ on:
|
||||
- 'config/scripts/daemon-endpoint-handover-smoke.mjs'
|
||||
- 'config/scripts/windows-daemon-workspace-close-repro.mjs'
|
||||
- 'config/scripts/verify-computer-native.mjs'
|
||||
# Why: the native-smoke job boots the built terminal daemon under plain
|
||||
# Node, so any change to the daemon bundle graph or the main build must
|
||||
# re-run it (the v1.4.129-rc.1 daemon outage shipped with green CI).
|
||||
# Re-smoke daemon bundle/build changes under plain Node; v1.4.129-rc.1 shipped a module-load outage despite green CI.
|
||||
- 'electron.vite.config.ts'
|
||||
- 'config/build-plugins/**'
|
||||
- 'src/main/daemon/**'
|
||||
@@ -90,10 +88,7 @@ jobs:
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
native-runtime: node
|
||||
# Why: without --config, bare vitest ignores config/vitest.config.ts (there
|
||||
# is no root config) and falls back to the 5s default timeout with no
|
||||
# Windows worker cap, so the real csc.exe launcher-compile tests time out
|
||||
# on hosted Windows. Use the shared config so this job matches pnpm test.
|
||||
# Use shared Vitest config: bare invocation misses config/ and its timeout/Windows worker cap, breaking real csc.exe tests.
|
||||
- run: >-
|
||||
pnpm vitest run --config config/vitest.config.ts
|
||||
config/scripts/macos-computer-helper-owner-loss-group-recovery.test.mjs
|
||||
@@ -130,19 +125,13 @@ jobs:
|
||||
- run: pnpm verify:computer-native
|
||||
- run: pnpm build:cli
|
||||
- run: pnpm run build:electron-vite:parallel
|
||||
# Why: boot the BUILT daemon-entry under plain Node the way production
|
||||
# forks it. v1.4.129-rc.1 shipped a daemon that exited at module load
|
||||
# (leaked electron require) while every other check passed; this fails
|
||||
# the PR when the built daemon cannot start on ubuntu-22.04 / windows.
|
||||
# Boot built daemon-entry under plain Node on Ubuntu/Windows, catching leaked electron requires like v1.4.129-rc.1.
|
||||
- name: Daemon boot smoke
|
||||
run: node config/scripts/daemon-boot-smoke.mjs
|
||||
# Why: a daemon that lost its endpoint name used to delete the replacement's
|
||||
# socket on exit, stranding a live daemon that hosts PTYs nothing can reach.
|
||||
# Only two real processes racing the same endpoint reproduce it.
|
||||
# Race real daemon processes on one endpoint; a loser must not unlink the replacement’s socket.
|
||||
- name: Daemon endpoint handover smoke
|
||||
run: node config/scripts/daemon-endpoint-handover-smoke.mjs
|
||||
# Why: workspace removal overlaps graceful renderer teardown with the
|
||||
# forced main-process sweep; exercise that exact pair against real ConPTY.
|
||||
# Exercise workspace removal’s renderer teardown plus forced main sweep against real ConPTY.
|
||||
- name: Windows daemon workspace-close repro
|
||||
if: runner.os == 'Windows'
|
||||
run: node config/scripts/windows-daemon-workspace-close-repro.mjs
|
||||
|
||||
@@ -1,49 +1,6 @@
|
||||
name: Daily macOS Dev Build
|
||||
|
||||
# Why: gives developers a signed macOS build of main once a day that the in-app
|
||||
# updater can install directly, without waiting for an RC cut — less noise than
|
||||
# hourly.
|
||||
#
|
||||
# Schedule is a single UTC cron (GH Actions has no timezone-aware schedules).
|
||||
# 18:15 UTC is late morning Pacific year-round (10:15am PST / 11:15am PDT). Minute
|
||||
# 15 avoids stacking with hourly-mac-build, which fires at minute 0 every hour.
|
||||
#
|
||||
# Deliberately narrow scope:
|
||||
# - macOS only. Other platforms keep using RC/stable.
|
||||
# - No tests or lint on the build job. After a live publish we fire-and-forget
|
||||
# the full E2E workflow at the cut SHA (same detached dispatch as
|
||||
# release-cut). A red suite must not fail or delay the daily.
|
||||
# - PR CI and release-cut remain the gates that matter.
|
||||
# - Signed AND notarized, exactly like a release. The notary round trip is the
|
||||
# one slow step kept: macOS anchors a notarized app's TCC grants on identifier
|
||||
# + team rather than on its cdhash, so those grants survive an update. Without
|
||||
# a ticket every daily reads as a new client and silently loses file access
|
||||
# under Documents/Desktop/Downloads.
|
||||
#
|
||||
# Artifacts publish to stablyai/orca-daily, never to stablyai/orca: the main
|
||||
# repo's releases atom feed exposes only its 10 newest entries, so high-volume
|
||||
# dev tags would evict every stable/RC entry and break updates for real users.
|
||||
# Separate from orca-hourly so someone riding main's hourlies never sees the
|
||||
# sparse daily series mixed into that list.
|
||||
#
|
||||
# GITHUB_TOKEN is scoped to this repo and cannot publish there, so writes use the
|
||||
# same GitHub App as hourly (installed on orca-daily with Contents: Read and
|
||||
# write). Provision the App secrets with
|
||||
# `bash config/scripts/setup-hourly-release-token.sh`, then grant the App access
|
||||
# to orca-daily with `bash config/scripts/setup-daily-release-repo.sh`:
|
||||
# HOURLY_RELEASE_APP_ID the App's numeric id (shared with hourly/adhoc)
|
||||
# HOURLY_RELEASE_APP_PRIVATE_KEY the App's .pem private key
|
||||
#
|
||||
# Installation tokens live one hour, which is why this mints three times. Install
|
||||
# and build need no token at all, and notarization can hold the publish step for
|
||||
# tens of minutes; minting again once the build is done starts the clock at the
|
||||
# first call that actually uses it rather than burning a third of it on
|
||||
# `pnpm install`. The upload step's own retry budget (2x45m) can outlive that
|
||||
# second token, so a third is minted after it for verify/publish/prune/cleanup —
|
||||
# without it a stuck notary run would strand a draft that cleanup gets a 401 on.
|
||||
# The release stays an unpublished draft until the manifest check passes, so the
|
||||
# worst case is still an invisible draft — and the build-number query counts
|
||||
# drafts, so it holds its number and the next run does not reuse it.
|
||||
# Publish daily main builds to orca-daily, separate from hourly and stable/RC feeds. Sign/notarize for update-preserved macOS TCC grants; PR/release pipelines own blocking validation, with E2E detached after publish. Cron 18:15 UTC means 10:15 PST/11:15 PDT, offset from hourly. Use the shared hourly App with write access to orca-daily (setup-hourly-release-token.sh/setup-daily-release-repo.sh). Mint tokens again after build and upload because 2×45min retry can exceed their 1h life. Keep releases draft until manifests verify; drafts retain build numbers.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -112,10 +69,7 @@ jobs:
|
||||
head_sha: ${{ steps.freshness.outputs.head_sha }}
|
||||
published: ${{ steps.publish_live.outcome == 'success' && 'true' || 'false' }}
|
||||
runs-on: blacksmith-6vcpu-macos-15
|
||||
# Why 150: it must exceed the worst case the retry budgets below can produce
|
||||
# (install 3x10 + publish 2x45 = 120, plus ~25 for checkout/build/verify), or
|
||||
# the job is killed mid-retry and no cleanup step runs at all. A typical run
|
||||
# is far shorter — this is the notary queue's tail, not its median.
|
||||
# Allow 150min for install 3×10 + publish 2×45 + ~25 setup/build/verify, so outer timeout cannot strand cleanup mid-retry.
|
||||
timeout-minutes: 150
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
@@ -126,10 +80,7 @@ jobs:
|
||||
ref: ${{ needs.resolve-main.outputs.head_sha }}
|
||||
# Version helpers only read HEAD; published versions come from git tags.
|
||||
fetch-depth: 1
|
||||
# Why: this job only reads stablyai/orca and never pushes; every write
|
||||
# goes to the daily repo through a minted App token passed by env.
|
||||
# Not persisting the checkout credential shrinks the blast radius if a
|
||||
# build step is compromised (zizmor: artipacked).
|
||||
# Read this repo without persisted credentials; write orca-daily through env-passed App token (zizmor: artipacked).
|
||||
persist-credentials: false
|
||||
|
||||
- name: Mint daily repo token
|
||||
@@ -141,8 +92,7 @@ jobs:
|
||||
owner: stablyai
|
||||
repositories: orca-daily
|
||||
|
||||
# Why: main is often idle. Rebuilding an unchanged commit burns a runner
|
||||
# hour and adds a redundant tag to the retention window.
|
||||
# Skip unchanged main to avoid duplicate builds/tags and runner cost.
|
||||
- name: Check whether main moved since the last daily
|
||||
id: freshness
|
||||
shell: bash
|
||||
@@ -158,9 +108,7 @@ jobs:
|
||||
echo "Forced dispatch; building $head_sha."
|
||||
exit 0
|
||||
fi
|
||||
# The previous daily records its source commit in the release body.
|
||||
# Drafts are excluded: an unpublished leftover never shipped, so treating
|
||||
# it as "the last build" would skip a build that never actually happened.
|
||||
# Drafts never shipped; exclude them when comparing the previous daily’s source commit.
|
||||
last_body="$(gh release list --repo "$DAILY_REPO" --limit 20 --json tagName,isDraft \
|
||||
--jq 'map(select(.isDraft | not)) | .[0].tagName // empty' 2>/dev/null || true)"
|
||||
if [[ -z "$last_body" ]]; then
|
||||
@@ -195,8 +143,7 @@ jobs:
|
||||
pnpm-lock.yaml
|
||||
mobile/pnpm-lock.yaml
|
||||
|
||||
# Why: packaging bundles the pinned Node into the macOS terminal host. Keyed on the pin alone
|
||||
# with no restore-keys, so a pin bump starts empty instead of carrying old archives forward.
|
||||
# Cache bundled host Node by exact pin without restore keys.
|
||||
- name: Locate the pinned Node runtime cache
|
||||
if: steps.freshness.outputs.should_build == 'true'
|
||||
shell: bash
|
||||
@@ -220,8 +167,7 @@ jobs:
|
||||
restore-keys: |
|
||||
electron-builder-mac-
|
||||
|
||||
# Why both CPUs: the mac config packages x64 and arm64 from this arm64
|
||||
# runner, so the install must carry both variants of the native optional deps.
|
||||
# Install x64 and arm64 optional natives for both packaged macOS targets.
|
||||
- name: Install dependencies
|
||||
if: steps.freshness.outputs.should_build == 'true'
|
||||
uses: nick-fields/retry@v4
|
||||
@@ -231,8 +177,7 @@ jobs:
|
||||
retry_wait_seconds: 30
|
||||
command: pnpm install --frozen-lockfile --cpu=current,x64,arm64
|
||||
|
||||
# Why here: electron-builder's beforePack requires out/mobile-web, and the bundle
|
||||
# build resolves React Native and Expo from mobile/node_modules.
|
||||
# Build required out/mobile-web using mobile’s React Native/Expo install.
|
||||
- uses: ./.github/actions/install-mobile-dependencies
|
||||
if: steps.freshness.outputs.should_build == 'true'
|
||||
|
||||
@@ -257,21 +202,12 @@ jobs:
|
||||
MAIN_REPO_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Existing titles, which carry the build number this series continues
|
||||
# from. The script picks the number, because it restarts per base version
|
||||
# and only the script knows which base this build resolved to.
|
||||
#
|
||||
# Why drafts count here but not in the freshness check: that check asks
|
||||
# "did this commit ship", where a draft is a no. This one asks "is the
|
||||
# number free", where a stranded draft still holds one.
|
||||
# The script continues numbering for its resolved base version; stranded drafts still reserve numbers,
|
||||
# although freshness checks exclude them because they never shipped.
|
||||
names="$(gh release list --repo "$DAILY_REPO" --limit 200 --json name \
|
||||
--jq '.[].name // empty')"
|
||||
# Why git tags, not GitHub releases: unpublishing a buggy cut deletes the
|
||||
# GitHub release and leaves the tag. That dragged hourlies backwards so
|
||||
# electron-updater stopped offering them; dailies would do the same. A
|
||||
# separate token because GH_TOKEN above is the App's, scoped to the
|
||||
# daily repo. Empty on failure — the script then falls back to
|
||||
# package.json, which is stale but never wrong enough to fail a build.
|
||||
# Tags survive release deletion, preventing updater version regression. Use a separate token because
|
||||
# GH_TOKEN’s App grant is scoped to the channel repo; lookup failure falls back to package.json.
|
||||
main_tags="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api \
|
||||
"repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" \
|
||||
--jq '.[].ref | sub("^refs/tags/"; "")' || true)"
|
||||
@@ -285,11 +221,7 @@ jobs:
|
||||
node config/scripts/daily-build-version.mjs \
|
||||
>"$RUNNER_TEMP/daily-identity.txt"
|
||||
grep -E '^(version|build_number)=' "$RUNNER_TEMP/daily-identity.txt"
|
||||
# Why check rather than trust: the checkout above pins the resolved main commit, but a
|
||||
# workflow_dispatch runs this file from whatever branch was dispatched. A
|
||||
# branch that edits this step while main still has the old script yields
|
||||
# an empty name and an untitled release — silent, and only visible once
|
||||
# someone opens the releases page. Fail here instead.
|
||||
# Dispatch YAML can be newer than the pinned source’s version script; reject missing output before publishing an untitled release.
|
||||
if ! grep -q '^name=' "$RUNNER_TEMP/daily-identity.txt"; then
|
||||
echo "::error::daily-build-version.mjs emitted no release name; this workflow and main's copy of the script are out of sync."
|
||||
exit 1
|
||||
@@ -301,9 +233,7 @@ jobs:
|
||||
run: pnpm build:release
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
# Why: daily builds are not an official channel — telemetry's transport
|
||||
# gate accepts only 'stable' or 'rc', so leaving this unset keeps them
|
||||
# silent, which is correct for unvetted dev artifacts.
|
||||
# Leave dev identity unset so official-only stable/rc telemetry stays silent.
|
||||
ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token
|
||||
|
||||
# After the app build so nothing that cleans out/ can drop it; electron-builder ships it.
|
||||
@@ -314,11 +244,7 @@ jobs:
|
||||
name: orcad-template
|
||||
path: out/orcad-template
|
||||
|
||||
# Why a second mint: everything from here on writes to the daily repo, and
|
||||
# the notary round trip inside the publish step can be tens of minutes. The
|
||||
# token minted at the top has already spent install + build of its one hour
|
||||
# on steps that never touched it; restarting the clock here gives the slow
|
||||
# part the full budget.
|
||||
# Refresh the one-hour token after read-only install/build, before long publish/notary work.
|
||||
- name: Re-mint daily repo token for publish
|
||||
id: app_token_publish
|
||||
if: steps.freshness.outputs.should_build == 'true'
|
||||
@@ -340,12 +266,9 @@ jobs:
|
||||
SHA: ${{ steps.freshness.outputs.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Kept at 12 even though the title shows 7: the freshness check above
|
||||
# parses this back out of the body to decide whether main has moved.
|
||||
# Freshness parses the 12-character source SHA from the body even though titles show seven.
|
||||
short_sha="${SHA:0:12}"
|
||||
# Why a file rather than an inline string: the publish step re-asserts
|
||||
# this same body, and the freshness check only works if the two agree
|
||||
# exactly. One source, written once, read twice.
|
||||
# Write one body file for creation and reassertion; freshness parsing requires exact agreement.
|
||||
notes_file="$RUNNER_TEMP/daily-release-notes.md"
|
||||
cat >"$notes_file" <<EOF
|
||||
Automated daily macOS and Windows dev build from commit \`$short_sha\`.
|
||||
@@ -362,16 +285,8 @@ jobs:
|
||||
(SmartScreen will warn about an unknown publisher). Every later switch,
|
||||
including back to Stable, works in-app from there.
|
||||
EOF
|
||||
# Why create it up front: electron-builder then uploads into a known tag
|
||||
# rather than inferring one from package.json.
|
||||
#
|
||||
# Why --draft: everything between here and the manifest check is a window
|
||||
# where the release exists but has no installable assets. A draft is
|
||||
# absent from the releases list and from listReleaseBuilds, so a job that
|
||||
# dies in that window — including a hard kill by the job timeout, which
|
||||
# runs no cleanup step at all — leaves something invisible rather than a
|
||||
# tag the picker offers and the download 404s on. It is flipped live only
|
||||
# after the manifest is verified.
|
||||
# Precreate a known tag as draft so builder cannot infer it from package.json.
|
||||
# Keep incomplete assets invisible until manifest verification, even if timeout kills the job without cleanup.
|
||||
gh release create "$TAG" \
|
||||
--repo "$DAILY_REPO" \
|
||||
--title "$NAME" \
|
||||
@@ -384,17 +299,13 @@ jobs:
|
||||
if: steps.freshness.outputs.should_build == 'true'
|
||||
uses: nick-fields/retry@v4
|
||||
with:
|
||||
# Why 45 like the release pipeline: an attempt is pack + notarize +
|
||||
# upload, and the notary queue is the unbounded part. Why 2 attempts and
|
||||
# not 3: a missed daily costs a day, and a third attempt buys less than
|
||||
# it costs in runner time once the notary is that stuck.
|
||||
# Allow two 45min pack/notarize/upload attempts; a third costs more runner time than a missed daily.
|
||||
timeout_minutes: 45
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 30
|
||||
command: node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON config/scripts/pinned-node-downloads.mjs darwin-arm64 darwin-x64 && node config/scripts/ensure-native-runtime.mjs --runtime=electron && if [ -f config/scripts/build-mac-notarization-overlap.mjs ]; then ORCA_MAC_DAILY=1 node config/scripts/build-mac-notarization-overlap.mjs --publish always; else ORCA_MAC_DAILY=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --mac --publish always; fi
|
||||
env:
|
||||
# Why: electron-builder's github publisher targets the repo named in the
|
||||
# config; the token must therefore carry write access to orca-daily.
|
||||
# Give the publisher App write access to its configured orca-daily target.
|
||||
GH_TOKEN: ${{ steps.app_token_publish.outputs.token }}
|
||||
ORCA_DAILY_BUILD_VERSION: ${{ steps.daily.outputs.version }}
|
||||
ORCA_BUILD_COMMIT: ${{ steps.daily.outputs.commit }}
|
||||
@@ -402,21 +313,12 @@ jobs:
|
||||
ORCA_REQUIRE_ORCAD_TEMPLATE: '1'
|
||||
CSC_LINK: ${{ secrets.MAC_CERTS }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
|
||||
# Why all three: electron-builder's notarize step authenticates to the
|
||||
# Apple notary service with the app-specific password, not with the
|
||||
# signing cert. Omitting them fails the build rather than skipping it,
|
||||
# since `notarize` is now on for this path.
|
||||
# Require Apple id/app-specific password/team for notary authentication, separate from signing cert.
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
# Why a third mint: the upload step above allows two 45-minute attempts, so
|
||||
# it can outlive the one-hour token minted before it. Every remaining step
|
||||
# writes to the daily repo, including the failure path that discards the
|
||||
# draft — a 401 there is exactly the stranded draft nobody can clean up.
|
||||
#
|
||||
# Why always(): a failed or cancelled upload is the case that needs this
|
||||
# most, since the cleanup step below runs only on that path.
|
||||
# Always refresh after upload: 2×45min can outlive the earlier token, including failed/cancelled uploads needing draft cleanup.
|
||||
- name: Re-mint daily repo token for verify and cleanup
|
||||
id: app_token_final
|
||||
if: always() && steps.freshness.outputs.should_build == 'true'
|
||||
@@ -427,9 +329,7 @@ jobs:
|
||||
owner: stablyai
|
||||
repositories: orca-daily
|
||||
|
||||
# Why: the updater resolves a tag, then fetches latest-mac.yml from it. A
|
||||
# release missing that manifest is a tag the picker offers and the download
|
||||
# 404s on, so fail loudly instead of leaving a broken entry.
|
||||
# Verify latest-mac.yml before exposing its release to prevent picker downloads returning 404.
|
||||
- name: Verify update manifest published
|
||||
if: steps.freshness.outputs.should_build == 'true'
|
||||
shell: bash
|
||||
@@ -441,10 +341,7 @@ jobs:
|
||||
assets="$(gh release view "$TAG" --repo "$DAILY_REPO" --json assets --jq '.assets[].name')"
|
||||
echo "Published assets:"
|
||||
echo "$assets"
|
||||
# Why exit 1 without deleting here: the release is still a draft, so it is
|
||||
# already invisible to users, and the failure handler below owns cleanup.
|
||||
# Deleting inline under `set -e` would also let the delete's exit code
|
||||
# preempt this explicit failure.
|
||||
# Drafts are already invisible; let failure cleanup delete them so set -e cannot mask this explicit failure.
|
||||
for required in latest-mac.yml; do
|
||||
if ! grep -qx "$required" <<<"$assets"; then
|
||||
echo "::error::Daily draft $TAG is missing $required; the updater could not install it."
|
||||
@@ -456,9 +353,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Why this is the last mutating step: publishing the draft is what makes the
|
||||
# build visible to listReleaseBuilds. Doing it only after the manifest check
|
||||
# means the picker can never offer a release whose assets are incomplete.
|
||||
# Publish only after manifest validation so the picker cannot offer incomplete assets.
|
||||
- name: Publish the verified release
|
||||
id: publish_live
|
||||
if: steps.freshness.outputs.should_build == 'true'
|
||||
@@ -470,33 +365,19 @@ jobs:
|
||||
NOTES_FILE: ${{ steps.release.outputs.notes_file }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# --title again: electron-builder resolves this draft by tag and may
|
||||
# rewrite its title on upload. Re-asserting here means the name the
|
||||
# picker reads is the one composed above, whatever it did in between.
|
||||
# --notes-file for the same reason, and it matters more: the next run's
|
||||
# freshness check parses the source commit back out of this body, so a
|
||||
# body the publisher overwrote would rebuild an unchanged main daily.
|
||||
# Builder may rewrite title/body on upload; reassert both so the picker name and next run’s source-SHA freshness check stay correct.
|
||||
gh release edit "$TAG" --repo "$DAILY_REPO" --draft=false --prerelease \
|
||||
--title "$NAME" --notes-file "$NOTES_FILE"
|
||||
echo "Published $TAG as \"$NAME\""
|
||||
|
||||
# Why: a draft left behind by a failed publish is invisible to users but still
|
||||
# holds its tag name, so the next run for the same minute would collide.
|
||||
#
|
||||
# Why it is gated on publish_live not having succeeded: a later failure (the
|
||||
# prune step) must not delete a release that already went live and that users
|
||||
# may already be installing. A job killed by the outer timeout runs no steps
|
||||
# at all — which is exactly why the release stays a draft until verified.
|
||||
# Why cancelled() too: a run stopped from the Actions UI is not a failure(),
|
||||
# so without it a manual cancel mid-publish would strand the draft.
|
||||
# Remove failed/cancelled stranded drafts, never releases already published by publish_live, including after prune failure. Outer timeout skips cleanup, so retain draft status until verified.
|
||||
- name: Discard the draft release on failure
|
||||
if: >-
|
||||
(failure() || cancelled()) && steps.release.outputs.tag != '' &&
|
||||
steps.publish_live.outcome != 'success'
|
||||
shell: bash
|
||||
env:
|
||||
# Fall back to the publish token: if the re-mint itself is what failed,
|
||||
# the older token is the only one left and may still have time on it.
|
||||
# If token refresh fails, let cleanup try the older token while it may still be valid.
|
||||
GH_TOKEN: ${{ steps.app_token_final.outputs.token || steps.app_token_publish.outputs.token }}
|
||||
TAG: ${{ steps.release.outputs.tag }}
|
||||
run: |
|
||||
@@ -507,33 +388,18 @@ jobs:
|
||||
echo "::warning::Could not discard draft $TAG; remove it manually."
|
||||
|
||||
- name: Prune old daily releases
|
||||
# Only after a live publish: $TAG is then a non-draft we must not delete,
|
||||
# and failed runs should not reshuffle retention around a draft that the
|
||||
# failure path is about to discard.
|
||||
# Prune only after publication; failed runs leave draft disposal to cleanup.
|
||||
if: steps.publish_live.outcome == 'success'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app_token_final.outputs.token }}
|
||||
# Protect the tag this run just shipped; at the retain cap, a bad sort
|
||||
# can otherwise mark the newest release as stale and delete it.
|
||||
# Never prune this run’s just-published tag at the retention cap.
|
||||
TAG: ${{ steps.release.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Why: --cleanup-tag so pruning does not leave orphan tags behind that
|
||||
# keep showing up in tag lists with no release or assets attached.
|
||||
# Drafts are excluded so retention counts shipped builds only; a stale
|
||||
# draft is handled by the failure path, not by the retention window.
|
||||
#
|
||||
# Sort by publishedAt (not createdAt). Many non-draft dailies can share
|
||||
# one createdAt (bulk import / re-create), so createdAt ranking is
|
||||
# unstable. publishedAt is real recency; tagName (...YYYYMMDDHHMM) is
|
||||
# the deterministic tie-break.
|
||||
#
|
||||
# Why force $TAG to the front before slicing: a hard retain-window seat
|
||||
# for this run's release. Dropping $TAG from the list *before* the slice
|
||||
# would permanently keep retain+1 releases; skipping it only in the
|
||||
# delete loop would under-prune when the sort is still wrong. Partition
|
||||
# keeps relative order of every other tag.
|
||||
# Delete pruned tags too; exclude drafts owned by failure cleanup. Rank by publishedAt/tagName:
|
||||
# createdAt ties (bulk import/recreation) can evict the just-published release. Partition $TAG to
|
||||
# the front before slicing so it owns exactly one retained seat without under-pruning or keeping retain+1.
|
||||
jq_filter='map(select(.isDraft | not)) | sort_by(.publishedAt // "", .tagName) | reverse'
|
||||
if [[ -n "${TAG:-}" ]]; then
|
||||
jq_filter+=" | (map(select(.tagName == \"${TAG//\"/\\\"}\")) + map(select(.tagName != \"${TAG//\"/\\\"}\")))"
|
||||
@@ -557,13 +423,7 @@ jobs:
|
||||
echo "::warning::Could not prune $tag"
|
||||
done <<<"$stale"
|
||||
|
||||
# Why detached dispatch, not a reusable workflow in this graph: the full suite
|
||||
# is currently red on main. Inlining it here would fail the daily after the
|
||||
# signed build already published. Same pattern as release-cut's post-release-e2e.
|
||||
#
|
||||
# Why --ref main plus the SHA input: daily tags live on orca-daily, not this
|
||||
# repo, so there is no cut tag for `gh workflow run --ref`. The workflow file
|
||||
# comes from main; checkout uses the exact commit this daily built.
|
||||
# Dispatch E2E separately so main’s red full suite cannot fail an already-published daily. Use main’s workflow plus exact built SHA because daily tags live in another repo.
|
||||
post-daily-e2e:
|
||||
needs: build-daily-mac
|
||||
if: ${{ needs.build-daily-mac.outputs.published == 'true' && needs.build-daily-mac.outputs.head_sha != '' }}
|
||||
@@ -588,21 +448,10 @@ jobs:
|
||||
done
|
||||
echo "::warning::Failed to dispatch post-daily E2E for $SHA after 3 attempts."
|
||||
|
||||
# Why this runs after the mac leg rather than beside it: the tag and version
|
||||
# are computed inside that job, so until it has run nothing else can name the
|
||||
# release to upload into. The cost is small enough not to matter — the Windows
|
||||
# leg measures ~7.5 min (install 2m45, build 35s, NSIS package 3m) against a
|
||||
# ~9.5 min mac run, which keeps a daily run far inside its interval.
|
||||
#
|
||||
# Why `./` rather than a pinned `@main`: `uses:` resolves against the ref this
|
||||
# file itself came from, which for an ordinary dispatch is main. Someone who
|
||||
# deliberately points the Actions "Use workflow from" picker at a branch
|
||||
# already gets that branch's copy of this entire file, so this follows the same
|
||||
# rule instead of inventing a second one.
|
||||
# Wait for mac release identity before Windows uploads; local ./ workflow follows the dispatch ref consistently.
|
||||
build-daily-win:
|
||||
needs: build-daily-mac
|
||||
# Only once the mac release is actually live: there is no release to upload
|
||||
# into otherwise, and the Windows workflow refuses to create one.
|
||||
# Start Windows publication only after the mac release is live; its workflow refuses to create a missing release.
|
||||
if: needs.build-daily-mac.outputs.published == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -1,41 +1,9 @@
|
||||
name: Dev Channel Windows Build
|
||||
|
||||
# Why its own file rather than steps inlined into hourly/daily/adhoc: one copy of
|
||||
# the Windows leg instead of three, and it stays dispatchable on its own so a
|
||||
# Windows artifact can be rebuilt for an existing tag without paying for the mac
|
||||
# leg's packaging and notarization again.
|
||||
#
|
||||
# Adhoc can stage Windows alongside macOS, then publish only after macOS succeeds.
|
||||
# Hourly, daily, and manual dispatches keep packaging and publishing together.
|
||||
#
|
||||
# Why unsigned: Windows release installers are signed by SignPath *after*
|
||||
# packaging, and release-cut budgets 1h + 4h for those approval waits. That does
|
||||
# not fit an hourly cadence and it does not fit "dispatch an adhoc build and go
|
||||
# get coffee". So dev-channel Windows builds ship unsigned, which has one real
|
||||
# consequence, handled in `src/shared/release-channel.ts`:
|
||||
#
|
||||
# electron-updater Authenticode-verifies every installer it downloads against
|
||||
# the publisherName baked into the *installed* app's app-update.yml. Stable and
|
||||
# RC carry 'SignPath Foundation', so they reject an unsigned dev installer and
|
||||
# no future build can fix the copies already installed. Dev builds omit the
|
||||
# name (config/electron-builder.config.cjs), so verification is skipped there.
|
||||
#
|
||||
# Net effect: the way *into* a dev channel on Windows is a one-time manual
|
||||
# installer run. Every way out — to another dev build, or back to Stable — works
|
||||
# through the in-app updater. The picker offers a download for exactly that jump.
|
||||
#
|
||||
# Mac workflows call this directly or stage artifacts for a later publish job.
|
||||
# It is also dispatchable to rebuild Windows for an existing tag:
|
||||
#
|
||||
# gh workflow run dev-channel-win-build.yml --ref main \
|
||||
# -f channel=adhoc -f tag=v1.4.178-adhoc.20260819010203 \
|
||||
# -f ref=<sha> -f version=1.4.178-adhoc.20260819010203
|
||||
# Share one independently dispatchable Windows dev build across hourly/daily/adhoc. Adhoc may stage beside Mac and publish after its success. Dev installers are unsigned because SignPath approval waits exceed this cadence. Installed stable/RC publisherName rejects them, requiring one manual entry install; dev builds omit publisherName so later in-app channel changes work. Manual rebuild: dispatch main with channel/tag/ref/version for an existing release.
|
||||
|
||||
on:
|
||||
# Why the inputs are duplicated: workflow_call does not accept `type: choice`,
|
||||
# and workflow_dispatch wants it so the Actions UI offers a menu instead of a
|
||||
# free-text box. The channel allowlist below is what actually enforces the set,
|
||||
# since a workflow_call caller can pass any string.
|
||||
# Duplicate inputs because workflow_call lacks choice UI; validate its free-string channels with the allowlist.
|
||||
workflow_call:
|
||||
inputs:
|
||||
channel:
|
||||
@@ -104,18 +72,14 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
# Keyed on the tag: re-dispatching the same tag must not race two uploads into
|
||||
# one release, but two different channels (or two adhoc branches) are the
|
||||
# ordinary case and must not wait on each other.
|
||||
# Serialize per tag to prevent competing uploads while independent channels/branches proceed.
|
||||
group: dev-channel-win-build-${{ inputs.tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build-win:
|
||||
if: github.repository == 'stablyai/orca'
|
||||
# Why the same environment as the mac workflows: this needs the App token
|
||||
# that can write to the dev-channel repos, and it should be reachable from
|
||||
# exactly the same place those secrets already live.
|
||||
# Use mac workflows’ same environment for cross-repo App secrets.
|
||||
environment: adhoc-mac-build
|
||||
# The fast image has VS Build Tools 2022; signed release provenance stays separate.
|
||||
runs-on: ${{ inputs.fast_release && 'blacksmith-8vcpu-windows-2025' || 'windows-2022' }}
|
||||
@@ -132,10 +96,7 @@ jobs:
|
||||
VERSION: ${{ inputs.version }}
|
||||
|
||||
steps:
|
||||
# Why vet before checkout: everything after this runs the checked-out code
|
||||
# with a token that can write to a release repo. The mac leg already vetted
|
||||
# the ref it resolved, but this workflow is dispatchable on its own, so it
|
||||
# re-derives the same guarantee rather than trusting its caller.
|
||||
# Re-vet source before checkout because standalone dispatch also exposes a release-write token.
|
||||
- name: Vet the requested inputs
|
||||
id: vetted
|
||||
shell: bash
|
||||
@@ -144,8 +105,7 @@ jobs:
|
||||
REPO_URL: https://github.com/${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# workflow_call takes channel as a free-text string, so the set is
|
||||
# enforced here rather than by the input type.
|
||||
# workflow_call inputs are free text; validate the channel set here.
|
||||
case "$CHANNEL" in
|
||||
hourly|daily|adhoc) ;;
|
||||
*)
|
||||
@@ -157,24 +117,18 @@ jobs:
|
||||
echo "::error::ref must be a full 40-character commit SHA, got '$REQUESTED_SHA'. The dispatching workflow passes the commit it resolved."
|
||||
exit 1
|
||||
fi
|
||||
# The tag must name the version being packaged, or the artifacts would
|
||||
# land in a release describing a different build.
|
||||
# Bind the tag to the packaged version so assets cannot describe a different build.
|
||||
if [[ "$TAG" != "v$VERSION" ]]; then
|
||||
echo "::error::tag '$TAG' does not match version '$VERSION'."
|
||||
exit 1
|
||||
fi
|
||||
# And the version must carry the channel's own prerelease identifier,
|
||||
# so an hourly artifact can never be uploaded into an adhoc release.
|
||||
# Require the channel’s prerelease identifier so hourly assets cannot enter adhoc releases.
|
||||
if [[ "$VERSION" != *"-$CHANNEL."* ]]; then
|
||||
echo "::error::version '$VERSION' is not a $CHANNEL version."
|
||||
exit 1
|
||||
fi
|
||||
# Reachability is the trust test: GitHub serves PR-only commits by SHA,
|
||||
# so resolving the object is not proof a branch or tag of this repo
|
||||
# reaches it. Bare + tree:0 keeps this to the commit graph; reftable
|
||||
# because branches that differ only in casing cannot both be stored by
|
||||
# the files backend on a case-insensitive runner disk, which fails the
|
||||
# entire fetch rather than the one ref.
|
||||
# Trust requires branch/tag reachability; GitHub serves PR-only SHAs too. Bare/tree:0 limits fetch to
|
||||
# the commit graph; reftable avoids case-only refs failing the entire fetch on case-insensitive disks.
|
||||
scratch="$RUNNER_TEMP/vet-requested-ref"
|
||||
git init -q --bare --ref-format=reftable "$scratch"
|
||||
git -C "$scratch" fetch -q --filter=tree:0 "$REPO_URL" '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'
|
||||
@@ -200,9 +154,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
# This job only reads stablyai/orca and never pushes; every write goes
|
||||
# to the dev-channel repo through a minted App token passed by env
|
||||
# (zizmor: artipacked).
|
||||
# Read this repo without persisted credentials; writes use the dev-repo env App token (zizmor: artipacked).
|
||||
persist-credentials: false
|
||||
|
||||
# Why: a relay-era ref (before #27215) needs the Windows relay addon this build no longer compiles.
|
||||
@@ -214,11 +166,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Why a guard and not just a build: the workflow file comes from the
|
||||
# dispatch ref, but the packaging config comes from the *built* commit. A
|
||||
# branch cut before Windows dev builds landed has a config that ignores
|
||||
# ORCA_WIN_*, which would resolve publish.repo to the main repo. Say that
|
||||
# in one sentence here rather than failing deep inside electron-builder.
|
||||
# Reject old packaging configs that ignore ORCA_WIN_* and would publish to the main repo.
|
||||
- name: Resolve the dev-channel packaging identity
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -247,8 +195,7 @@ jobs:
|
||||
pnpm-lock.yaml
|
||||
mobile/pnpm-lock.yaml
|
||||
|
||||
# Caches the Electron binary and electron-builder's tool downloads (nsis,
|
||||
# winCodeSign). Same key shape as release-cut's Windows leg.
|
||||
# Cache Electron/NSIS/winCodeSign tools with release-cut’s key shape.
|
||||
- name: Cache electron-builder downloads
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
@@ -259,11 +206,7 @@ jobs:
|
||||
restore-keys: |
|
||||
electron-builder-win-
|
||||
|
||||
# Why retried: pnpm install triggers electron's postinstall, which pulls the
|
||||
# Electron binary from GitHub release assets, and that CDN returns transient
|
||||
# 504s often enough to lose a build to it.
|
||||
# Why host-only: this job packages only for its own runner OS and
|
||||
# architecture, so the default host-scoped install is deliberate.
|
||||
# Retry transient Electron CDN 504s during install; keep host-scoped natives for this runner’s only target.
|
||||
- name: Install dependencies
|
||||
uses: nick-fields/retry@v4
|
||||
with:
|
||||
@@ -272,13 +215,11 @@ jobs:
|
||||
retry_wait_seconds: 30
|
||||
command: pnpm install --frozen-lockfile
|
||||
|
||||
# Why here: electron-builder's beforePack requires out/mobile-web, and the bundle
|
||||
# build resolves React Native and Expo from mobile/node_modules.
|
||||
# Build required out/mobile-web from mobile’s React Native/Expo install.
|
||||
- uses: ./.github/actions/install-mobile-dependencies
|
||||
if: inputs.javascript_source_sha == ''
|
||||
|
||||
# Why the packaging check runs before the 20-minute build: it only needs
|
||||
# node_modules, and a stale config should cost seconds rather than a build.
|
||||
# Validate packaging config after install, before the expensive build.
|
||||
- name: Verify dev-channel packaging identity
|
||||
shell: bash
|
||||
run: node config/scripts/verify-dev-channel-packaging.mjs --channel="$CHANNEL" --platform=win32
|
||||
@@ -302,9 +243,7 @@ jobs:
|
||||
env:
|
||||
ORCA_RELEASE_JAVASCRIPT_SOURCE_SHA: ${{ inputs.javascript_source_sha }}
|
||||
ORCA_RELEASE_JAVASCRIPT_BUILD_IDENTITY: dev
|
||||
# Why unset ORCA_BUILD_IDENTITY: telemetry's transport gate accepts only
|
||||
# 'stable' or 'rc', so leaving it unset keeps dev builds silent — which
|
||||
# is correct for unvetted artifacts. Same as the mac dev channels.
|
||||
# Unset dev build identity so official stable/rc telemetry stays silent.
|
||||
ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token
|
||||
|
||||
# Inline so refs predating the wait can still use the trusted dispatch workflow.
|
||||
@@ -340,8 +279,7 @@ jobs:
|
||||
name: orcad-template
|
||||
path: out/orcad-template
|
||||
|
||||
# Why the token is minted here and not at the top: installation tokens live
|
||||
# one hour and nothing before this point writes anything.
|
||||
# Mint the one-hour write token after read-only setup/build.
|
||||
- name: Mint dev channel repo token
|
||||
if: ${{ !inputs.stage_only }}
|
||||
id: app_token
|
||||
@@ -352,10 +290,7 @@ jobs:
|
||||
owner: stablyai
|
||||
repositories: orca-${{ inputs.channel }}
|
||||
|
||||
# Why: electron-builder's publisher creates a release when it cannot find
|
||||
# the tag ("publish: always"). If the mac leg failed and discarded its draft
|
||||
# while this was building, that would mint a fresh, untitled, Windows-only
|
||||
# release. Check first and fail instead.
|
||||
# Require existing release: publish:always would recreate a discarded mac draft as an untitled Windows-only release.
|
||||
- name: Confirm the target release still exists
|
||||
if: ${{ !inputs.stage_only }}
|
||||
shell: bash
|
||||
@@ -382,16 +317,10 @@ jobs:
|
||||
ORCA_BUILD_COMMIT: ${{ inputs.ref }}
|
||||
# beforePack and afterPack fail the package when the template is absent.
|
||||
ORCA_REQUIRE_ORCAD_TEMPLATE: ${{ inputs.orcad_template && '1' || '' }}
|
||||
# Why: electron-publish refuses to upload into a release published more
|
||||
# than two hours ago (gitHubPublisher.getOrCreateRelease). The mac leg
|
||||
# publishes the draft live as soon as *it* finishes, so a slow notary
|
||||
# queue plus a slow Windows build can cross that line and silently drop
|
||||
# every Windows asset. This is the documented escape hatch.
|
||||
# Allow upload beyond publisher’s 2h release-age guard; slow mac notarization/Windows build can otherwise silently drop every asset.
|
||||
EP_GH_IGNORE_TIME: 'true'
|
||||
|
||||
# Why: the updater resolves a tag, then fetches latest.yml from it. A
|
||||
# release carrying the installer but not the manifest is one the picker
|
||||
# offers and the update 404s on, so assert both.
|
||||
# Require installer and latest.yml, preventing picker-visible releases with missing updater manifests.
|
||||
- name: Verify Windows update manifest published
|
||||
if: ${{ !inputs.stage_only }}
|
||||
shell: bash
|
||||
|
||||
@@ -105,8 +105,7 @@ jobs:
|
||||
tag="$INPUT_TAG"
|
||||
[[ "$EVENT_NAME" == "release" ]] && tag="$RELEASE_TAG"
|
||||
|
||||
# Match the stable desktop format used by release-policy.yml. This
|
||||
# intentionally rejects mobile-* and all -rc.* tags.
|
||||
# Use release-policy’s stable desktop tag shape, excluding mobile-* and RC tags.
|
||||
stable_tag=false
|
||||
[[ "$tag" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] && stable_tag=true
|
||||
|
||||
@@ -147,9 +146,7 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
|
||||
# Resolve the tag to an immutable commit before handing it to the
|
||||
# deployment job. This prevents a force-moved tag from changing the
|
||||
# source between authorization and checkout.
|
||||
# Pin the immutable commit before deployment so force-moving a tag cannot swap authorized source.
|
||||
tag_ref_json=''
|
||||
for attempt in 1 2 3; do
|
||||
if tag_ref_json="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" 2>/dev/null)"; then
|
||||
@@ -173,9 +170,7 @@ jobs:
|
||||
fi
|
||||
echo "commit_sha=$commit_sha" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Stable tags cut before the docs package was merged are valid Orca
|
||||
# releases, but cannot produce a docs deployment. Skip them before
|
||||
# requesting the protected production environment.
|
||||
# Old stable tags may predate docs; skip before requesting the protected production environment.
|
||||
docs_source_available=false
|
||||
for attempt in 1 2 3; do
|
||||
if gh api "repos/$GITHUB_REPOSITORY/contents/docs/site/package.json?ref=$commit_sha" >/dev/null 2>&1; then
|
||||
|
||||
+49
-22
@@ -70,6 +70,8 @@ jobs:
|
||||
if: inputs.test_files == '' || github.event_name != 'pull_request' || inputs.needs_build
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
changed_e2e_shards: ${{ steps.changed-e2e-shards.outputs.shards }}
|
||||
env:
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
|
||||
@@ -79,6 +81,19 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Plan changed E2E allocation
|
||||
id: changed-e2e-shards
|
||||
env:
|
||||
TEST_FILES_JSON: ${{ inputs.test_files }}
|
||||
run: |
|
||||
node --input-type=module <<'NODE'
|
||||
import { appendFileSync, existsSync } from 'node:fs'
|
||||
const modulePath = './config/scripts/ci-e2e-job-selection.mjs'
|
||||
const selection = existsSync(modulePath) ? await import(modulePath) : {}
|
||||
const shards = selection.generalE2eShards?.(process.env.TEST_FILES_JSON) ?? ['1/1']
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `shards=${JSON.stringify(shards)}\n`)
|
||||
NODE
|
||||
|
||||
- name: Install native build tools
|
||||
env:
|
||||
ORCA_E2E_APT_PACKAGES: build-essential python3
|
||||
@@ -203,9 +218,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Fourteen scheduled runs averaged 24.6 minutes per shard; shards 4
|
||||
# and 9 repeatedly hit the 30-minute cap. A 12-way trial still left
|
||||
# one 30-minute shard, so 14 gives the suite enough failure headroom.
|
||||
# Use 14 shards: scheduled average 24.6min and repeated 30min caps remained even in a 12-way trial.
|
||||
- shard: '1/14'
|
||||
shard_name: 1-of-14
|
||||
- shard: '2/14'
|
||||
@@ -260,14 +273,7 @@ jobs:
|
||||
name: e2e-build-out${{ inputs.artifact_suffix }}
|
||||
path: out/
|
||||
|
||||
# Why: the Electron suite is wall-clock constrained on OSS runners, but
|
||||
# multiple Electron apps on one Xvfb VM contend on git/Chromium resources.
|
||||
# Sharding keeps each VM at one Playwright worker while splitting the
|
||||
# headless suite across separate runners.
|
||||
# SKIP_BUILD makes Playwright globalSetup reuse the single build job's
|
||||
# artifact instead of starting five concurrent electron-vite builds.
|
||||
# ORCA_E2E_FORWARD_APP_LOGS keeps startup failures visible when Electron
|
||||
# launches but never creates a BrowserWindow.
|
||||
# Shard one worker per VM to avoid Electron/git/Xvfb contention. Reuse one SKIP_BUILD artifact and forward app logs for pre-window failures.
|
||||
- name: Balance E2E shard from timing evidence
|
||||
env:
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
@@ -308,9 +314,7 @@ jobs:
|
||||
if: matrix.shard == '1/14'
|
||||
run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/worktree-switch-first-paint.spec.ts --config tests/playwright.config.ts --project=electron-headful --workers=1
|
||||
|
||||
# Why: Playwright retains traces/screenshots only on failure. Uploading
|
||||
# them as an artifact makes post-mortem debugging on CI possible without
|
||||
# re-running locally.
|
||||
# Upload failure traces/screenshots so CI failures are diagnosable without reproduction.
|
||||
- name: Upload Playwright traces
|
||||
if: failure() || cancelled()
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -321,11 +325,15 @@ jobs:
|
||||
if-no-files-found: ignore
|
||||
|
||||
changed-e2e:
|
||||
name: changed e2e specs
|
||||
name: changed e2e specs (${{ matrix.shard }})
|
||||
needs: [build]
|
||||
if: inputs.test_files != '' && (github.event_name != 'pull_request' || inputs.run_changed_e2e)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: ${{ fromJSON(needs.build.outputs.changed_e2e_shards || '["1/1"]') }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -334,8 +342,7 @@ jobs:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Install native build and headless UI tools
|
||||
# Why ripgrep: Quick Open's bounded host-side search requires rg instead of an
|
||||
# unbounded inventory fallback; the paired fixture exercises that real boundary.
|
||||
# Install rg for bounded Quick Open searches exercised by the paired fixture.
|
||||
env: *e2e_tool_packages
|
||||
run: *install_e2e_tools
|
||||
|
||||
@@ -353,6 +360,7 @@ jobs:
|
||||
env:
|
||||
TEST_FILES_JSON: ${{ inputs.test_files }}
|
||||
LAYOUT_OWNER: ${{ inputs.layout_owner }}
|
||||
SHARD: ${{ matrix.shard }}
|
||||
run: |
|
||||
if [ -f config/scripts/ci-e2e-job-selection.mjs ]; then
|
||||
printf '%s\n' "$TEST_FILES_JSON" | node config/scripts/ci-e2e-job-selection.mjs > "$RUNNER_TEMP/general-e2e-specs"
|
||||
@@ -385,8 +393,28 @@ jobs:
|
||||
if grep -l '@headful' "${TEST_FILES[@]}" >/dev/null; then
|
||||
E2E_PROJECT_ARGS+=(--project=electron-headful)
|
||||
fi
|
||||
E2E_SELECTION_ARGS=(--shard="$SHARD")
|
||||
if [ -f config/scripts/ci-e2e-shard-plan.mjs ]; then
|
||||
mkdir -p ci-shards-changed
|
||||
pnpm exec playwright test "${TEST_FILES[@]}" --config tests/playwright.config.ts --project=electron-headless "${E2E_PROJECT_ARGS[@]}" --list --reporter=json > ci-shards-changed/discovery.json
|
||||
export ORCA_SHARD_SOURCE_SHA="$(git rev-parse HEAD)"
|
||||
node config/scripts/ci-e2e-shard-plan.mjs ci-shards-changed/discovery.json "$SHARD" ci-shards-changed
|
||||
pnpm exec playwright test "${TEST_FILES[@]}" --config tests/playwright.config.ts --project=electron-headless "${E2E_PROJECT_ARGS[@]}" --test-list=ci-shards-changed/selected.txt --list --reporter=json > ci-shards-changed/selected-discovery.json
|
||||
node config/scripts/ci-e2e-shard-plan.mjs --verify ci-shards-changed/assignment.json ci-shards-changed/selected-discovery.json
|
||||
E2E_SELECTION_ARGS=(--test-list=ci-shards-changed/selected.txt)
|
||||
fi
|
||||
xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env "${E2E_ENV[@]}" \
|
||||
pnpm run test:e2e "${TEST_FILES[@]}" --workers=1 "${E2E_PROJECT_ARGS[@]}"
|
||||
pnpm run test:e2e "${TEST_FILES[@]}" "${E2E_SELECTION_ARGS[@]}" --workers=1 "${E2E_PROJECT_ARGS[@]}"
|
||||
|
||||
- name: Upload changed E2E shard assignment
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: changed-e2e-shard-${{ strategy.job-index }}${{ inputs.artifact_suffix }}-attempt-${{ github.run_attempt }}
|
||||
path: ci-shards-changed/
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Summarize layout save divergence
|
||||
if: always() && inputs.layout_owner == 'main'
|
||||
@@ -407,7 +435,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: layout-divergence-changed${{ inputs.artifact_suffix }}-attempt-${{ github.run_attempt }}
|
||||
name: layout-divergence-changed-${{ strategy.job-index }}${{ inputs.artifact_suffix }}-attempt-${{ github.run_attempt }}
|
||||
path: layout-divergence/
|
||||
retention-days: 14
|
||||
if-no-files-found: warn
|
||||
@@ -416,7 +444,7 @@ jobs:
|
||||
if: failure() || cancelled()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: playwright-traces-changed${{ inputs.artifact_suffix }}
|
||||
name: playwright-traces-changed-${{ strategy.job-index }}${{ inputs.artifact_suffix }}
|
||||
path: test-results/
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
@@ -475,8 +503,7 @@ jobs:
|
||||
slot="$(node config/scripts/build-orcad-prebuilds.mjs --print-slot)"
|
||||
pnpm build:orcad-prebuilds
|
||||
pnpm build:orcad-prebuilds --require-slots "$slot"
|
||||
# The headless oracle's orcad host must hold the same layout owner as the Electron host,
|
||||
# and so must the template `orca serve` materializes its server from.
|
||||
# The orcad host and its materialized template must use the Electron host’s layout owner.
|
||||
if [ -n "$LAYOUT_OWNER" ]; then
|
||||
export ORCA_LAYOUT_OWNER="$LAYOUT_OWNER"
|
||||
pnpm build:orcad
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
name: Homebrew Cask Bump
|
||||
|
||||
# Why: keep the stablyai/homebrew-orca tap in sync with GitHub Releases.
|
||||
# Triggers:
|
||||
# - workflow_call → release-cut.yml finished publishing a stable tag
|
||||
# - workflow_dispatch → manual backfill for a specific tag
|
||||
#
|
||||
# The job computes the sha256 of orca-macos-arm64.dmg + orca-macos-x64.dmg
|
||||
# from the release, templates the channel-specific cask in this repo with the
|
||||
# new version and hashes, then PRs the result into stablyai/homebrew-orca.
|
||||
# Stable tags update Casks/orca.rb; RC tags update Casks/orca@rc.rb.
|
||||
#
|
||||
# Why no `release.published` trigger: other products shipped from this repo
|
||||
# (e.g. mobile under `mobile-v*`) also publish GitHub Releases. A blanket
|
||||
# `release.published` trigger would fire this workflow for any of them and
|
||||
# attempt to download `orca-macos-*.dmg` from a non-desktop release, at
|
||||
# best 404ing and at worst (if the filter were weaker) rewriting the cask
|
||||
# to a non-desktop version. The tap is only ever bumped by the desktop
|
||||
# release pipeline calling this workflow directly.
|
||||
# Update desktop stable/RC casks in stablyai/homebrew-orca with release DMG hashes through release-cut workflow_call or manual backfill. Avoid release.published because mobile/non-desktop releases lack those DMGs and must never rewrite casks.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -132,10 +116,7 @@ jobs:
|
||||
print(src)
|
||||
PY
|
||||
|
||||
# Why: reuse the existing buf0-bot GitHub App instead of minting a new
|
||||
# PAT. The app is installed org-wide, so it has access to
|
||||
# stablyai/homebrew-orca automatically. GITHUB_TOKEN cannot push
|
||||
# cross-repo; a short-lived installation token can.
|
||||
# Use org-installed buf0-bot’s short-lived App token for cross-repo pushes; GITHUB_TOKEN is repo-scoped.
|
||||
- name: Generate buf0-bot token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
@@ -166,8 +147,7 @@ jobs:
|
||||
cp "$CASK_PATH" "tap/$CASK_PATH"
|
||||
|
||||
cd tap
|
||||
# Why: commit author must match the buf0-bot app so commits are
|
||||
# attributed to the bot in the tap's history / PR UI.
|
||||
# Match the buf0-bot App’s commit author for tap history/PR attribution.
|
||||
git config user.name "buf0-bot[bot]"
|
||||
git config user.email "252831055+buf0-bot[bot]@users.noreply.github.com"
|
||||
|
||||
@@ -182,8 +162,7 @@ jobs:
|
||||
git commit -m "$TITLE"
|
||||
git push -u origin "$branch" --force
|
||||
|
||||
# Why: repeated backfills can race an existing PR branch, so edit
|
||||
# the PR title if creation reports that one already exists.
|
||||
# Backfills can race an existing branch; update its PR title when creation finds one.
|
||||
gh pr create \
|
||||
--title "$TITLE" \
|
||||
--body "Automated bump of $CASK_PATH from stablyai/orca release $TAG." \
|
||||
@@ -191,8 +170,5 @@ jobs:
|
||||
--head "$branch" \
|
||||
|| gh pr edit "$branch" --title "$TITLE"
|
||||
|
||||
# Why: squash-merge immediately rather than --auto. The tap has no
|
||||
# required checks or branch protection, and `gh pr merge --auto`
|
||||
# only activates when there's something to wait on — on a repo
|
||||
# with no gates it silently no-ops, leaving the PR open forever.
|
||||
# Squash immediately: this tap has no gates, so gh pr merge --auto silently no-ops.
|
||||
gh pr merge "$branch" --squash --delete-branch
|
||||
|
||||
@@ -1,39 +1,6 @@
|
||||
name: Hourly macOS Dev Build
|
||||
|
||||
# Why: gives developers a signed macOS build of main every hour that the in-app
|
||||
# updater can install directly, without waiting for an RC cut.
|
||||
#
|
||||
# Deliberately narrow scope:
|
||||
# - macOS only. Other platforms keep using RC/stable.
|
||||
# - No tests, no lint, no e2e. This channel trades safety for latency; PR CI
|
||||
# and release-cut remain the gates that matter.
|
||||
# - Signed AND notarized, exactly like a release. The notary round trip is the
|
||||
# one slow step kept: macOS anchors a notarized app's TCC grants on identifier
|
||||
# + team rather than on its cdhash, so those grants survive an update. Without
|
||||
# a ticket every hourly reads as a new client and silently loses file access
|
||||
# under Documents/Desktop/Downloads — 24 times a day.
|
||||
#
|
||||
# Artifacts publish to stablyai/orca-hourly, never to stablyai/orca: the main
|
||||
# repo's releases atom feed exposes only its 10 newest entries, so 24 hourly
|
||||
# tags a day would evict every stable/RC entry and break updates for real users.
|
||||
#
|
||||
# GITHUB_TOKEN is scoped to this repo and cannot publish there, so writes use a
|
||||
# GitHub App installed on orca-hourly with Contents: Read and write. Its private
|
||||
# key does not expire, unlike a PAT — nothing here needs yearly rotation, and the
|
||||
# credential belongs to the org rather than to whoever created it.
|
||||
#
|
||||
# Provision the two secrets with `bash config/scripts/setup-hourly-release-token.sh`:
|
||||
# HOURLY_RELEASE_APP_ID the App's numeric id
|
||||
# HOURLY_RELEASE_APP_PRIVATE_KEY the App's .pem private key
|
||||
#
|
||||
# Installation tokens live one hour, so the build job mints twice. Install and
|
||||
# build need no token at all, and notarization can hold the publish step for tens
|
||||
# of minutes; minting again once the build is done starts the clock at the first
|
||||
# call that actually uses it rather than burning a third of it on `pnpm install`.
|
||||
# A pathological retry can still outrun the second token, but the release is an
|
||||
# unpublished draft until the manifest check passes, so the damage is a stranded
|
||||
# invisible draft — and the build-number query counts drafts, so it holds its
|
||||
# number and the next run does not reuse it.
|
||||
# Publish signed/notarized main builds to orca-hourly; notarization preserves TCC grants across updates. PR/release gates own validation. Separate repo avoids hourly tags crowding stable/RC Atom entries. Provision shared HOURLY_RELEASE_APP_ID/private key with setup-hourly-release-token.sh and App write access there. Refresh one-hour token after read-only build; a retry may still outlive it, leaving only an invisible draft whose build number remains reserved until manifests validate.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -96,9 +63,7 @@ jobs:
|
||||
echo "Forced dispatch; building $head_sha."
|
||||
exit 0
|
||||
fi
|
||||
# The previous hourly records its source commit in the release body.
|
||||
# Drafts are excluded: an unpublished leftover never shipped, so treating
|
||||
# it as "the last build" would skip a build that never actually happened.
|
||||
# Drafts never shipped; exclude them when comparing the previous hourly’s source commit.
|
||||
last_body="$(gh release list --repo "$HOURLY_REPO" --limit 20 --json tagName,isDraft \
|
||||
--jq 'map(select(.isDraft | not)) | .[0].tagName // empty' 2>/dev/null || true)"
|
||||
if [[ -z "$last_body" ]]; then
|
||||
@@ -140,10 +105,7 @@ jobs:
|
||||
head_sha: ${{ needs.preflight.outputs.head_sha }}
|
||||
published: ${{ steps.publish_live.outcome == 'success' && 'true' || 'false' }}
|
||||
runs-on: blacksmith-6vcpu-macos-15
|
||||
# Why 150: it must exceed the worst case the retry budgets below can produce
|
||||
# (install 3x10 + publish 2x45 = 120, plus ~25 for checkout/build/verify), or
|
||||
# the job is killed mid-retry and no cleanup step runs at all. A typical run
|
||||
# is far shorter — this is the notary queue's tail, not its median.
|
||||
# Allow 150min for install 3×10 + publish 2×45 + ~25 setup/build/verify; outer timeout otherwise interrupts cleanup.
|
||||
timeout-minutes: 150
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
@@ -154,10 +116,7 @@ jobs:
|
||||
ref: ${{ needs.preflight.outputs.head_sha }}
|
||||
# Version helpers only read HEAD; published versions come from git tags.
|
||||
fetch-depth: 1
|
||||
# Why: this job only reads stablyai/orca and never pushes; every write
|
||||
# goes to the hourly repo through a minted App token passed by env.
|
||||
# Not persisting the checkout credential shrinks the blast radius if a
|
||||
# build step is compromised (zizmor: artipacked).
|
||||
# Read this repo without persisted credentials; write orca-hourly via env App token (zizmor: artipacked).
|
||||
persist-credentials: false
|
||||
|
||||
- name: Mint hourly repo token
|
||||
@@ -182,8 +141,7 @@ jobs:
|
||||
pnpm-lock.yaml
|
||||
mobile/pnpm-lock.yaml
|
||||
|
||||
# Why: packaging bundles the pinned Node into the macOS terminal host. Keyed on the pin alone
|
||||
# with no restore-keys, so a pin bump starts empty instead of carrying old archives forward.
|
||||
# Cache bundled host Node by exact pin without restore keys.
|
||||
- name: Locate the pinned Node runtime cache
|
||||
shell: bash
|
||||
run: echo "ORCA_NODE_RUNTIME_CACHE_DIR=$HOME/Library/Caches/orca-node-runtime" >>"$GITHUB_ENV"
|
||||
@@ -204,8 +162,7 @@ jobs:
|
||||
restore-keys: |
|
||||
electron-builder-mac-
|
||||
|
||||
# Why both CPUs: the mac config packages x64 and arm64 from this arm64
|
||||
# runner, so the install must carry both variants of the native optional deps.
|
||||
# Install both x64/arm64 optional natives for both macOS packages.
|
||||
- name: Install dependencies
|
||||
uses: nick-fields/retry@v4
|
||||
with:
|
||||
@@ -214,12 +171,10 @@ jobs:
|
||||
retry_wait_seconds: 30
|
||||
command: pnpm install --frozen-lockfile --cpu=current,x64,arm64
|
||||
|
||||
# Why here: electron-builder's beforePack requires out/mobile-web, and the bundle
|
||||
# build resolves React Native and Expo from mobile/node_modules.
|
||||
# Build required out/mobile-web from mobile’s React Native/Expo install.
|
||||
- uses: ./.github/actions/install-mobile-dependencies
|
||||
|
||||
# Why: signing is what makes an hourly installable over an existing Orca, so
|
||||
# a missing cert must fail here rather than after a 20-minute build.
|
||||
# Require cert before expensive build; signing enables installation over existing Orca.
|
||||
- name: Verify macOS signing environment
|
||||
run: node config/scripts/verify-macos-release-env.mjs
|
||||
env:
|
||||
@@ -237,30 +192,17 @@ jobs:
|
||||
MAIN_REPO_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Existing titles, which carry the build number this series continues
|
||||
# from. The script picks the number, because it restarts per base version
|
||||
# and only the script knows which base this build resolved to.
|
||||
#
|
||||
# Why drafts count here but not in the freshness check: that check asks
|
||||
# "did this commit ship", where a draft is a no. This one asks "is the
|
||||
# number free", where a stranded draft still holds one.
|
||||
# The script continues numbering for its resolved base version; stranded drafts still reserve numbers,
|
||||
# although freshness checks exclude them because they never shipped.
|
||||
names="$(gh release list --repo "$HOURLY_REPO" --limit 200 --json name \
|
||||
--jq '.[].name // empty')"
|
||||
# Why git tags, not GitHub releases: unpublishing a buggy cut deletes the
|
||||
# GitHub release and leaves the tag. On 2026-09-14 we deleted v1.4.202's
|
||||
# release for a bug; hourlies had already climbed to 1.4.203, then
|
||||
# `gh release list` fell back to v1.4.201 and the next hourlies shipped
|
||||
# as 1.4.202-hourly — which electron-updater will not install over
|
||||
# 1.4.203-hourly or over the still-tagged 1.4.202. A separate token
|
||||
# because GH_TOKEN above is the App's, scoped to the hourly repo. Empty
|
||||
# on failure — the script then falls back to package.json, which is
|
||||
# stale but never wrong enough to fail a build.
|
||||
# Tags survive release deletion, preventing updater version regression. Use a separate token because
|
||||
# GH_TOKEN’s App grant is scoped to the channel repo; lookup failure falls back to package.json.
|
||||
# 2026-09-14: deleting v1.4.202 made release-list fallback ship 1.4.202-hourly below installed 1.4.203-hourly.
|
||||
main_tags="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api \
|
||||
"repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" \
|
||||
--jq '.[].ref | sub("^refs/tags/"; "")' || true)"
|
||||
# Already-shipped channel tags are a second floor: even if main's tag
|
||||
# list is empty this run, a 1.4.203-hourly already out must not be
|
||||
# followed by a 1.4.202-hourly.
|
||||
# Already-shipped channel tags also floor versions: never follow installed 1.4.203-hourly with 1.4.202-hourly.
|
||||
channel_tags="$(gh release list --repo "$HOURLY_REPO" --limit 200 --json tagName \
|
||||
--jq '.[].tagName' || true)"
|
||||
published="$main_tags"$'\n'"$channel_tags"
|
||||
@@ -269,11 +211,7 @@ jobs:
|
||||
node config/scripts/hourly-build-version.mjs \
|
||||
>"$RUNNER_TEMP/hourly-identity.txt"
|
||||
grep -E '^(version|build_number)=' "$RUNNER_TEMP/hourly-identity.txt"
|
||||
# Why check rather than trust: the checkout above pins the resolved main commit, but a
|
||||
# workflow_dispatch runs this file from whatever branch was dispatched. A
|
||||
# branch that edits this step while main still has the old script yields
|
||||
# an empty name and an untitled release — silent, and only visible once
|
||||
# someone opens the releases page. Fail here instead.
|
||||
# Dispatch YAML can be newer than the pinned source’s version script; reject missing output before publishing an untitled release.
|
||||
if ! grep -q '^name=' "$RUNNER_TEMP/hourly-identity.txt"; then
|
||||
echo "::error::hourly-build-version.mjs emitted no release name; this workflow and main's copy of the script are out of sync."
|
||||
exit 1
|
||||
@@ -284,9 +222,7 @@ jobs:
|
||||
run: pnpm build:release
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
# Why: hourly builds are not an official channel — telemetry's transport
|
||||
# gate accepts only 'stable' or 'rc', so leaving this unset keeps them
|
||||
# silent, which is correct for unvetted dev artifacts.
|
||||
# Unset dev identity so official-only stable/rc telemetry stays silent.
|
||||
ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token
|
||||
|
||||
# After the app build so nothing that cleans out/ can drop it; electron-builder ships it.
|
||||
@@ -296,11 +232,7 @@ jobs:
|
||||
name: orcad-template
|
||||
path: out/orcad-template
|
||||
|
||||
# Why a second mint: everything from here on writes to the hourly repo, and
|
||||
# the notary round trip inside the publish step can be tens of minutes. The
|
||||
# token minted at the top has already spent install + build of its one hour
|
||||
# on steps that never touched it; restarting the clock here gives the slow
|
||||
# part the full budget.
|
||||
# Refresh write token after build to give long notary/upload work a full hour.
|
||||
- name: Re-mint hourly repo token for publish
|
||||
id: app_token_publish
|
||||
uses: actions/create-github-app-token@v2
|
||||
@@ -320,19 +252,10 @@ jobs:
|
||||
SHA: ${{ needs.preflight.outputs.head_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Kept at 12 even though the title shows 7: the freshness check above
|
||||
# parses this back out of the body to decide whether main has moved.
|
||||
# Freshness parses the 12-character source SHA from the body even though titles show seven.
|
||||
short_sha="${SHA:0:12}"
|
||||
# Why create it up front: electron-builder then uploads into a known tag
|
||||
# rather than inferring one from package.json.
|
||||
#
|
||||
# Why --draft: everything between here and the manifest check is a window
|
||||
# where the release exists but has no installable assets. A draft is
|
||||
# absent from the releases list and from listReleaseBuilds, so a job that
|
||||
# dies in that window — including a hard kill by the job timeout, which
|
||||
# runs no cleanup step at all — leaves something invisible rather than a
|
||||
# tag the picker offers and the download 404s on. It is flipped live only
|
||||
# after the manifest is verified.
|
||||
# Precreate a known tag as draft so builder cannot infer it from package.json.
|
||||
# Keep incomplete assets invisible until manifest verification, even if timeout kills the job without cleanup.
|
||||
gh release create "$TAG" \
|
||||
--repo "$HOURLY_REPO" \
|
||||
--title "$NAME" \
|
||||
@@ -355,17 +278,13 @@ jobs:
|
||||
- name: Publish hourly macOS artifacts
|
||||
uses: nick-fields/retry@v4
|
||||
with:
|
||||
# Why 45 like the release pipeline: an attempt is pack + notarize +
|
||||
# upload, and the notary queue is the unbounded part. Why 2 attempts and
|
||||
# not 3: a missed hourly costs an hour, and the next cron picks the same
|
||||
# commit up, so a third attempt buys less than it costs in runner time.
|
||||
# Allow two 45min pack/notarize/upload attempts; next hourly picks the same commit rather than paying for a third.
|
||||
timeout_minutes: 45
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 30
|
||||
command: node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON config/scripts/pinned-node-downloads.mjs darwin-arm64 darwin-x64 && node config/scripts/ensure-native-runtime.mjs --runtime=electron && if [ -f config/scripts/build-mac-notarization-overlap.mjs ]; then ORCA_MAC_HOURLY=1 node config/scripts/build-mac-notarization-overlap.mjs --publish always; else ORCA_MAC_HOURLY=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --mac --publish always; fi
|
||||
env:
|
||||
# Why: electron-builder's github publisher targets the repo named in the
|
||||
# config; the token must therefore carry write access to orca-hourly.
|
||||
# Give publisher token write access to configured orca-hourly repo.
|
||||
GH_TOKEN: ${{ steps.app_token_publish.outputs.token }}
|
||||
ORCA_HOURLY_BUILD_VERSION: ${{ steps.hourly.outputs.version }}
|
||||
ORCA_BUILD_COMMIT: ${{ steps.hourly.outputs.commit }}
|
||||
@@ -373,17 +292,12 @@ jobs:
|
||||
ORCA_REQUIRE_ORCAD_TEMPLATE: '1'
|
||||
CSC_LINK: ${{ secrets.MAC_CERTS }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
|
||||
# Why all three: electron-builder's notarize step authenticates to the
|
||||
# Apple notary service with the app-specific password, not with the
|
||||
# signing cert. Omitting them fails the build rather than skipping it,
|
||||
# since `notarize` is now on for this path.
|
||||
# Require Apple id/app-specific password/team for notarization, separate from signing cert.
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
# Why: the updater resolves a tag, then fetches latest-mac.yml from it. A
|
||||
# release missing that manifest is a tag the picker offers and the download
|
||||
# 404s on, so fail loudly instead of leaving a broken entry.
|
||||
# Verify latest-mac.yml before exposing release; missing manifests cause picker download 404s.
|
||||
- name: Verify update manifest published
|
||||
shell: bash
|
||||
env:
|
||||
@@ -394,10 +308,7 @@ jobs:
|
||||
assets="$(gh release view "$TAG" --repo "$HOURLY_REPO" --json assets --jq '.assets[].name')"
|
||||
echo "Published assets:"
|
||||
echo "$assets"
|
||||
# Why exit 1 without deleting here: the release is still a draft, so it is
|
||||
# already invisible to users, and the failure handler below owns cleanup.
|
||||
# Deleting inline under `set -e` would also let the delete's exit code
|
||||
# preempt this explicit failure.
|
||||
# Drafts are already invisible; let failure cleanup delete them so set -e cannot mask this explicit failure.
|
||||
for required in latest-mac.yml; do
|
||||
if ! grep -qx "$required" <<<"$assets"; then
|
||||
echo "::error::Hourly draft $TAG is missing $required; the updater could not install it."
|
||||
@@ -409,9 +320,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Why this is the last mutating step: publishing the draft is what makes the
|
||||
# build visible to listReleaseBuilds. Doing it only after the manifest check
|
||||
# means the picker can never offer a release whose assets are incomplete.
|
||||
# Publish only after manifests verify so picker never offers incomplete assets.
|
||||
- name: Publish the verified release
|
||||
id: publish_live
|
||||
shell: bash
|
||||
@@ -421,21 +330,11 @@ jobs:
|
||||
NAME: ${{ steps.hourly.outputs.name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# --title again: electron-builder resolves this draft by tag and may
|
||||
# rewrite its title on upload. Re-asserting here means the name the
|
||||
# picker reads is the one composed above, whatever it did in between.
|
||||
# Reassert the picker title after electron-builder may rewrite the tag’s draft on upload.
|
||||
gh release edit "$TAG" --repo "$HOURLY_REPO" --draft=false --prerelease --title "$NAME"
|
||||
echo "Published $TAG as \"$NAME\""
|
||||
|
||||
# Why: a draft left behind by a failed publish is invisible to users but still
|
||||
# holds its tag name, so the next run for the same minute would collide.
|
||||
#
|
||||
# Why it is gated on publish_live not having succeeded: a later failure (the
|
||||
# prune step) must not delete a release that already went live and that users
|
||||
# may already be installing. A job killed by the outer timeout runs no steps
|
||||
# at all — which is exactly why the release stays a draft until verified.
|
||||
# Why cancelled() too: a run stopped from the Actions UI is not a failure(),
|
||||
# so without it a manual cancel mid-publish would strand the draft.
|
||||
# Delete failed/cancelled drafts but never publish_live releases, including after prune failure; outer timeouts run no cleanup.
|
||||
- name: Discard the draft release on failure
|
||||
if: >-
|
||||
(failure() || cancelled()) && steps.release.outputs.tag != '' &&
|
||||
@@ -452,34 +351,18 @@ jobs:
|
||||
echo "::warning::Could not discard draft $TAG; remove it manually."
|
||||
|
||||
- name: Prune old hourly releases
|
||||
# Only after a live publish: $TAG is then a non-draft we must not delete,
|
||||
# and failed runs should not reshuffle retention around a draft that the
|
||||
# failure path is about to discard.
|
||||
# Prune only after live publish; failed runs leave draft disposal to cleanup.
|
||||
if: steps.publish_live.outcome == 'success'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app_token_publish.outputs.token }}
|
||||
# Protect the tag this run just shipped; at the retain cap, a bad sort
|
||||
# can otherwise mark the newest release as stale and delete it.
|
||||
# Exclude the just-published tag from retention pruning.
|
||||
TAG: ${{ steps.release.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Why: --cleanup-tag so pruning does not leave orphan tags behind that
|
||||
# keep showing up in tag lists with no release or assets attached.
|
||||
# Drafts are excluded so retention counts shipped builds only; a stale
|
||||
# draft is handled by the failure path, not by the retention window.
|
||||
#
|
||||
# Sort by publishedAt (not createdAt). Many non-draft hourlies share one
|
||||
# createdAt (bulk import / re-create), so createdAt ranking is unstable:
|
||||
# reverse of a stable sort then drops the just-published tag past the
|
||||
# retain window. publishedAt is real recency; tagName (...YYYYMMDDHHMM)
|
||||
# is the deterministic tie-break.
|
||||
#
|
||||
# Why force $TAG to the front before slicing: a hard retain-window seat
|
||||
# for this run's release. Dropping $TAG from the list *before* the slice
|
||||
# would permanently keep retain+1 releases; skipping it only in the
|
||||
# delete loop would under-prune when the sort is still wrong. Partition
|
||||
# keeps relative order of every other tag.
|
||||
# Delete pruned tags too; exclude drafts owned by failure cleanup. Rank by publishedAt/tagName:
|
||||
# createdAt ties (bulk import/recreation) can evict the just-published release. Partition $TAG to
|
||||
# the front before slicing so it owns exactly one retained seat without under-pruning or keeping retain+1.
|
||||
jq_filter='map(select(.isDraft | not)) | sort_by(.publishedAt // "", .tagName) | reverse'
|
||||
if [[ -n "${TAG:-}" ]]; then
|
||||
jq_filter+=" | (map(select(.tagName == \"${TAG//\"/\\\"}\")) + map(select(.tagName != \"${TAG//\"/\\\"}\")))"
|
||||
@@ -503,21 +386,10 @@ jobs:
|
||||
echo "::warning::Could not prune $tag"
|
||||
done <<<"$stale"
|
||||
|
||||
# Why this runs after the mac leg rather than beside it: the tag and version
|
||||
# are computed inside that job, so until it has run nothing else can name the
|
||||
# release to upload into. The cost is small enough not to matter — the Windows
|
||||
# leg measures ~7.5 min (install 2m45, build 35s, NSIS package 3m) against a
|
||||
# ~9.5 min mac run, which keeps a hourly run far inside its interval.
|
||||
#
|
||||
# Why `./` rather than a pinned `@main`: `uses:` resolves against the ref this
|
||||
# file itself came from, which for an ordinary dispatch is main. Someone who
|
||||
# deliberately points the Actions "Use workflow from" picker at a branch
|
||||
# already gets that branch's copy of this entire file, so this follows the same
|
||||
# rule instead of inventing a second one.
|
||||
# Wait for mac tag/version before Windows upload; local ./ workflow consistently follows the dispatch ref.
|
||||
build-hourly-win:
|
||||
needs: build-hourly-mac
|
||||
# Only once the mac release is actually live: there is no release to upload
|
||||
# into otherwise, and the Windows workflow refuses to create one.
|
||||
# Publish Windows only into the existing live mac release.
|
||||
if: needs.build-hourly-mac.outputs.published == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
name: Mobile Android Release
|
||||
|
||||
# Why a separate workflow from iOS: iOS releases go through App Store review,
|
||||
# which can take days. Decoupling the triggers lets an Android release ship
|
||||
# immediately without waiting on iOS, and vice versa.
|
||||
# Release Android independently so App Store review cannot block its shipping cadence.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -23,10 +21,7 @@ jobs:
|
||||
android-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Why: the "Create GitHub Release" step below uses the default GITHUB_TOKEN
|
||||
# to call `gh release create`, which requires contents:write. Without this,
|
||||
# the job builds the APK fine but fails at release time with
|
||||
# "HTTP 403: Resource not accessible by integration".
|
||||
# Grant contents:write for gh release create; build success alone does not authorize publication.
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -75,10 +70,7 @@ jobs:
|
||||
- name: Build Android release APK
|
||||
run: cd android && ./gradlew assembleRelease
|
||||
|
||||
# Why: an install that fails with a missing certificate or a package-parse error is
|
||||
# usually a download that died near the end, and the signature block sits in the last
|
||||
# ~100 KB. Publishing the size and digest is what lets a reporter tell a truncated
|
||||
# download apart from a bad build without anyone re-deriving them from the asset.
|
||||
# Publish size/digest to distinguish truncated downloads (missing tail signature block) from bad APK builds.
|
||||
- name: Checksum the APK
|
||||
id: apk
|
||||
run: |
|
||||
@@ -93,10 +85,8 @@ jobs:
|
||||
apk="${apks[0]}"
|
||||
bytes="$(wc -c < "$apk" | tr -d ' ')"
|
||||
sha256="$(sha256sum "$apk" | cut -d ' ' -f 1)"
|
||||
# A sibling file in `sha256sum -c` format, so verifying a download is one command
|
||||
# and no retyped digest. The asset globs below match *.apk and skip it. The ` *`
|
||||
# marker is binary mode: Git Bash's sha256sum reads text mode as a licence to
|
||||
# translate line endings while hashing, which would fail on a valid APK.
|
||||
# Publish sha256sum -c sidecars, excluded by *.apk globs. Use binary “ *” mode:
|
||||
# Git Bash text-mode newline translation can reject a valid APK.
|
||||
printf '%s *%s\n' "$sha256" "$(basename "$apk")" > "$apk.sha256"
|
||||
|
||||
{
|
||||
@@ -144,18 +134,13 @@ jobs:
|
||||
'${{ steps.apk.outputs.bytes }}' '${{ steps.apk.outputs.sha256 }}'
|
||||
}
|
||||
|
||||
# The section is always last, so dropping from its heading to EOF leaves the
|
||||
# generated notes intact. \r* because a body round-tripped through the API has CRLFs.
|
||||
# Verification is last; replace from heading to EOF, accepting API-roundtripped CRLFs.
|
||||
drop_verification_section() {
|
||||
sed '/^### Verify your download\r*$/,$d'
|
||||
}
|
||||
|
||||
# Why: reuse the desktop release path's character-safe truncation so a multi-byte
|
||||
# character cannot be split at the cap. The section's own length is reserved out of
|
||||
# that cap, because appending after truncating would push a near-limit body past
|
||||
# GitHub's API limit and fail the call — on the upload path, after --clobber has
|
||||
# already replaced the assets. `wc -c` counts bytes, so the section's multi-byte
|
||||
# characters over-reserve, which errs toward a shorter body.
|
||||
# Truncate by characters like desktop releases and reserve the appended section inside GitHub’s cap,
|
||||
# or --clobber could replace assets before notes fail. wc -c over-reserves multibyte section text safely.
|
||||
write_notes_with_verification() {
|
||||
NOTES_FILE="$notes_file" \
|
||||
NOTES_RESERVE="$(verification_section | wc -c | tr -d ' ')" \
|
||||
@@ -179,26 +164,21 @@ jobs:
|
||||
--clobber \
|
||||
android/app/build/outputs/apk/release/*.apk \
|
||||
"${{ steps.apk.outputs.checksum }}"
|
||||
# Why the body is rewritten too: --clobber replaced the APK, so a digest left
|
||||
# over from the previous build now describes a file nobody can download, and a
|
||||
# reader comparing against it would reject a good APK.
|
||||
# --clobber replaced the APK; rewrite its digest so users cannot compare against obsolete bytes.
|
||||
gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json body --jq .body \
|
||||
| drop_verification_section > "$notes_file"
|
||||
write_notes_with_verification
|
||||
gh release edit "$tag" --repo "$GITHUB_REPOSITORY" --notes-file "$notes_file"
|
||||
else
|
||||
# Why: release tags live on side branches, so GitHub's automatic
|
||||
# previous-tag detection reaches back several releases; that body
|
||||
# already exceeds the 125000-character API limit and grows each
|
||||
# release. Pin the comparison base and cap the size.
|
||||
# Side-branch release tags make automatic previous-tag detection span multiple releases and exceed
|
||||
# the 125000-character API cap; pin the comparison base and truncate.
|
||||
previous_tag="$(
|
||||
gh release list --repo "$GITHUB_REPOSITORY" --limit 200 --json tagName --jq '.[].tagName' \
|
||||
| grep '^mobile-android-v' | grep -Fxv "$tag" | sort -V | tail -1 || true
|
||||
)"
|
||||
|
||||
if [ -n "$previous_tag" ]; then
|
||||
# Why: gh writes the JSON error body to stdout on an HTTP error, so a
|
||||
# non-empty file is not proof of success — gate on exit status.
|
||||
# gh writes HTTP error JSON to stdout too; require exit success, not merely a nonempty file.
|
||||
if ! gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" -X POST \
|
||||
-f tag_name="$tag" \
|
||||
-f target_commitish="$GITHUB_SHA" \
|
||||
@@ -222,8 +202,7 @@ jobs:
|
||||
"${{ steps.apk.outputs.checksum }}"
|
||||
fi
|
||||
|
||||
# Why a separate job: the release is already published here, so a failed bump only leaves
|
||||
# the site's APK download on the previous release and must not fail the release run.
|
||||
# Keep site bump advisory after publication; failure only leaves its download link on the prior APK.
|
||||
bump-apk-download:
|
||||
needs: android-build
|
||||
if: needs.android-build.outputs.publish_release == 'true'
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
name: Mobile iOS Release
|
||||
|
||||
# Why a separate workflow from Android: iOS releases go through App Store
|
||||
# review, which can take days. Decoupling the triggers lets an Android release
|
||||
# ship immediately without waiting on iOS, and vice versa.
|
||||
# Release iOS independently so App Store review cannot block Android.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -28,19 +26,13 @@ permissions:
|
||||
|
||||
jobs:
|
||||
ios-build:
|
||||
# GitHub-hosted macOS runner: required for Xcode. Expo SDK 55's
|
||||
# expo-modules-core declares swift_version 6.0 and uses Swift 6 syntax
|
||||
# (@MainActor isolation). Xcode 16.x (macos-15) can't even parse it
|
||||
# ("unknown attribute 'MainActor'"), so we need Xcode 26.x → macos-26.
|
||||
# Use macos-26/Xcode 26 for Expo SDK 55’s Swift 6 syntax/isolation; Xcode 16 runners cannot parse it.
|
||||
runs-on: macos-26
|
||||
# Archive + upload is ~30–40m when healthy; 90m leaves margin for setup.
|
||||
timeout-minutes: 90
|
||||
|
||||
env:
|
||||
# Why: this job and ios-distribute resolve gems ~25 minutes apart, so both
|
||||
# must install the committed Gemfile.lock exactly. Frozen turns a lockfile
|
||||
# drift into a setup failure instead of two different fastlane versions in
|
||||
# one release.
|
||||
# Freeze Gemfile.lock in both jobs; installs ~25min apart must not resolve different fastlane versions.
|
||||
BUNDLE_FROZEN: 'true'
|
||||
|
||||
outputs:
|
||||
@@ -106,12 +98,7 @@ jobs:
|
||||
working-directory: mobile/ios
|
||||
run: bundle exec pod install
|
||||
|
||||
# Why: `-allowProvisioningUpdates` + the App Store Connect API key can
|
||||
# create/refresh provisioning profiles, but it cannot recreate the
|
||||
# distribution certificate's PRIVATE KEY across runs. So we import a
|
||||
# pre-exported distribution .p12 (created once via Apple Developer) into a
|
||||
# throwaway keychain. The keychain is ephemeral to the runner and torn
|
||||
# down with the VM; nothing secret is written to the repo.
|
||||
# Import exported distribution .p12 into ephemeral runner keychain: provisioning updates/API key cannot recreate its private key.
|
||||
- name: Import distribution certificate
|
||||
env:
|
||||
IOS_DIST_CERT_P12: ${{ secrets.IOS_DIST_CERT_P12 }}
|
||||
|
||||
@@ -10,9 +10,7 @@ on:
|
||||
- converted_to_draft
|
||||
paths:
|
||||
- 'mobile/**'
|
||||
# Why all of src/shared: mobile imports hundreds of its modules, directly and through the RPC
|
||||
# recordings and the host params check, so any shared edit can move a golden or break mobile's
|
||||
# typecheck. Why the root lockfile: those modules resolve their packages from the root install.
|
||||
# Route shared edits to mobile’s imports/RPC recordings/typecheck; root lockfile changes affect those modules’ root-resolved packages.
|
||||
- 'src/shared/**'
|
||||
# Mobile's Node RPC recording tools import this package.
|
||||
- 'src/packages/process-host/**'
|
||||
@@ -27,8 +25,7 @@ on:
|
||||
- 'src/main/runtime/agent-session-*.ts'
|
||||
- 'src/main/native-chat/agent-session-wire/**'
|
||||
- 'src/main/runtime/structured-attention-original-delivery.test-fixture.ts'
|
||||
# Why: this job holds the only checks that load the Fastfile, so edits to
|
||||
# it or to the release workflow it guards must re-run them.
|
||||
# Rerun the only Fastfile-loading checks for release-workflow/Fastfile edits.
|
||||
- '.github/workflows/mobile.yml'
|
||||
- 'config/mobile-ci.vitest.config.mjs'
|
||||
- 'config/scripts/run-mobile-ci-tests.mjs'
|
||||
@@ -57,9 +54,7 @@ on:
|
||||
- '!src/main/**/*.test.[jt]s'
|
||||
- '!src/main/**/*.test.[jt]sx'
|
||||
- '!src/main/**/*.test.[cm][jt]s'
|
||||
# Why main too: two pull requests can each pass against their own base and disagree once both
|
||||
# land, and `verify` never runs on main. The same source paths as above, less the CI inputs only
|
||||
# `verify` reads.
|
||||
# Check matching source inputs on main too: independently passing PRs can conflict after merge, and verify is PR-only.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -98,9 +93,7 @@ on:
|
||||
- '!src/main/**/*.test.[cm][jt]s'
|
||||
|
||||
concurrency:
|
||||
# Per commit on main, not per branch. GitHub cancels any PENDING run in a group when a new one
|
||||
# queues, whatever `cancel-in-progress` says, so one shared main group drops the middle merge of
|
||||
# three -- and a merge that breaks main there is exactly what `main-tests` checks for.
|
||||
# Key main concurrency by commit: GitHub replaces pending runs even with cancel-in-progress:false, hiding middle-merge regressions.
|
||||
group: mobile-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -111,15 +104,11 @@ jobs:
|
||||
verify:
|
||||
name: Mobile verify
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.draft != true
|
||||
# Why ARM: 209s of this job is Vitest and nothing here needs x86: no Android SDK, emulator, gradle,
|
||||
# Hermes or Watchman, no docker, and no artifacts. The Gemfile.lock lists the generic `ruby`
|
||||
# platform, so frozen bundler installs without an aarch64-linux entry.
|
||||
# Use ARM for pure Vitest; no x86 native toolchain is needed and Gemfile.lock’s ruby platform supports frozen install.
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
|
||||
|
||||
env:
|
||||
# Why: an unfrozen bundler silently re-resolves when Gemfile.lock drifts
|
||||
# from the Gemfile, which is how the release jobs could land on different
|
||||
# fastlane versions in the first place. Fail here instead.
|
||||
# Freeze bundler to reject Gemfile drift before release jobs resolve different fastlane versions.
|
||||
BUNDLE_FROZEN: 'true'
|
||||
ORCA_MOBILE_TYPECHECK_COMPILER: 'bun'
|
||||
|
||||
@@ -152,9 +141,7 @@ jobs:
|
||||
echo 'should_run=true' >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# bundler-cache installs mobile/Gemfile.lock, so this job is also what
|
||||
# proves the pinned fastlane the release workflow depends on still
|
||||
# resolves — before a release run finds out.
|
||||
# Bundler-cache proves the release’s pinned fastlane still resolves before a release needs it.
|
||||
- name: Setup Ruby and fastlane
|
||||
id: ruby-tools
|
||||
background: true
|
||||
@@ -168,8 +155,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Why here: pr.yml runs these import rules before mobile's install, when mobile's tsconfig
|
||||
# (which extends expo's) can't load, so its import cycles were never seen. Zero tolerance.
|
||||
# Check mobile import cycles after Expo deps install; pre-install pr.yml cannot load its tsconfig.
|
||||
- name: Lint mobile imports with mobile dependencies installed
|
||||
working-directory: .
|
||||
run: node node_modules/oxlint/bin/oxlint --config config/oxlint-code-quality-native-plugins.json mobile --deny-warnings
|
||||
@@ -189,12 +175,7 @@ jobs:
|
||||
background: true
|
||||
run: node scripts/check-tests-typecheck-ratchet.mjs
|
||||
|
||||
# This includes the bridged replay of the whole recording corpus, which used to be a second
|
||||
# step of its own behind RPC_FOUNDATION_BRIDGE=1. A gate nobody can forget to set is the point:
|
||||
# it fails when a divergence class grows, when a divergence lands in no class at all, or when
|
||||
# one of the 103 goldens inside the C1 page closure changes the verdict it is pinned to. It is
|
||||
# ~3 min of test time on its own, and Vitest runs it on a worker beside the rest of the suite,
|
||||
# so folding it in costs a fraction of that in wall time and one step less to skip.
|
||||
# Run bridged recording corpus unconditionally: reject growing/unclassified divergence and changed verdicts among 103 C1 goldens. Share worker time instead of a separate skippable env-gated step.
|
||||
- name: Detect mobile test inputs
|
||||
id: test-scope
|
||||
shell: bash
|
||||
@@ -221,9 +202,7 @@ jobs:
|
||||
# All checks read the completed install; compilers need not hold up test workers.
|
||||
- wait: [production-types, test-types, ruby-tools]
|
||||
|
||||
# Reports, never gates: the goldens are content-addressed JSON, so their raw diff is hashes.
|
||||
# This decodes which recorded behaviour the pull request moves into the job summary, and runs
|
||||
# after a red Test too, when a reviewer most wants it.
|
||||
# Decode hash-addressed golden behavior changes for review, including after failed tests; advisory only.
|
||||
- name: Summarize RPC recording changes
|
||||
if: ${{ !cancelled() }}
|
||||
continue-on-error: true
|
||||
@@ -237,11 +216,7 @@ jobs:
|
||||
if: steps.ruby-scope.outputs.should_run != 'false'
|
||||
run: ruby fastlane/fastfile_testflight_arguments_test.rb
|
||||
|
||||
# Why: nothing else in CI loads the Fastfile, so a syntax error, a broken
|
||||
# require, or an undefined constant only surfaces mid-release — the
|
||||
# ios-distribute job failed every run for six days that way. `lanes` just
|
||||
# loads and lists, so it needs no App Store Connect credentials and makes
|
||||
# no network calls to Apple.
|
||||
# Load/list Fastfile lanes without Apple credentials/network to catch syntax/require/constants before release; ios-distribute failed six days without this guard.
|
||||
- name: Smoke-check the Fastfile
|
||||
if: steps.ruby-scope.outputs.should_run != 'false'
|
||||
env:
|
||||
@@ -341,9 +316,7 @@ jobs:
|
||||
background: true
|
||||
run: node scripts/check-tests-typecheck-ratchet.mjs
|
||||
|
||||
# The whole suite, not only the recordings: the mutant and page-bridge suites read the goldens
|
||||
# too. A red run here names each golden and the `rpc:record` command; the author of the merge
|
||||
# that turned it red re-records in a follow-up.
|
||||
# Run all golden consumers, including mutant/page-bridge suites; failures name the recording and rpc:record command.
|
||||
- name: Setup Bun for tests
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
|
||||
@@ -22,10 +22,7 @@ jobs:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
# Why no checkout: the Files API already has per-file additions/deletions.
|
||||
# Why the default branch and never pull/<n>/head: this job holds a write-scoped
|
||||
# GITHUB_TOKEN, so it may only execute reviewed code. A PR that edits these
|
||||
# scripts takes effect once merged.
|
||||
# Read file counts through API without checkout; execute only default-branch scripts because GITHUB_TOKEN has write scope. PR edits take effect after merge.
|
||||
- name: Count test vs non-test LoC
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
+40
-142
@@ -18,12 +18,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# Why: a README/docs-only PR used to start the full matrix (test shards,
|
||||
# two package jobs, typecheck, git compat, xterm, shell contracts). Path
|
||||
# filters on `on.pull_request` would drop the `verify` check entirely; this
|
||||
# detector keeps verify as the required aggregate and skips the expensive jobs.
|
||||
# Per-job outputs also skip git-compat/xterm/packaging/shell when those
|
||||
# inputs are unchanged; empty diffs fail closed and run everything.
|
||||
# Keep required verify present for docs-only PRs while routing expensive jobs by inputs; empty diffs fail closed to full checks.
|
||||
code_paths:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.draft != true
|
||||
name: detect changes and check repository guards
|
||||
@@ -67,11 +62,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Why depth 50 and not 0: every diff below resolves to HEAD^1, so only the merge commit
|
||||
# and a little slack are needed. Fetching all 8127 refs' commit graph cost ~20s here and
|
||||
# is charged to the start of all 22 jobs, since they all need this one.
|
||||
# Why blob:none stays: the sparse tree below is ~7 files, so there are no blobs to
|
||||
# materialize and no promisor refetch. Measured 9.5s -> 1.6s against 20.7s today.
|
||||
# Fetch 50 commits for merge HEAD^1 diffs instead of all 8127 refs (~20s); sparse ~7-file blob:none checkout avoids materialization/refetch (9.5s→1.6s).
|
||||
fetch-depth: 50
|
||||
filter: blob:none
|
||||
sparse-checkout: |
|
||||
@@ -128,12 +119,9 @@ jobs:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Why HEAD^1 and not --merge-base: HEAD is the pull request merge commit, so its first
|
||||
# parent is the base side already. Computing a merge base instead would require the
|
||||
# payload base SHA to be in the graph, which is what forced a full-history checkout.
|
||||
# PR merge HEAD’s first parent is the base; --merge-base would require the payload SHA/full history.
|
||||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||||
# Why --no-renames: name-only rename detection can report only the destination.
|
||||
# A code file moved under docs/ must still expose its code-side deletion.
|
||||
# --no-renames retains code-side deletions when files move into docs.
|
||||
CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR "$DIFF_BASE" HEAD)"
|
||||
echo "Changed paths:"
|
||||
printf '%s\n' "$CHANGED"
|
||||
@@ -156,13 +144,11 @@ jobs:
|
||||
if git diff --quiet --no-renames --diff-filter=ACDRTUXB "$DIFF_BASE" HEAD; then
|
||||
E2E_SCOPE_ARGS=(--daemon-init-test-support)
|
||||
fi
|
||||
# Source routes are executable contracts so a test can prove exact
|
||||
# authorities, exclusions, and sentinels without evaluating workflow shell.
|
||||
# Executable routes let tests prove authorities/exclusions/sentinels without evaluating shell.
|
||||
TEST_FILES_JSON="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs "${E2E_SCOPE_ARGS[@]}")"
|
||||
echo "test_files=$TEST_FILES_JSON" >> "$GITHUB_OUTPUT"
|
||||
printf '%s\n' "$TEST_FILES_JSON" | node config/scripts/ci-e2e-job-selection.mjs --job-outputs >> "$GITHUB_OUTPUT"
|
||||
# Why its own signal: the real-IME lane is a whole ibus session, not a spec, so it must
|
||||
# trigger on IME source rather than on a spec name in some route's list.
|
||||
# Real IME runs an entire IBus session; route it by IME source, independently of spec lists.
|
||||
NATIVE_IME_SOURCE_CHANGED="$(printf '%s\n' "$CHANGED" | node config/scripts/pr-e2e-source-routing.mjs --native-ime-source)"
|
||||
echo "native_ime_source_changed=$NATIVE_IME_SOURCE_CHANGED" >> "$GITHUB_OUTPUT"
|
||||
WSL_CHANGED="$(git diff --name-only --no-renames --diff-filter=ACDMR "$DIFF_BASE" HEAD)"
|
||||
@@ -192,11 +178,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Why depth 50: the gates below diff against HEAD^1, so no merge base is computed and
|
||||
# the payload base SHA need not be in the graph.
|
||||
# Why no blob:none here, unlike code_paths: this job checks out all 30,226 files, and
|
||||
# the filter then forces a second promisor fetch of nearly every blob. Measured 23s
|
||||
# blobless against ~11s without it.
|
||||
# Fetch 50 for merge HEAD^1, no merge-base graph needed. Full-tree checkout omits blob:none to avoid refetching ~30k blobs (23s versus ~11s).
|
||||
fetch-depth: 50
|
||||
persist-credentials: false
|
||||
|
||||
@@ -216,10 +198,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
run: node -p '"executable=" + process.execPath' >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Why two guarded installs: the mixed root+mobile store entry is 537 MB against
|
||||
# 321 MB for root alone, and restoring it costs 8.6s against 4.6s. Most PRs skip the
|
||||
# mobile install below, so they were paying 216 MB for packages they never link. The
|
||||
# root-only key is also the one the hourly warmer reseeds. Only one of these runs.
|
||||
# Choose one root/mobile install: root-only store is 321MB/4.6s versus mixed 537MB/8.6s, saving unused mobile downloads on most PRs.
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
if: needs.code_paths.outputs.mobile_dependencies != 'true'
|
||||
with:
|
||||
@@ -325,9 +304,7 @@ jobs:
|
||||
# Mobile installation changes import resolution for the native cycle check.
|
||||
- wait: native-code-quality
|
||||
|
||||
# Why here: the changed-code gate lints mobile files too, and its type-aware pass
|
||||
# resolves types from mobile/node_modules. Without the install every mobile type
|
||||
# degrades to an `error` type — reported as phantom findings against the changed lines.
|
||||
# Install mobile before type-aware changed-code lint or unresolved mobile types become phantom error-type findings.
|
||||
- uses: ./.github/actions/install-mobile-dependencies
|
||||
if: needs.code_paths.outputs.static_analysis == 'true' && needs.code_paths.outputs.mobile_dependencies == 'true'
|
||||
|
||||
@@ -419,9 +396,7 @@ jobs:
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: node config/scripts/check-mobile-shared-dependency-versions.mjs
|
||||
|
||||
# Why: extraction writes sorted evidence to an isolated temporary path,
|
||||
# so feature PRs need one normalized AST pass rather than a three-OS matrix.
|
||||
# Use preflight's pinned Bun even when the optional local pin differs.
|
||||
# Extract sorted evidence to isolated temp paths with one normalized platform-independent AST pass; use preflight’s pinned Bun.
|
||||
- name: Verify localization extraction
|
||||
if: '!cancelled()'
|
||||
id: localization-extraction
|
||||
@@ -461,9 +436,7 @@ jobs:
|
||||
- uses: ./.github/actions/build-mobile-web-bundle
|
||||
if: steps.orcad-smoke-scope.outputs.run == 'true'
|
||||
|
||||
# Why both: the ratchet proves nothing reachable from the runtime imports electron,
|
||||
# which is a property of the import graph. This proves the Node artifact it enables
|
||||
# actually boots, pairs, creates a worktree and round-trips a real PTY.
|
||||
# Pair import-graph electron exclusion with built-Node boot/pair/worktree/PTY smoke; graph purity alone does not prove runtime operation.
|
||||
- name: Boot orcad and round-trip a terminal
|
||||
if: steps.orcad-smoke-scope.outputs.run == 'true'
|
||||
env:
|
||||
@@ -503,11 +476,7 @@ jobs:
|
||||
|
||||
- wait: [localization-catalogs, localization-extraction]
|
||||
|
||||
# Why: project-owned type declarations must live in .ts so tsc
|
||||
# actually checks them. TypeScript's skipLibCheck: true (inherited
|
||||
# from @electron-toolkit/tsconfig) silently widens unresolved names
|
||||
# in .d.ts to `any`, which is how #1186 shipped a broken IPC signature
|
||||
# past typecheck. See .github/CONTRIBUTING.md#type-declarations-prefer-ts-over-dts.
|
||||
# Keep project declarations in .ts: skipLibCheck silently widened unresolved .d.ts names to any, hiding #1186 IPC breakage. See .github/CONTRIBUTING.md#type-declarations-prefer-ts-over-dts.
|
||||
- name: Guard against project-owned .d.ts in preload/shared
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: |
|
||||
@@ -618,8 +587,7 @@ jobs:
|
||||
"alpine/git:edge-2.38.1|2.38.1"
|
||||
"alpine/git:v2.49.1|2.49.1"
|
||||
)
|
||||
# Why pull up front: a lane's first `docker run` otherwise pulls its image
|
||||
# while the sibling lane is mid-test, and that stall is charged to the test.
|
||||
# Pull before tests so a sibling’s timed lane cannot be stalled by the first image download.
|
||||
for spec in "${specs[@]}"; do
|
||||
docker pull --quiet "${spec%%|*}"
|
||||
done
|
||||
@@ -655,12 +623,7 @@ jobs:
|
||||
done
|
||||
exit "$status"
|
||||
|
||||
# Why this job: Orca's session index-heal depends on a Codex behavior — a
|
||||
# `thread/read` of an unindexed rollout performs a read-repair that inserts the
|
||||
# `threads` row. Every unit test drives a stub app-server and asserts only that the
|
||||
# call did not error, so if Codex dropped the repair they would all stay green while
|
||||
# the subsystem went inert. This runs the pinned real binary and fails when the
|
||||
# repair stops happening. Pinned because the binary is the thing expected to drift.
|
||||
# Run pinned real Codex: thread/read must repair an unindexed rollout’s threads row; stub-only units cannot detect upstream removal of this behavior.
|
||||
codex_index_heal_contract:
|
||||
name: Codex index-heal contract
|
||||
needs: [code_paths, preflight]
|
||||
@@ -692,9 +655,7 @@ jobs:
|
||||
|
||||
- name: Verify Codex index-heal contract
|
||||
env:
|
||||
# Why REQUIRED: without a binary the suite skips, and a job that skips
|
||||
# reports success. This turns a failed or missing install into a red test
|
||||
# instead of a green no-op.
|
||||
# Require installed binary or skip would report green without exercising the contract.
|
||||
ORCA_CODEX_CONTRACT_REQUIRED: '1'
|
||||
ORCA_CODEX_CONTRACT_VERSION: ${{ env.CODEX_CLI_VERSION }}
|
||||
run: |
|
||||
@@ -719,9 +680,7 @@ jobs:
|
||||
pnpm exec vitest run --config config/vitest.config.ts \
|
||||
src/main/pty/codex-no-daemon-binary-contract.test.ts
|
||||
|
||||
# Why both pins and latest: Orca approves its managed-home hook entry with a hash
|
||||
# Codex computes, so a release that changes it must redden here before users
|
||||
# meet a review screen.
|
||||
# Test pins plus latest because Codex’s managed-home approval hash can change before users encounter review prompts.
|
||||
- name: Verify Codex hook file-entry contract (pinned)
|
||||
env:
|
||||
ORCA_CODEX_HOOK_CONTRACT_REQUIRED: '1'
|
||||
@@ -768,9 +727,7 @@ jobs:
|
||||
needs.code_paths.result == 'success' &&
|
||||
needs.code_paths.outputs.xterm_patch_sync == 'true' &&
|
||||
needs.preflight.result == 'success'
|
||||
# Why ARM: the patch check rebuilds 4 packages x 2 builds and byte-compares against the
|
||||
# checked-in bundles. Those were generated on Linux x64 and reproduce byte-for-byte on
|
||||
# darwin-arm64, so the output is neither host-arch nor host-OS dependent.
|
||||
# Use ARM for four-package pristine/patched rebuild byte comparison: output generated on Linux x64 reproduces on darwin-arm64.
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
|
||||
|
||||
steps:
|
||||
@@ -807,10 +764,7 @@ jobs:
|
||||
# Bound stalled prerequisite downloads instead of waiting for GitHub's 6-hour default.
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
# Why: the suites below gate their live fish tests on the binary, which is
|
||||
# right on a developer machine and wrong here — this job is a required check
|
||||
# and its fish lane is the only end-to-end guard for #9993, so a skip would
|
||||
# report green with nothing exercised. Turns those skips into failures.
|
||||
# Require live fish instead of developer-friendly skips: this is the only full #9993 guard.
|
||||
ORCA_REQUIRE_FISH: '1'
|
||||
# Why: the runner image ships pwsh, and the codex wrapper's PowerShell lane must not skip.
|
||||
ORCA_REQUIRE_PWSH: '1'
|
||||
@@ -867,9 +821,7 @@ jobs:
|
||||
|
||||
- wait: shells
|
||||
|
||||
# Separate from the install so the failure names the contract, not an apt error.
|
||||
# ORCA_REQUIRE_FISH re-checks this at test time; this step just fails in seconds
|
||||
# instead of after a full dependency install.
|
||||
# Fail fish prerequisites separately before dependency install; ORCA_REQUIRE_FISH checks again in tests.
|
||||
- name: Require fish 4+
|
||||
run: |
|
||||
version="$(fish --version 2>/dev/null || true)"
|
||||
@@ -926,17 +878,13 @@ jobs:
|
||||
run_relay_integration: ${{ needs.code_paths.outputs.relay_integration != 'false' || needs.code_paths.outputs.orcad_browser == 'true' }}
|
||||
browser_contract: ${{ needs.code_paths.outputs.orcad_browser == 'true' }}
|
||||
|
||||
# Why a sibling and not part of the test workflow: it is advisory, so it must not delay the
|
||||
# gate. Inside unit-tests.yml a caller's `needs: test` waited for it, holding verify ~36s
|
||||
# after the last shard. Deliberately absent from verify's needs for the same reason.
|
||||
# Keep advisory evidence sibling to tests and outside verify.needs; reusable-workflow dependency otherwise added ~36s after last shard.
|
||||
unit_selection_evidence:
|
||||
needs: [test]
|
||||
if: ${{ !cancelled() && (needs.test.result == 'success' || needs.test.result == 'failure') }}
|
||||
uses: ./.github/workflows/unit-selection-evidence.yml
|
||||
|
||||
# Why its own job: it needs mobile/node_modules and a real browser, and the sharded `test`
|
||||
# matrix would pay for both on every shard to run two files. It builds the same bundle the
|
||||
# package job ships, and adds the render and census checks packaging does not run.
|
||||
# Isolate two browser/mobile files to avoid every unit shard paying for their installs; test the same shipped bundle plus render/census.
|
||||
mobile_web_app:
|
||||
name: mobile web app bundle
|
||||
needs: [code_paths]
|
||||
@@ -958,9 +906,7 @@ jobs:
|
||||
pnpm-lock.yaml
|
||||
mobile/pnpm-lock.yaml
|
||||
|
||||
# The drawer check runs on WebKit as well as Chrome, because the shell's iOS WebView is
|
||||
# WebKit and the Chrome above cannot stand in for it. Downloaded rather than resolved from
|
||||
# the runner: Ubuntu ships no WebKit build to point at.
|
||||
# Test drawer with WebKit as iOS WebView uses it; runner Chrome cannot substitute.
|
||||
- name: Install WebKit for the drawer check
|
||||
id: webkit
|
||||
background: true
|
||||
@@ -976,10 +922,7 @@ jobs:
|
||||
background: true
|
||||
run: node config/scripts/run-mobile-web-app-checks.mjs --prepare-route-snapshot "$RUNNER_TEMP/mobile-routes.json"
|
||||
|
||||
# Why the runner's Google Chrome and not a downloaded chromium: same reason as the orcad
|
||||
# browser job -- Ubuntu 24.04 only ships an AppArmor userns profile for the Chrome .deb.
|
||||
# Why fail instead of skip: a silently skipped render check is the failure this job exists
|
||||
# to prevent.
|
||||
# Require runner Google Chrome: Ubuntu’s AppArmor userns profile covers its .deb, unlike downloaded Chromium; missing render checks must fail.
|
||||
- name: Resolve Chrome for the render check
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -997,16 +940,7 @@ jobs:
|
||||
# Browser checks must observe both a finished install and the built bundle.
|
||||
- wait: [webkit, mobile-routes]
|
||||
|
||||
# The bundling tests skip themselves where mobile dependencies are absent, which is how they
|
||||
# stay green in the sharded `test` job. This is the job that installs them, so here a missing
|
||||
# install has to fail rather than skip everything the job exists to run.
|
||||
# Why a prefix and not a file list: the list this replaces had gone stale twice without
|
||||
# anyone noticing, because a census whose closure block skips without the env flag below is
|
||||
# green in the sharded `test` job whether or not it ever runs here. The prefix is the same
|
||||
# one `pr-code-change-scope.mjs` fires this job on, so naming a test into the family is all
|
||||
# it takes to have it run. Quoted because these are vitest filename filters, matched as
|
||||
# substrings against the discovered files, and the shell must not touch them.
|
||||
# Route censuses share fresh dependency lists for this invocation; scratch builds stay independent.
|
||||
# Require mobile install and select census family by quoted filename prefix, matching routing; stale file lists/env-gated skips previously hid missing coverage.
|
||||
- name: Builder, override census and render checks
|
||||
env:
|
||||
ORCA_MOBILE_WEB_APP_DEPS_REQUIRED: '1'
|
||||
@@ -1026,15 +960,11 @@ jobs:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
|
||||
|
||||
steps:
|
||||
# Why fetch-depth 0: the harness extracts the newest release tag to skew
|
||||
# current code against it. The default shallow clone has no tags, which is
|
||||
# why this cannot ride along in the sharded `test` job.
|
||||
# Fetch tags/full history for newest-release skew; shallow unit shards cannot supply it.
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Why blob:none: full history is needed for the merge-base diff, but historical
|
||||
# file contents are not. Blobs are ~89% of this repo's pack, and Git fetches the
|
||||
# few this job actually reads on demand.
|
||||
# Use blob:none for merge-base history because blobs are ~89% of pack; fetch rare historical contents on demand.
|
||||
fetch-depth: 0
|
||||
filter: blob:none
|
||||
persist-credentials: false
|
||||
@@ -1057,8 +987,7 @@ jobs:
|
||||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||||
pnpm run check:runtime-launcher-protocol-ratchet -- --base "$DIFF_BASE"
|
||||
|
||||
# Runs the whole directory so a new file there cannot be left out; an empty match
|
||||
# exits 1 ("No test files found"), so this lane cannot pass while running zero tests.
|
||||
# Run the entire directory; zero matches fail so new contracts cannot be omitted silently.
|
||||
- name: Old/new client and server compatibility journeys
|
||||
run: pnpm exec vitest run --config config/vitest.config.ts tests/e2e/cross-version-wire/
|
||||
|
||||
@@ -1141,9 +1070,7 @@ jobs:
|
||||
ORCA_DAEMON_SHUTDOWN_FIXTURE_CACHE_IMAGE: ${{ steps.daemon-fixture-cache.outputs.image }}
|
||||
run: node config/scripts/run-daemon-shutdown-descendants-docker.mjs
|
||||
|
||||
# Why --no-file-parallelism: every file here launches a full Electron stack twice, and each
|
||||
# probe carries its own in-process deadline. Four at once on a 4-vCPU runner starve each other
|
||||
# past those deadlines; serial, every probe owns the runner.
|
||||
# Serialize dual-Electron lifecycle files: four concurrent probes on four vCPUs starve each other past their own deadlines.
|
||||
- name: Test Linux Electron lifecycle boundary
|
||||
env:
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
@@ -1243,9 +1170,7 @@ jobs:
|
||||
background: true
|
||||
run: pnpm run build:web-from-renderer
|
||||
|
||||
# Why here and not inside "Build package inputs": this job assembles packaging inputs step by
|
||||
# step instead of calling build:release, and electron-builder's beforePack guard hard-fails
|
||||
# without out/mobile-web. This is the real page, ~8 MB, not a bootstrap document.
|
||||
# Build the real ~8MB mobile page because stepwise package assembly bypasses build:release and beforePack refuses its absence.
|
||||
- name: Build mobile web bundle
|
||||
run: pnpm run build:mobile-web
|
||||
|
||||
@@ -1341,8 +1266,7 @@ jobs:
|
||||
restore-keys: |
|
||||
electron-builder-win-
|
||||
|
||||
# Why persist-native-cache false: this job later rebuilds the same path for
|
||||
# Electron. A post-job save would store the Electron ABI under the Node key.
|
||||
# Do not save Node cache post-job: later Electron rebuild mutates the same paths to another ABI.
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
id: deps
|
||||
with:
|
||||
@@ -1379,9 +1303,7 @@ jobs:
|
||||
- name: Cache Windows CLI launcher
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
# Why the cargo directories ride along: a hit on .build skips the build
|
||||
# entirely, but a miss otherwise recompiles the resource crate from a
|
||||
# cold registry.
|
||||
# Cache cargo registry with .build so misses do not rebuild resource crates cold.
|
||||
path: |
|
||||
native/windows-cli-launcher/.build
|
||||
native/windows-cli-launcher/target
|
||||
@@ -1390,9 +1312,7 @@ jobs:
|
||||
~/.cargo/registry
|
||||
key: windows-cli-launcher-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/windows-cli-launcher/**', 'native/windows-agent-hook/**', 'config/scripts/build-windows-cli-launcher.mjs', 'config/scripts/build-windows-agent-hook.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'resources/build/icon.ico', 'package.json') }}
|
||||
|
||||
# Why an explicit step rather than trusting the runner image: the packaged
|
||||
# CLI launcher is a native Rust binary, and a host without cargo fails deep
|
||||
# inside electron-builder's native hook instead of here.
|
||||
# Require cargo early; otherwise packaged Rust launcher fails deep inside builder native hooks.
|
||||
- name: Ensure the Rust toolchain for the Windows CLI launcher
|
||||
shell: pwsh
|
||||
run: |
|
||||
@@ -1402,8 +1322,7 @@ jobs:
|
||||
}
|
||||
cargo --version
|
||||
|
||||
# Why before the tests: the orca-agent-hook.exe suites then share one prebuilt binary rather
|
||||
# than each starting its own build mid-run.
|
||||
# Build agent-hook once before tests so suites share the binary instead of compiling concurrently.
|
||||
- name: Build the Windows agent hook
|
||||
run: node config/scripts/build-windows-agent-hook.mjs
|
||||
|
||||
@@ -1420,8 +1339,7 @@ jobs:
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts @files
|
||||
|
||||
# Why its own step: the CPU burners that make taskkill slow would starve
|
||||
# every other test file sharing the runner.
|
||||
# Isolate slow-taskkill CPU burners so they cannot starve unrelated test files.
|
||||
- name: Test Windows tree kill under CPU load
|
||||
env:
|
||||
ORCA_WINDOWS_TREE_KILL_LOAD: '1'
|
||||
@@ -1429,9 +1347,7 @@ jobs:
|
||||
pnpm exec vitest run --config config/vitest.config.ts
|
||||
src/shared/__tests__/process-host/windows-tree-kill-late-taskkill.win32.test.ts
|
||||
|
||||
# Why the :parallel variant: identical to build:release except the three
|
||||
# electron-vite targets overlap instead of running back to back. The Linux package
|
||||
# job already packages and smoke-tests an AppImage built that way.
|
||||
# Use parallel Vite targets as already packaged/smoked on Linux; release behavior is otherwise identical.
|
||||
- name: Build package inputs
|
||||
env:
|
||||
ORCA_CI_MAIN_SOURCEMAPS: '0'
|
||||
@@ -1482,10 +1398,7 @@ jobs:
|
||||
needs_build: ${{ needs.code_paths.outputs.e2e_needs_build != 'false' }}
|
||||
managed_ssh_shards: ${{ needs.code_paths.outputs.e2e_ssh_shards || '["1/2","2/2"]' }}
|
||||
|
||||
# Why its own required job, apart from `e2e`: the rest of the suite is red on main, but the
|
||||
# layout oracle is green there, and its known-on-main list absorbs today's findings. Only the two
|
||||
# oracle specs run, through the same reusable workflow: the window spec in the changed-spec lane,
|
||||
# the headless spec in the orcad lane on both serve hosts.
|
||||
# Gate the green layout oracles separately from main’s red broader E2E suite: changed-spec window plus both orcad serve hosts.
|
||||
layout_oracle:
|
||||
name: layout oracle
|
||||
needs: [code_paths, preflight]
|
||||
@@ -1505,12 +1418,7 @@ jobs:
|
||||
needs_build: true
|
||||
artifact_suffix: -layout-oracle
|
||||
|
||||
# Why this is not in verify's needs: it is the first PR-gate run of a harness whose reliability
|
||||
# is only known from nightly main runs (20/20 green, 2026-08-09..2026-08-29, p50 3m25s). It
|
||||
# reports a red X on the PR without blocking, exactly like `e2e` above. Deliberately no
|
||||
# continue-on-error: that renders the check green and hides the signal it exists to give. To
|
||||
# make it blocking, add it to verify.needs, add TERMINAL_IME_NATIVE to the env below, and
|
||||
# require `success || skipped` outside the strict loop — see the note on `e2e`.
|
||||
# Keep native IME advisory while PR reliability is established (nightly 20/20, 2026-08-09..29, p50 3m25s); do not hide red via continue-on-error. To gate, add verify.needs, TERMINAL_IME_NATIVE env and separate success-or-skipped validation.
|
||||
terminal_ime_native:
|
||||
name: real IME
|
||||
needs: code_paths
|
||||
@@ -1530,9 +1438,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
# Why: pnpm's global virtual store links each package to only its declared dependencies, which
|
||||
# is how developers install. Hoisting hides an undeclared one everywhere else, so install that way
|
||||
# here with HOME and the store in an empty temp dir, where no stray node_modules can mask it.
|
||||
# Install like developers’ global virtual store in empty HOME/store; hoisting or stray dependencies can mask undeclared imports.
|
||||
dependency_isolation:
|
||||
name: dependency isolation under the global virtual store
|
||||
needs: [code_paths]
|
||||
@@ -1546,8 +1452,7 @@ jobs:
|
||||
ORCA_PNPM_FORCE_GLOBAL_VIRTUAL_STORE_IN_CI: '1'
|
||||
|
||||
steps:
|
||||
# Job-wide (via GITHUB_ENV, since job env cannot read runner.temp) so every pnpm command,
|
||||
# including pnpm run's own install check, keeps this layout.
|
||||
# Persist layout via GITHUB_ENV (job env cannot read runner.temp) for every pnpm command/install check.
|
||||
- name: Point HOME and the pnpm store at empty temp dirs
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/empty-home"
|
||||
@@ -1613,14 +1518,7 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
# Why: e2e is deliberately absent from needs. The suite is currently red on
|
||||
# main (every scheduled run), so gating merges on it would block any PR that
|
||||
# touches tests/e2e/** — including the ones fixing the suite. Until it is
|
||||
# green the job runs and reports for E2E-path PRs without blocking. To flip
|
||||
# it on: add `e2e` to needs, add E2E to the env below, and require
|
||||
# `"$E2E" = success || skipped` after the loop — skipped is the normal
|
||||
# result for a path-filtered job and must keep passing, so it has to be
|
||||
# checked outside the loop or it would excuse the jobs above.
|
||||
# Keep E2E advisory while main’s scheduled suite is red, allowing fixes to merge. To gate, add e2e to needs/E2E env and check success-or-skipped outside the strict required-job loop.
|
||||
- name: Require successful checks
|
||||
env:
|
||||
CODE_PATHS: ${{ needs.code_paths.result }}
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
name: Cut Release
|
||||
|
||||
# Why: single entry point for manually cutting releases.
|
||||
# Replaces the old local `pnpm release:*` scripts and the standalone scheduled
|
||||
# RC workflow so releases are always reproducible from CI and can never be
|
||||
# accidentally tagged against an uncommitted or non-main working tree.
|
||||
#
|
||||
# Flow:
|
||||
# 1. Resolve `ref` to a SHA.
|
||||
# 2. Read the latest stable release from GitHub.
|
||||
# 3. Compute the next version from `kind` (rc | patch | minor | major).
|
||||
# 4. For stable kinds, REFUSE if the new version is <= the latest stable.
|
||||
# This is the only guard electron-updater actually needs — it compares
|
||||
# semver within a channel, so a regressing "latest" is the one thing
|
||||
# that breaks auto-update for fresh installs.
|
||||
# 5. Write package.json, commit (detached), tag, push tag.
|
||||
# 6. If ref was the tip of origin/main, try to fast-forward its version;
|
||||
# a pull-request-only rule leaves main unchanged and still pushes the tag.
|
||||
# 7. Build and publish artifacts from the tag.
|
||||
# Resolve ref/latest stable, compute next kind, reject stable version regression, commit/tag detached and build/publish from tag. Fast-forward main’s version only when source was its tip and policy permits; PR-only rules leave main unchanged. CI replaces local/scheduled release paths to avoid accidental working-tree releases.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -61,10 +45,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
cut:
|
||||
# Why: this job bumps package.json and fast-forwards main. On a fork with
|
||||
# Actions enabled, the scheduled cut would run against the fork's main and
|
||||
# diverge it (version line) every slot, conflicting every PR back upstream.
|
||||
# Gate to the canonical repo so the workflow no-ops on forks.
|
||||
# Limit version/tag mutations to canonical repo; fork schedules must not diverge their main and conflict upstream.
|
||||
if: github.repository == 'stablyai/orca'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
@@ -81,13 +62,7 @@ jobs:
|
||||
source_short_sha: ${{ steps.resolve.outputs.short_sha }}
|
||||
ships_orcad_template: ${{ steps.orcad-template-support.outputs.ships }}
|
||||
steps:
|
||||
# Why inlined (not m-s-abeer/update-gha-summary-with-workflow-inputs):
|
||||
# this job runs with contents:write and secret scope, so avoid executing
|
||||
# any external (mutable @v1) action here. Surfaces every
|
||||
# workflow_dispatch input as a table for audit; the resolved commit /
|
||||
# branch / tag enrichment is written later in "Resolve ref SHA".
|
||||
# Inputs are passed as JSON via env and parsed by jq as data — never
|
||||
# interpolated into the shell — to avoid injection from dispatch values.
|
||||
# Render dispatch audit summary locally rather than run mutable external action beside write token/secrets; parse env JSON with jq as data.
|
||||
- name: Summarize workflow inputs
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
@@ -98,8 +73,7 @@ jobs:
|
||||
echo ""
|
||||
echo "| Input | Value |"
|
||||
echo "| --- | --- |"
|
||||
# Values are data from env JSON; wrap in backticks for readability.
|
||||
# Newlines collapsed so a multi-line input cannot break the table.
|
||||
# Treat env JSON as data; collapse newlines so inputs cannot break the summary table.
|
||||
jq -r '(. // {}) | to_entries[] | "| `\(.key)` | `\(.value | tostring | gsub("\n"; " "))` |"' <<<"$INPUTS_JSON"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
@@ -110,9 +84,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
# Version recovery needs complete history and tags, but not historical file contents.
|
||||
filter: blob:none
|
||||
# Why: version math recovers unpublished tags; checkout's default
|
||||
# fetch-tags:false hides them, so a patch cut recreates vX.Y.Z and
|
||||
# `git push` overwrites the existing tag.
|
||||
# Fetch unpublished tags for version recovery; hidden tags can recreate/overwrite an existing version.
|
||||
fetch-tags: true
|
||||
|
||||
# Why: a relay-era ref (before #27215) needs the Windows relay addon this build no longer compiles.
|
||||
@@ -138,9 +110,7 @@ jobs:
|
||||
- name: Resolve ref SHA
|
||||
id: resolve
|
||||
env:
|
||||
# Why: keep the caller's ref as data (env) so we can label it in the
|
||||
# summary without shell-interpolating a dispatch-controlled string
|
||||
# into the script body.
|
||||
# Keep caller ref in env data so summary labels cannot inject shell.
|
||||
INPUT_REF: ${{ github.event_name == 'schedule' && 'main' || inputs.ref }}
|
||||
REPO: ${{ github.repository }}
|
||||
SERVER_URL: ${{ github.server_url }}
|
||||
@@ -153,9 +123,7 @@ jobs:
|
||||
echo "sha=$sha" >>"$GITHUB_OUTPUT"
|
||||
echo "short_sha=$short_sha" >>"$GITHUB_OUTPUT"
|
||||
|
||||
# Why: only push the version-bump commit back to main when the
|
||||
# caller is releasing the exact tip of main. For any older or
|
||||
# off-main ref we leave main alone and only publish the tag.
|
||||
# Push the version bump to main only when cutting its exact tip; older/off-main refs publish tags only.
|
||||
git fetch origin main --quiet
|
||||
main_sha="$(git rev-parse origin/main)"
|
||||
if [[ "$sha" == "$main_sha" ]]; then
|
||||
@@ -164,11 +132,7 @@ jobs:
|
||||
echo "push_main=false" >>"$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Always surface the resolved commit in the job summary, plus any
|
||||
# branches/tags that currently point at it (clickable). The raw
|
||||
# `ref` input alone is ambiguous (branch vs tag vs SHA); for SHA
|
||||
# inputs it also hides the human-readable names operators need
|
||||
# when auditing RC cuts.
|
||||
# Show the immutable commit and matching ref links: raw ref input is ambiguous and bare SHAs hide operator-readable names.
|
||||
repo_url="${SERVER_URL}/${REPO}"
|
||||
|
||||
branches="$(
|
||||
@@ -184,8 +148,7 @@ jobs:
|
||||
|| true
|
||||
)"
|
||||
|
||||
# Build comma-separated markdown links. Branch/tag names go in the
|
||||
# URL path as-is (slashes must stay literal for GitHub tree URLs).
|
||||
# Preserve literal slashes in branch/tag tree URLs.
|
||||
linkify_names() {
|
||||
local url_kind="$1"
|
||||
local names="$2"
|
||||
@@ -215,8 +178,7 @@ jobs:
|
||||
branch_md="$(linkify_names branch "$branches")"
|
||||
tag_md="$(linkify_names tag "$tags")"
|
||||
|
||||
# When no branch tip matches (historical SHA cuts), fall back to
|
||||
# name-rev so the summary still shows something like `main~3`.
|
||||
# Historical SHAs may lack a matching branch tip; name-rev supplies an auditable main~N label.
|
||||
contains_md="_none_"
|
||||
if [[ "$branch_md" == "_none_" ]]; then
|
||||
approx="$(git name-rev --name-only --no-undefined --refs='refs/remotes/origin/*' "$sha" 2>/dev/null || true)"
|
||||
@@ -274,8 +236,7 @@ jobs:
|
||||
pt_hour=$(TZ=America/Los_Angeles date '+%H')
|
||||
pt_minute=$(TZ=America/Los_Angeles date '+%M')
|
||||
|
||||
# Why: GitHub may deliver a scheduled event long after the intended
|
||||
# time, so delayed 4:16 AM runs must not cut the 3:00 AM release.
|
||||
# Delayed schedule delivery (e.g. 4:16 AM) must not cut the 3:00 AM slot.
|
||||
if [[ "$pt_hour" == "03" || "$pt_hour" == "15" ]]; then
|
||||
echo "allowed=true" >>"$GITHUB_OUTPUT"
|
||||
echo "reason=target_hour:${pt_hour}:${pt_minute}" >>"$GITHUB_OUTPUT"
|
||||
@@ -289,15 +250,13 @@ jobs:
|
||||
id: existing
|
||||
if: github.event_name == 'schedule' && steps.window.outputs.allowed == 'true'
|
||||
run: |
|
||||
# Why: scheduled runs retry inside each target hour, so make the
|
||||
# schedule idempotent by embedding a slot marker in the release commit.
|
||||
# Repeated target-hour schedules deduplicate via a slot marker in the release commit.
|
||||
if git log origin/main --grep="\\[rc-slot:${{ steps.slot.outputs.value }}\\]" -n 1 --format=%H | grep -q .; then
|
||||
echo "already_ran=true" >>"$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Why: this preserves dedupe across the older scheduled workflow's
|
||||
# first runs, before all RC cuts shared release-cut's slot marker.
|
||||
# Preserve dedupe for older scheduled workflows that predate the shared slot marker.
|
||||
latest_rc_tag="$(git for-each-ref --sort=-creatordate --format='%(refname:short) %(creatordate:iso-strict)' 'refs/tags/v*-rc.*' | head -n 1)"
|
||||
if [[ -n "$latest_rc_tag" ]]; then
|
||||
latest_rc_tag_name="${latest_rc_tag%% *}"
|
||||
@@ -334,8 +293,7 @@ jobs:
|
||||
|
||||
- name: Publish complete release-cut RC drafts from prior runs
|
||||
id: publish_drafts
|
||||
# Why: a manual RC dispatch should unstick any complete RC draft before
|
||||
# deciding whether to cut another tag.
|
||||
# Recover complete RC drafts before deciding whether manual dispatch needs another tag.
|
||||
if: steps.window.outputs.allowed == 'true' && !(github.event_name == 'workflow_dispatch' && inputs.dry_run)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -343,9 +301,7 @@ jobs:
|
||||
|
||||
- name: Compute next version
|
||||
id: version
|
||||
# Why: if an RC run only had complete drafts to publish, stop there
|
||||
# instead of immediately cutting another RC after the recovered one.
|
||||
# Stable dispatches should still cut the requested stable release.
|
||||
# Stop after RC-only draft recovery; stable dispatch still cuts its requested version.
|
||||
if: steps.window.outputs.allowed == 'true' && steps.existing.outputs.already_ran != 'true' && !(github.event_name == 'workflow_dispatch' && inputs.dry_run) && !((github.event_name == 'schedule' || inputs.kind == 'rc') && steps.publish_drafts.outputs.published_count != '0' && steps.publish_drafts.outputs.skipped_count == '0')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -355,35 +311,16 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Latest stable release tag, picked by *tag shape* and semver max:
|
||||
# - must start with `v<digit>` (desktop convention, e.g. v1.3.32)
|
||||
# - must NOT contain `-rc.` (not a prerelease)
|
||||
#
|
||||
# Why not the GitHub `isPrerelease` flag: electron-builder's publish
|
||||
# step has flipped that flag back to `false` on RC releases before
|
||||
# (v1.3.22-rc.2 on 2026-04-27 briefly became "latest" on GitHub and
|
||||
# poisoned the math here). Tag format is authoritative.
|
||||
#
|
||||
# Why the `^v[0-9]` prefix: other products shipped from this repo
|
||||
# use their own prefixes (e.g. `mobile-v0.0.1`). Without the prefix
|
||||
# gate the latest mobile release would be selected as "latest
|
||||
# stable", then `strip_pre()` would reduce `mobile-v0.0.1` to
|
||||
# `mobile`, `Number("mobile")` → NaN → 0, and a patch-bump would
|
||||
# produce `0.0.1` — exactly the wedge on 2026-05-04 (run
|
||||
# 25304336767). Any non-desktop tag shape must be excluded here.
|
||||
#
|
||||
# Why not `gh release list` order: GitHub can list a newer published
|
||||
# stable after older releases. On 2026-06-04, v1.4.44 existed but
|
||||
# the list returned v1.4.42 first, causing a manual RC cut to reopen
|
||||
# the already-shipped 1.4.43 series as v1.4.43-rc.0.
|
||||
# Use desktop v<digit> tag shape and numeric semver max, excluding RC/mobile tags.
|
||||
# Builder flipped v1.3.22-rc.2’s prerelease flag on 2026-04-27; that flag cannot authorize stable math.
|
||||
# 2026-05-04/run25304336767: mobile-v0.0.1 collapsed through NaN to 0.0.1 without the desktop prefix.
|
||||
# 2026-06-04: release-list order put v1.4.42 before v1.4.44 and reopened 1.4.43 as rc.0.
|
||||
latest_stable="$(node config/scripts/latest-stable-release.mjs)"
|
||||
latest_stable="${latest_stable#v}"
|
||||
echo "Latest stable: ${latest_stable:-<none>}"
|
||||
|
||||
# Strip any prerelease suffix before numeric math. Without this,
|
||||
# `Number("1-rc")` returns NaN and `(NaN||0)+1` silently collapses
|
||||
# to 1 — exactly the path that produced v1.3.1-rc.4 on 2026-04-27
|
||||
# when latest_stable was misread as a prerelease tag.
|
||||
# Strip prerelease suffixes before numeric math: Number("1-rc") becomes NaN and silently collapsed
|
||||
# to 1, producing v1.3.1-rc.4 on 2026-04-27 when latest_stable was misclassified.
|
||||
strip_pre() { echo "${1%%-*}"; }
|
||||
|
||||
semver_gt() {
|
||||
@@ -415,16 +352,8 @@ jobs:
|
||||
}
|
||||
|
||||
require_valid_version_suffix() {
|
||||
# Why a dot-appended identifier (rc.N.perf): it sorts just
|
||||
# above its own base rc.N but BELOW rc.N+1, so suffixed side-
|
||||
# branch builds never outrank the main RC series and cannot
|
||||
# hijack the update channel; clients find them by matching the
|
||||
# identifier ("perf") in the prerelease components.
|
||||
# Why the numeric alternation rather than plain [0-9A-Za-z]+:
|
||||
# semver forbids a leading zero on an all-digit identifier, and
|
||||
# `npm version` silently renormalizes rc.4.01 to rc.4.1 while the
|
||||
# tag step keeps the literal input — so the shipped package.json
|
||||
# version and its own release tag would name different releases.
|
||||
# Append rc.N.<identifier> so side builds sort above rc.N but below rc.N+1; clients match the suffix.
|
||||
# Reject leading-zero numeric identifiers: npm normalizes rc.4.01 to rc.4.1 while tagging keeps the input.
|
||||
if [[ ! "$1" =~ ^(0|[1-9][0-9]*|[0-9A-Za-z]*[A-Za-z][0-9A-Za-z]*)$ ]]; then
|
||||
echo "::error::version_suffix (or the trailing .identifier in version) must be alphanumeric with no leading zero on an all-digit identifier, got: $1" >&2
|
||||
exit 1
|
||||
@@ -502,18 +431,10 @@ jobs:
|
||||
|
||||
package_stable="$(current_package_stable)"
|
||||
if [[ -n "$package_stable" ]]; then
|
||||
# Why: if a stable release is deleted after its version-bump commit
|
||||
# reached main, GitHub's release list regresses. package.json is the
|
||||
# floor for the current ref so the next cut cannot reuse an older
|
||||
# stable number just because the public release was nuked.
|
||||
# Deleted releases can regress GitHub’s version list after their bump reached main; package.json floors the current ref.
|
||||
if semver_gt "$package_stable" "$latest_stable"; then
|
||||
# Skip floor-tag recovery when an explicit version is requested:
|
||||
# recover_unpublished_tag can exit 0, which would recover the
|
||||
# package-floor tag instead of cutting the requested version —
|
||||
# defeating the very rollback scenario the override exists for.
|
||||
# We still raise latest_stable to the floor below so the explicit
|
||||
# version is gated against it; the collision recovery for the
|
||||
# requested tag runs later.
|
||||
# Explicit versions must skip floor-tag recovery (which can exit successfully with the wrong tag).
|
||||
# Still enforce the package floor; recover collisions for the requested tag later.
|
||||
if [[ "$KIND" != "rc" && -z "${EXPLICIT_VERSION:-}" ]]; then
|
||||
package_tag="v$package_stable"
|
||||
if git rev-parse "$package_tag" >/dev/null 2>&1; then
|
||||
@@ -526,84 +447,44 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
|
||||
# Explicit version override (manual dispatch only).
|
||||
#
|
||||
# Why: kind-based math derives the next number from the latest
|
||||
# *published* stable. When a shipped stable is deleted (e.g. a
|
||||
# rolled-back 1.4.154), the release list regresses to the prior
|
||||
# stable, so a kind cut recomputes a number at or below the nuked one
|
||||
# and strands every client that already installed the deleted build.
|
||||
# The package.json floor above only recovers this when the deleted
|
||||
# version's bump commit is on the ref being cut, which a hotfix cut
|
||||
# from an older RC ref does not carry. An explicit version lets a
|
||||
# human assert the exact target (e.g. leapfrog to 1.4.155); the
|
||||
# updater-safety gate and tag-collision recovery below still apply.
|
||||
# Manual explicit versions let hotfixes from older refs leapfrog deleted shipped versions (e.g. 1.4.154 → 1.4.155)
|
||||
# when published-release math regresses and the ref lacks the newer package floor. Keep updater/tag-collision gates.
|
||||
new=""
|
||||
if [[ -n "${EXPLICIT_VERSION:-}" ]]; then
|
||||
explicit="${EXPLICIT_VERSION#v}"
|
||||
# Why the optional trailing identifier: it lets an operator name a
|
||||
# suffixed side-branch RC (X.Y.Z-rc.N.perf) directly, the same shape
|
||||
# the rc path cuts. Note this only ever admits one *above* the
|
||||
# series head — the gate below refuses a suffixed rc at or below it
|
||||
# just like a bare one, so this is a second spelling of
|
||||
# `version=X.Y.Z-rc.N` + `version_suffix`, not a way back into a
|
||||
# series that already shipped.
|
||||
# Why rc.(0|[1-9][0-9]{0,8}): the `-le` below compares with bash's
|
||||
# machine-width integers, so both ends of that range fall *open* on
|
||||
# exactly the RCs this gate must catch. A leading zero (rc.08) is an
|
||||
# invalid octal literal, and the failed test makes the `if` false.
|
||||
# Past INTMAX the literal wraps two's-complement, so whether it
|
||||
# reads as above or below the published rc depends on the value:
|
||||
# rc.99999999999999999999 wraps to 7766279631452241919 and sails
|
||||
# through. The cut then lands a tag that pins highest_rc_for_base
|
||||
# at 1e20 forever, and every later cut wraps to a *lower* rc that
|
||||
# sorts below it, so the fleet never updates again. Nine digits is
|
||||
# far above any real series and exact in bash math either way.
|
||||
# Accept one optional side-branch RC identifier, still strictly above the series head; it grants no rollback exception.
|
||||
# Bound rc ordinals to nine digits without leading zeros: Bash -le misreads rc.08 as octal and wraps INTMAX,
|
||||
# letting rc.99999999999999999999 pass as 7766279631452241919 and pin highest_rc_for_base at 1e20 forever.
|
||||
if [[ ! "$explicit" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.(0|[1-9][0-9]{0,8})(\.[0-9A-Za-z]+)?)?$ ]]; then
|
||||
echo "::error::version must be X.Y.Z, X.Y.Z-rc.N, or X.Y.Z-rc.N.suffix, got: $EXPLICIT_VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Why route the embedded identifier through the same validator the
|
||||
# kind path uses: the regex above only checks shape, and rc.4.01
|
||||
# is a shape-valid identifier that is not valid semver.
|
||||
# Reuse identifier validation: rc.4.01 matches shape but is invalid semver.
|
||||
if [[ "$explicit" == *-rc.*.* ]]; then
|
||||
require_valid_version_suffix "${explicit##*.}"
|
||||
fi
|
||||
# Same updater-safety gate the kind path enforces: stable line must
|
||||
# strictly increase over the latest published stable (prerelease
|
||||
# identifiers ignored for the comparison).
|
||||
# Explicit stable lines must strictly exceed published stable, ignoring prerelease identifiers.
|
||||
if ! semver_gt "$explicit" "$latest_stable"; then
|
||||
echo "::error::Refusing explicit version $explicit: not greater than latest stable $latest_stable." >&2
|
||||
exit 1
|
||||
fi
|
||||
# Why a second gate for prereleases: semver_gt compares through
|
||||
# strip_pre(), so the stable-line check reads 1.4.156-rc.0 as
|
||||
# 1.4.156 and waves it past a 1.4.155 stable even when rc.0..rc.3
|
||||
# already shipped — republishing an RC *below* what clients run,
|
||||
# the same regression class as the rc.4 cut that orphaned live
|
||||
# daemons. Anchor on the same rc history the kind path uses so the
|
||||
# override can only ever advance the series it targets.
|
||||
# Stable-line comparison alone admits rc.0 after rc.3 shipped. Require the override to advance its
|
||||
# own series too, avoiding the rc.4 regression that orphaned live daemons.
|
||||
if [[ "$explicit" == *-rc.* ]]; then
|
||||
explicit_base="${explicit%%-*}"
|
||||
explicit_rc="${explicit#*-rc.}"
|
||||
explicit_rc="${explicit_rc%%.*}"
|
||||
highest_explicit_rc="$(highest_rc_for_base "$explicit_base")"
|
||||
if [[ -n "$highest_explicit_rc" && "$explicit_rc" -le "$highest_explicit_rc" ]]; then
|
||||
# Why the remedy is spelled this narrowly: kind=rc derives its
|
||||
# base from bump(latest_stable, patch), so it can only resume a
|
||||
# series on that base. A minor/major series (1.5.0-rc.N) exists
|
||||
# only because this override created it, and pointing an
|
||||
# operator at kind=rc there would cut an unrelated release.
|
||||
# kind=rc resumes only latest stable’s next patch; minor/major RC series require explicit overrides,
|
||||
# or the suggested remedy would cut an unrelated release.
|
||||
echo "::error::Refusing explicit version $explicit: rc.$explicit_rc is not above rc.$highest_explicit_rc, the highest already cut for $explicit_base. Request rc.$((highest_explicit_rc + 1)) or higher. If you are resuming an unpublished tag and $explicit_base is the next patch after latest stable $latest_stable, dispatch kind=rc instead, which recovers that tag when it was cut from the ref you dispatch; otherwise cut rc.$((highest_explicit_rc + 1)) and leave the unpublished tag alone." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
new="$explicit"
|
||||
# Why here too: the suffix append below lives in the kind path the
|
||||
# override skips, so an operator passing both inputs used to get
|
||||
# their suffix silently dropped. Only a bare rc can take one — a
|
||||
# stable X.Y.Z.perf is not valid semver, and re-suffixing an
|
||||
# already-suffixed rc would produce rc.N.perf.perf.
|
||||
# The override bypasses kind-path suffixing: append here too, only to bare RCs, avoiding invalid
|
||||
# stable X.Y.Z.perf or repeated rc.N.perf.perf suffixes.
|
||||
if [[ -n "${VERSION_SUFFIX:-}" ]]; then
|
||||
# Same bounded rc pattern as the shape check above, so the two
|
||||
# cannot drift apart under a later edit.
|
||||
@@ -620,23 +501,15 @@ jobs:
|
||||
if [[ -z "$new" ]]; then
|
||||
case "$KIND" in
|
||||
rc)
|
||||
# Why: RCs always stabilize the *next* patch after whatever
|
||||
# is currently published as stable. Earlier logic tried to
|
||||
# "continue the current series" by reading the highest git
|
||||
# tag, which silently reopened a series that had already
|
||||
# shipped (e.g. cutting v1.3.21-rc.7 after v1.3.21 stable
|
||||
# was out). Anchoring to latest_stable + patch eliminates
|
||||
# that class of bug; minor/major RCs are cut by running
|
||||
# that stable kind first.
|
||||
# RCs target published stable + next patch. Highest-tag continuation reopened shipped series
|
||||
# (v1.3.21-rc.7 after v1.3.21 stable); cut minor/major stable first to start those RC bases.
|
||||
base="$(bump "$latest_stable" patch)"
|
||||
highest_rc="$(highest_rc_for_base "$base")"
|
||||
if [[ -z "$highest_rc" ]]; then
|
||||
new="${base}-rc.0"
|
||||
else
|
||||
existing_rc_tag="v${base}-rc.${highest_rc}"
|
||||
# Why: a failed or GitHub-stuck run can leave the highest RC
|
||||
# tag attached to a draft/missing release. Resume only when it
|
||||
# was cut from this ref; stale attempts advance to rc.N+1.
|
||||
# Resume highest draft/missing RC only when cut from this ref; stale attempts advance to rc.N+1.
|
||||
if git rev-parse "$existing_rc_tag" >/dev/null 2>&1; then
|
||||
recover_unpublished_tag "$existing_rc_tag" "latest RC in series" || true
|
||||
fi
|
||||
@@ -655,10 +528,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Why: a stale orphan stable tag can exist from an older release
|
||||
# ref after main has moved on. If it cannot be recovered for the
|
||||
# current ref, advance to the next stable version instead of
|
||||
# wedging every future patch cut on the same collision.
|
||||
# If an old orphan stable tag cannot be recovered for this ref, advance rather than wedge future patch cuts.
|
||||
for _ in {1..100}; do
|
||||
candidate_tag="v$new"
|
||||
if ! git rev-parse "$candidate_tag" >/dev/null 2>&1; then
|
||||
@@ -689,29 +559,9 @@ jobs:
|
||||
esac
|
||||
fi
|
||||
|
||||
# Orphan-tag recovery.
|
||||
#
|
||||
# Why: if a previous cut pushed the tag but was cancelled (or the
|
||||
# dependent release build jobs otherwise failed to start) before the
|
||||
# GitHub Release was published, the tag now exists on the remote
|
||||
# but "latest stable" still points at the prior version. Every
|
||||
# subsequent patch cut then recomputes the same version and dies
|
||||
# on "Tag already exists." This exact sequence wedged the cut
|
||||
# pipeline on 2026-05-01 when v1.3.26 was pushed by a cancelled
|
||||
# run (25237882049) — every patch cut after that rehit the same
|
||||
# tag for hours until the orphan release was dispatched by hand.
|
||||
#
|
||||
# Recovery policy: if the tag exists AND no GitHub release has
|
||||
# been published for it (draft-or-absent both count as "not
|
||||
# shipped"), treat this as a resumable state: emit the existing
|
||||
# tag as the job output so the downstream release build jobs run
|
||||
# against it and finishes what the earlier attempt started. The
|
||||
# bump/commit/push steps are skipped in that case — there is
|
||||
# nothing to bump; the tag is already on the remote.
|
||||
#
|
||||
# Refuse collisions only when the tag *and* a published release
|
||||
# already exist — that's a real conflict (someone tagged manually
|
||||
# over a shipped version) and needs human attention.
|
||||
# Recover existing draft/absent releases by emitting the pushed tag and rerunning builds without bump/commit/push.
|
||||
# 2026-05-01/run25237882049: cancellation after pushing v1.3.26 wedged every patch cut on that orphan tag
|
||||
# until manual release dispatch. A tag plus published release is a real collision requiring operator attention.
|
||||
if git rev-parse "v$new" >/dev/null 2>&1; then
|
||||
recover_unpublished_tag "v$new" "tag collision" || {
|
||||
echo "::error::Tag v$new already exists and cannot be recovered for this ref. Refusing to re-cut over an existing version." >&2
|
||||
@@ -731,12 +581,9 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Why: use npm version --no-git-tag-version so we control the commit
|
||||
# message and tag name explicitly (avoids npm's `v1.2.3` prefix
|
||||
# assumptions and any lifecycle scripts that would run on bump).
|
||||
# Control commit message/tag ourselves via npm version --no-git-tag-version, avoiding npm’s prefix/lifecycle assumptions.
|
||||
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||
# Why: freshness compares installs against the bundled manifest, so a tag must
|
||||
# never ship skill bytes it doesn't describe. Node built-ins only; no install.
|
||||
# Tags must describe their bundled skill bytes; verify freshness with Node built-ins before install.
|
||||
if ! node config/scripts/generate-skill-bundle-manifest.mjs; then
|
||||
echo "::error::Refusing to tag v$VERSION: the committed skill manifest does not match skills/. Land a regeneration on main, then re-run the cut." >&2
|
||||
exit 1
|
||||
@@ -747,23 +594,14 @@ jobs:
|
||||
commit_message="$commit_message [rc-slot:$SLOT]"
|
||||
fi
|
||||
if git diff --cached --quiet; then
|
||||
# Why: a failed cut can push the version bump to main before the
|
||||
# release is published. Re-cutting then needs a fresh taggable
|
||||
# release commit even though package.json is already at VERSION.
|
||||
# A failed cut may already have bumped main; re-cut still needs a fresh taggable commit.
|
||||
git commit --allow-empty -m "$commit_message"
|
||||
else
|
||||
git commit -m "$commit_message"
|
||||
fi
|
||||
# Why: a lint that greps this file cannot see a path built from an env
|
||||
# var, a composite action, or concatenation, and `git commit` has forms
|
||||
# (-a, -i, --only, a pathspec) that commit the working tree rather than
|
||||
# the index. Assert what the commit actually carries, so the tag can
|
||||
# only ever ship the version bump, no matter which step staged what or
|
||||
# how the commit was spelled.
|
||||
# -F because the allowlist is literal: unanchored, `.` would match any
|
||||
# character and quietly admit a path like `packageXjson`.
|
||||
# -m --first-parent: plain diff-tree prints NOTHING for a merge commit,
|
||||
# which would make this guard pass silently rather than fail closed.
|
||||
# Validate the committed tree, not staged intent: git commit -a/-i/--only/pathspec can bypass the index.
|
||||
# Literal -F matching cannot accidentally admit packageXjson; -m --first-parent makes merge commits visible
|
||||
# to diff-tree so the allowlist cannot silently pass an empty merge diff.
|
||||
committed="$(git diff-tree --no-commit-id --name-only -r -m --first-parent HEAD |
|
||||
grep -vxF -e 'package.json' || true)"
|
||||
if [[ -n "$committed" ]]; then
|
||||
@@ -924,10 +762,7 @@ jobs:
|
||||
npm install -g node-gyp@11.5.0
|
||||
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
|
||||
|
||||
# Why: this install runs lifecycle scripts, so node-gyp rebuilds
|
||||
# native/windows-registry and fetches that Node version's headers from
|
||||
# nodejs.org. One `read ECONNRESET` there failed this blocking gate and the
|
||||
# whole cut. Retry like the release build's install below.
|
||||
# Retry node-gyp header fetch ECONNRESET so transient install failures cannot fail the whole release.
|
||||
- name: Install dependencies
|
||||
uses: nick-fields/retry@v4
|
||||
with:
|
||||
@@ -939,10 +774,7 @@ jobs:
|
||||
- name: Build Electron app for platform golden
|
||||
run: npx electron-vite build --mode e2e
|
||||
|
||||
# Why: this job is defined on the dispatch ref (usually main) but checks
|
||||
# out the release tag. Cherry-pick / hotfix tags can predate a golden
|
||||
# script that main already calls; --if-present keeps those cuts green
|
||||
# instead of failing with ERR_PNPM_NO_SCRIPT.
|
||||
# Use --if-present for golden scripts absent from older hotfix/cut tags, despite newer dispatch YAML.
|
||||
- name: Run terminal rendering golden on Linux
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
@@ -1186,9 +1018,7 @@ jobs:
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Design D2: every desktop build ships the orcad template (server JS plus every target's
|
||||
# addons), merged from the node-server lanes that qualified each slot at this tag. It needs no
|
||||
# signing quota, so it runs beside the release gates instead of behind them.
|
||||
# Build every target’s qualified orcad slot at the tag beside release gates; unsigned template consumes no signing quota.
|
||||
orcad-template:
|
||||
needs: cut
|
||||
if: needs.cut.outputs.should_release == 'true' && needs.cut.outputs.ships_orcad_template == 'true'
|
||||
@@ -1200,9 +1030,7 @@ jobs:
|
||||
ref: refs/tags/${{ needs.cut.outputs.tag }}
|
||||
fast_runners: false
|
||||
|
||||
# Why: artifact jobs submit Windows binaries to SignPath. Keep every
|
||||
# quota-consuming build behind all blocking release gates so a late test
|
||||
# failure cannot create signing requests that can never be published.
|
||||
# Gate SignPath artifact requests behind blocking tests to avoid approvals for unpublishable builds.
|
||||
release-preflight:
|
||||
needs:
|
||||
- cut
|
||||
@@ -1285,9 +1113,7 @@ jobs:
|
||||
~/.cache/electron-builder
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
# Why: hosted runners hard-cap jobs at 6h; the Windows SignPath waits
|
||||
# (1h inner + 4h installer) are budgeted to fit under this with the
|
||||
# build itself, so a slow approval can't kill the job mid-flow.
|
||||
# Fit 1h inner+4h installer SignPath waits and build under hosted runner’s 6h cap.
|
||||
timeout-minutes: 360
|
||||
|
||||
permissions:
|
||||
@@ -1310,16 +1136,9 @@ jobs:
|
||||
echo "Windows artifact/signing steps are disabled on reruns (attempt $GITHUB_RUN_ATTEMPT)."
|
||||
echo "Existing signed release assets must be reused; dispatch a fresh release only when a rebuild is required." >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# Why: `uses: ./…` resolves from the checked-out tag, not from the workflow
|
||||
# ref, so cutting from an older/off-main ref whose tree predates a composite
|
||||
# action would fail the step with "Can't find 'action.yml'". Restore the
|
||||
# actions directory from the commit this workflow file itself came from.
|
||||
# Not Windows-only: every platform now consumes install-mobile-dependencies, so
|
||||
# any of them can be the one whose cut ref predates the action.
|
||||
# Restore composite actions from workflow commit: uses:./ resolves checked-out tag, which may predate mobile/dependency actions.
|
||||
- name: Restore draft-publish scripts from the workflow ref
|
||||
# Why: this job checks out the release tag, so a cut from an older SHA
|
||||
# still has electron-builder releaseType:release and no re-draft helper.
|
||||
# The workflow YAML is from main; restore the scripts it invokes.
|
||||
# Restore current workflow helper scripts when old cut tags still publish live and lack re-draft support.
|
||||
shell: bash
|
||||
env:
|
||||
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
||||
@@ -1370,23 +1189,14 @@ jobs:
|
||||
pnpm-lock.yaml
|
||||
mobile/pnpm-lock.yaml
|
||||
|
||||
# Why: release builds hit the same native-module postinstall path as
|
||||
# PR CI, so keep the pinned node-gyp override here too instead of
|
||||
# relying on pnpm's bundled copy. Scoped to Linux via runner.os (not
|
||||
# a specific matrix image) because the failing postinstall has only
|
||||
# been observed on Linux runners — see run 25081763129. The macOS
|
||||
# and Windows release jobs exercise the same pnpm install path and
|
||||
# have not reproduced it, so keep the gate narrow until we know why.
|
||||
# Using runner.os instead of matrix.os == 'ubuntu-latest' means the
|
||||
# gate still works if another Linux matrix entry is added later.
|
||||
# Override non-executable pnpm node-gyp only on Linux (run 25081763129); runner.os keeps future Linux entries covered.
|
||||
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
npm install -g node-gyp@11.5.0
|
||||
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
|
||||
|
||||
# Cache the Electron binary + electron-builder tool downloads
|
||||
# (winCodeSign, nsis, squirrel, AppImage). Saves ~30-90s per job.
|
||||
# Cache Electron and builder downloads (winCodeSign/NSIS/Squirrel/AppImage), saving ~30–90s per job.
|
||||
- name: Cache electron-builder downloads
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
@@ -1405,12 +1215,7 @@ jobs:
|
||||
# The release cache above restores the same tools, so this only needs to save on a miss.
|
||||
lookup-only: true
|
||||
|
||||
# Why: pnpm install triggers electron's postinstall, which downloads the
|
||||
# Electron binary from GitHub release assets. GitHub's download CDN
|
||||
# occasionally returns 504s that fail the whole release. Retry on
|
||||
# failure so transient network errors don't require a manual re-run.
|
||||
# Why host-only: this job packages only for its own runner OS and
|
||||
# architecture, so the default host-scoped install is deliberate.
|
||||
# Retry transient Electron CDN 504s during install; keep host-scoped native dependencies for this platform’s package.
|
||||
- name: Install dependencies
|
||||
uses: nick-fields/retry@v4
|
||||
with:
|
||||
@@ -1419,28 +1224,15 @@ jobs:
|
||||
retry_wait_seconds: 30
|
||||
command: pnpm install --frozen-lockfile
|
||||
|
||||
# Why here: electron-builder's beforePack requires out/mobile-web, and the bundle
|
||||
# build resolves React Native and Expo from mobile/node_modules.
|
||||
# Build required out/mobile-web from mobile’s React Native/Expo dependencies.
|
||||
- uses: ./.github/actions/install-mobile-dependencies
|
||||
|
||||
# Why: `pnpm build:release` verifies the Linux computer-use provider by
|
||||
# importing AT-SPI bindings, which are runtime package deps but are not
|
||||
# present on stock GitHub Ubuntu release runners.
|
||||
# Why: `rpm` is needed by electron-builder's fpm backend to produce the
|
||||
# .rpm artifact. Stock Ubuntu runners do not ship it.
|
||||
# Install AT-SPI runtime bindings for Linux provider verification and rpm for builder’s fpm backend; stock Ubuntu lacks them.
|
||||
- name: Install Linux computer-use provider dependencies
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y python3-gi gir1.2-atspi-2.0 at-spi2-core xclip xdotool rpm
|
||||
|
||||
# Why: telemetry's transport gate (`src/main/telemetry/client.ts:IS_OFFICIAL_BUILD`)
|
||||
# requires the build identity to be the literal string `stable` or `rc`,
|
||||
# substituted by electron-vite's `define` block at build time. Derive
|
||||
# that identity from the release tag here — `stable` for plain semver
|
||||
# (`vX.Y.Z`), `rc` for prerelease (`vX.Y.Z-rc.N`). The strict regex is
|
||||
# a safety net: this workflow only fires on cut-tags that already match
|
||||
# one of those shapes, but if a future change ever loosens that, we
|
||||
# refuse to ship rather than let an unclassified build go out with
|
||||
# `BUILD_IDENTITY = null`.
|
||||
# Derive literal stable/rc telemetry identity from strict tag shape; refuse unclassified builds rather than ship silent null identity (#1385 follow-up).
|
||||
- name: Classify release tag for telemetry build identity
|
||||
id: tag-classify
|
||||
shell: bash
|
||||
@@ -1448,9 +1240,7 @@ jobs:
|
||||
TAG: ${{ needs.cut.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Why the optional trailing identifier: suffixed side-branch RCs
|
||||
# (vX.Y.Z-rc.N.perf) are rc-channel prerelease builds — same telemetry
|
||||
# identity as plain RCs.
|
||||
# Suffixed vX.Y.Z-rc.N.perf builds keep the same rc-channel telemetry identity.
|
||||
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+(\.[0-9A-Za-z]+)?$ ]]; then
|
||||
identity=rc
|
||||
elif [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
@@ -1462,10 +1252,7 @@ jobs:
|
||||
echo "identity=$identity" >>"$GITHUB_OUTPUT"
|
||||
echo "Classified $TAG as $identity"
|
||||
|
||||
# Why an explicit step rather than trusting the runner image: the packaged
|
||||
# CLI launcher is a native Rust binary, and a Windows host without cargo
|
||||
# fails deep inside electron-builder's native hook instead of here. Real
|
||||
# Windows dev machines do not have it by default either.
|
||||
# Require cargo before builder invokes the packaged Rust launcher hook.
|
||||
- name: Ensure the Rust toolchain for the Windows CLI launcher
|
||||
if: matrix.platform == 'win'
|
||||
shell: pwsh
|
||||
@@ -1514,15 +1301,11 @@ jobs:
|
||||
- name: Gate runtime file-watcher process isolation
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
# Why: #8212 is a native-process crash contract. Prove both the Node
|
||||
# host and the exact Electron runtime survive SIGSEGV before packaging.
|
||||
# #8212: require Node and the exact Electron runtime to survive native SIGSEGV before packaging.
|
||||
node config/scripts/runtime-file-watcher-fault-harness.mjs
|
||||
ELECTRON_RUN_AS_NODE=1 pnpm exec electron config/scripts/runtime-file-watcher-fault-harness.mjs
|
||||
|
||||
# Why: main ships minified with sourcemap:'hidden' and packaging drops
|
||||
# out/**/*.map from app.asar, so a crash trace from a released build is
|
||||
# otherwise undecodable. The main bundle is platform-independent, so one
|
||||
# leg publishes the maps for the whole release.
|
||||
# Publish hidden main sourcemaps separately because app.asar drops maps; one platform suffices for platform-independent main bundle.
|
||||
- name: Bundle main-process source maps
|
||||
id: bundle-main-sourcemaps
|
||||
if: matrix.platform == 'linux-x64'
|
||||
@@ -1532,8 +1315,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$(find out/main -name '*.js.map' -print -quit)" ]; then
|
||||
# Older cut tags predate the hidden-source-map build setting. They
|
||||
# are valid legacy releases, but have no map bundle to publish.
|
||||
# Old valid cut tags predate hidden sourcemaps and have no bundle to publish.
|
||||
if grep -Eq "sourcemap:[[:space:]]*['\"]hidden['\"]" electron.vite.config.ts; then
|
||||
echo "::error::No main-process source maps in out/main despite build.sourcemap='hidden'."
|
||||
exit 1
|
||||
@@ -1543,9 +1325,7 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
echo "has_maps=true" >>"$GITHUB_OUTPUT"
|
||||
# Why: every entry in electron-builder's `files` is a negation, so
|
||||
# app-builder prepends `**/*` and packs anything left in the workspace
|
||||
# root into app.asar. Stage the bundle outside the checkout instead.
|
||||
# All-negation builder files prepend **/*; stage outside checkout to avoid packing maps into app.asar.
|
||||
find out/main -name '*.js.map' -print | sort | zip -q -X "$RUNNER_TEMP/orca-sourcemaps-$TAG.zip" -@
|
||||
ls -l "$RUNNER_TEMP/orca-sourcemaps-$TAG.zip"
|
||||
|
||||
@@ -1596,16 +1376,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# The release command prepares and verifies this host's Electron native modules first.
|
||||
ORCA_REUSE_PREPARED_NATIVE_RUNTIME: '1'
|
||||
# Why: the NSIS uninstaller only exists inside electron-builder's
|
||||
# uninstaller pass, which deletes it right after embedding it. The sign
|
||||
# hook in config/scripts/windows-uninstaller-signing.cjs copies it out
|
||||
# here so it can ride the inner-binaries SignPath request below.
|
||||
# Why runner.temp and never the workspace: `files` in
|
||||
# config/electron-builder.config.cjs is all-negation, so app-builder
|
||||
# prepends `**/*` and packs whatever is left in the checkout root. This
|
||||
# step retries up to 3 times; attempt 1 writes the file after packing,
|
||||
# but attempts 2 and 3 would then pack the unsigned uninstaller into
|
||||
# app.asar - the exact defect this chain exists to remove.
|
||||
# Capture NSIS uninstaller in sign hook before builder embeds/deletes it, including it in inner-binary signing. Keep capture under runner.temp: all-negation files includes checkout leftovers, and retry would pack unsigned uninstaller into app.asar.
|
||||
ORCA_WIN_UNINSTALLER_EXPORT_PATH: ${{ runner.temp }}\uninstaller-signing\unsigned\orca-uninstaller.exe
|
||||
|
||||
- name: Verify Windows node-pty ConPTY runtime
|
||||
@@ -1630,18 +1401,9 @@ jobs:
|
||||
if: matrix.platform == 'win' && github.run_attempt == 1
|
||||
uses: ./.github/actions/install-signpath-module
|
||||
|
||||
# ── Windows inner-binary signing (issue #7785) ─────────────────────
|
||||
# Why: SignPath cannot deep-sign inside NSIS installers, so inner PE
|
||||
# files (Orca.exe, node-pty *.node, DLLs) are signed via a separate zip
|
||||
# request, then the installer is rebuilt from the signed tree before the
|
||||
# existing installer signing request below. The NSIS uninstaller rides
|
||||
# this same request (it is the MDE update cluster: old-uninstaller.exe /
|
||||
# Uninstall Orca.exe), captured through electron-builder's sign hook and
|
||||
# swapped back in during the rebuild — no third approval wait. Production
|
||||
# releases require the entire signing chain to succeed (#23383).
|
||||
# #7785/#23383: sign inner PE zip, including captured NSIS uninstaller, then rebuild/sign installer; SignPath cannot deep-sign inside NSIS and this avoids a third approval.
|
||||
|
||||
# Why: only unsigned PE files go to SignPath. Files that already carry a
|
||||
# valid signature (Microsoft's OpenConsole.exe) must keep their signer.
|
||||
# Stage only unsigned PE files; preserve existing vendor signatures such as Microsoft OpenConsole.
|
||||
- name: Stage unsigned inner PE files for signing
|
||||
id: stage-inner
|
||||
if: matrix.platform == 'win' && github.run_attempt == 1
|
||||
@@ -1685,12 +1447,8 @@ jobs:
|
||||
Write-Host "Skipped $($skipped.Count) already-signed files:"
|
||||
$skipped | ForEach-Object { Write-Host " $_" }
|
||||
|
||||
# Why the uninstaller rides this request: it is the file MDE flagged in
|
||||
# the whole update cluster (old-uninstaller.exe / Uninstall Orca.exe),
|
||||
# and folding it in here costs no extra approval wait. Why it is kept
|
||||
# out of inner-signing-list.txt: that list drives the copy-back into
|
||||
# dist/win-unpacked, and the uninstaller does not live there — it is
|
||||
# re-injected through the sign hook during the rebuild instead.
|
||||
# Include MDE-flagged old-uninstaller.exe/Uninstall Orca.exe in this approval without another wait.
|
||||
# Exclude it from win-unpacked’s copy-back list; the sign hook re-injects it during rebuild.
|
||||
$exportedUninstaller = Join-Path $env:RUNNER_TEMP 'uninstaller-signing\unsigned\orca-uninstaller.exe'
|
||||
if (-not (Test-Path -LiteralPath $exportedUninstaller)) {
|
||||
throw "No exported NSIS uninstaller at $exportedUninstaller."
|
||||
@@ -1822,9 +1580,7 @@ jobs:
|
||||
New-Item -ItemType Directory -Path signed-inner -Force
|
||||
Expand-Archive -Path signed-inner.zip -DestinationPath signed-inner -Force
|
||||
|
||||
# Why: copy back strictly by the staged list so a layout mismatch in the
|
||||
# returned artifact fails before publication instead of silently
|
||||
# shipping a mix of signed and unsigned binaries.
|
||||
# Copy back only staged paths; layout mismatch must fail before mixed signed/unsigned assets ship.
|
||||
- name: Restore signed inner binaries into unpacked app
|
||||
id: restore-signed-inner
|
||||
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.download-signed-inner.outcome == 'success'
|
||||
@@ -1858,8 +1614,7 @@ jobs:
|
||||
throw "Signed inner artifact did not round-trip cleanly ($($failures.Count) failures)."
|
||||
}
|
||||
|
||||
# Why: SignPath rewrote the template's Windows binaries, and the client materializer checks
|
||||
# each file against the template manifest, so it must record the signed bytes.
|
||||
# Recompute template checksums after SignPath rewrites Windows binaries so materialization validates signed bytes.
|
||||
- name: Reseal the orcad template over its signed binaries
|
||||
id: reseal-orcad-template
|
||||
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.restore-signed-inner.outcome == 'success' && needs.cut.outputs.ships_orcad_template == 'true'
|
||||
@@ -1891,21 +1646,7 @@ jobs:
|
||||
Copy-Item -LiteralPath $signed.FullName -Destination (Join-Path $signedDir 'orca-uninstaller.exe') -Force
|
||||
Write-Host ("{0,-14} uninstaller <{1}>" -f $signature.Status, $signature.SignerCertificate.Subject)
|
||||
|
||||
# Why this step exists: electron-builder's CopyElevateHelper re-copies a
|
||||
# pristine elevate.exe from its download cache over resources\elevate.exe
|
||||
# on EVERY nsis pack — including the --prepackaged rebuild below — which
|
||||
# clobbered the SignPath signature in v1.4.129-rc.4. There is no supported
|
||||
# way to disable just the copy, so we overwrite the cache's copy with our
|
||||
# signed one (identical bytes plus signature) so the clobber becomes a
|
||||
# no-op. Known quirk: the cache persists across releases via actions/cache,
|
||||
# so later runs may see elevate.exe as already signed and skip staging it —
|
||||
# that is fine (the signature is timestamped) and the evidence gate checks
|
||||
# elevate.exe in the shipped installer unconditionally.
|
||||
#
|
||||
# The cache lookup lives in a script because the inline path this step used
|
||||
# (`<cache>\nsis`) matches no app-builder-lib layout, and `SilentlyContinue`
|
||||
# plus `exit 0` turned that miss into a green step — v1.4.193 and v1.4.194
|
||||
# shipped an unsigned elevate.exe that way. A miss now fails the step.
|
||||
# Builder recopies cached elevate.exe on every NSIS pack, clobbering signature in v1.4.129-rc.4; replace cache copy with signed bytes before rebuild. Persisted timestamped signatures may skip later staging; shipped-installer evidence still checks them. Scripted lookup fails missing cache paths: old nonexistent <cache>/nsis plus silent success shipped unsigned elevate.exe in v1.4.193/v1.4.194.
|
||||
- name: Replace cached elevate.exe with the signed copy
|
||||
id: sign-elevate-cache
|
||||
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.restore-signed-inner.outcome == 'success'
|
||||
@@ -1915,9 +1656,7 @@ jobs:
|
||||
if (-not (Test-Path $signed)) {
|
||||
throw 'No elevate.exe in win-unpacked resources.'
|
||||
}
|
||||
# Why this guard stays: windows-signing-rehearsal.yml shares the
|
||||
# electron-builder-win-<lockfile hash> cache key with this workflow, so a
|
||||
# test-certificate elevate.exe must never be staged into a release cache.
|
||||
# Rehearsal shares the builder cache: never seed a release with its test-certificate elevate.exe.
|
||||
$signature = Get-AuthenticodeSignature -FilePath $signed
|
||||
$subject = if ($null -eq $signature.SignerCertificate) { '<none>' } else { $signature.SignerCertificate.Subject }
|
||||
if ($signature.Status -ne 'Valid' -or $subject -notlike '*CN=SignPath Foundation*') {
|
||||
@@ -2107,9 +1846,7 @@ jobs:
|
||||
}
|
||||
$signature.SignerCertificate | Format-List Subject,Issuer,NotBefore,NotAfter,Thumbprint
|
||||
|
||||
# Why: evidence gate for inner-binary signing (issue #7785, supersedes
|
||||
# PR #7170's Orca.exe-only gate — this covers every staged .exe/.dll/.node
|
||||
# by extracting the shipped installer). Unsigned binaries block publication.
|
||||
# #7785 evidence extracts every staged exe/dll/node from shipped installer, superseding #7170’s Orca.exe-only gate; unsigned assets block publish.
|
||||
- name: Verify Windows inner binary signatures
|
||||
if: matrix.platform == 'win' && github.run_attempt == 1
|
||||
shell: pwsh
|
||||
@@ -2162,10 +1899,8 @@ jobs:
|
||||
$report = New-Object System.Collections.Generic.List[string]
|
||||
$failures = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
# Why: verify the files a user actually gets on disk, not the build
|
||||
# tree — 7z parses the NSIS exe directly as its embedded payload.
|
||||
# Resolve 7za via app-builder-lib; electron-builder 26.9+ dropped the
|
||||
# bundled 7zip-bin package the old hardcoded path relied on (#6487).
|
||||
# Verify shipped NSIS payload bytes via 7z, not the build tree. Resolve 7za through app-builder-lib:
|
||||
# builder 26.9+ dropped bundled 7zip-bin (#6487).
|
||||
$7zaOutput = node config/scripts/resolve-7za-path.mjs
|
||||
$7zaExitCode = $LASTEXITCODE
|
||||
if ($7zaExitCode -ne 0) {
|
||||
@@ -2181,11 +1916,7 @@ jobs:
|
||||
|
||||
$root = Resolve-Path 'inner-evidence-extract'
|
||||
# Verify the CLI even when a cached signed copy bypassed staging (#23383).
|
||||
# Why elevate.exe is always appended: staging skips already-signed
|
||||
# files, and the persisted electron-builder cache can carry a
|
||||
# previously signed elevate.exe — so it may be absent from the list
|
||||
# in some runs, yet it is the file most at risk of losing its
|
||||
# signature in the NSIS rebuild. Verify it in every release.
|
||||
# Always verify elevate.exe, even when cached signatures skipped staging; NSIS rebuild can clobber it.
|
||||
$targets = @(Get-Content 'inner-signing-list.txt')
|
||||
foreach ($requiredTarget in @('Orca.exe', 'resources\bin\orca.exe', 'resources\bin\orca-agent-hook.exe')) {
|
||||
if ($targets -notcontains $requiredTarget) { $targets += $requiredTarget }
|
||||
@@ -2193,17 +1924,9 @@ jobs:
|
||||
if ($targets -notcontains 'resources\elevate.exe') {
|
||||
$targets += 'resources\elevate.exe'
|
||||
}
|
||||
# Why the uninstaller is not in $targets: NSIS embeds it in its own
|
||||
# compressed data section (`File /oname=${UNINSTALL_FILENAME}` in
|
||||
# app-builder-lib templates/nsis/include/installer.nsh), not in the
|
||||
# app 7z payload extracted above - the bundled 7za cannot see it.
|
||||
# What the receipt proves and does not: the digest comparison is
|
||||
# equal by construction (the hook digests the bytes it copied from
|
||||
# this same file), so the real signal is that the receipt exists at
|
||||
# all - the import leg ran, and these are the bytes it embedded. The
|
||||
# signature check below is the part with teeth. The shipped-artifact
|
||||
# check lives in windows-signing-rehearsal.yml, which installs the
|
||||
# installer and inspects the uninstaller it drops on disk.
|
||||
# The uninstaller lives in NSIS’s compressed data section, outside the app 7z payload.
|
||||
# A receipt proves the sign-hook import ran, not the installed bytes; its digest equality is by construction.
|
||||
# Check that input’s signature here; windows-signing-rehearsal installs/inspects the actual shipped uninstaller.
|
||||
if ($env:UNINSTALLER_SIGNING_COMPLETED -eq 'true') {
|
||||
$signedUninstaller = Join-Path $env:RUNNER_TEMP 'uninstaller-signing\signed\orca-uninstaller.exe'
|
||||
$receipt = "$signedUninstaller.embedded-sha256"
|
||||
@@ -2246,10 +1969,7 @@ jobs:
|
||||
if ($failures.Count -gt 0) {
|
||||
$failures | ForEach-Object { Write-Host "::warning::$_" }
|
||||
$message = "Windows inner-binary evidence gate found $($failures.Count) problems."
|
||||
# Why assigned before any I/O: a write that throws here would reach
|
||||
# the catch with $policyFailure still null, so a required-mode
|
||||
# signature failure would be re-reported as ERRORED and the per-file
|
||||
# report overwritten — the exact masking the hoist exists to prevent.
|
||||
# Set policyFailure before I/O: write errors must not turn FAILED into ERRORED and overwrite per-file evidence.
|
||||
if ($required) {
|
||||
$policyFailure = $message
|
||||
} else {
|
||||
@@ -2258,8 +1978,7 @@ jobs:
|
||||
Add-GateEvidence "VERDICT: FAILED — $message"
|
||||
Add-GateSummary "FAILED — $message"
|
||||
} else {
|
||||
# $report, not $targets: the embedded uninstaller is reported but
|
||||
# is not one of the extracted payload targets.
|
||||
# Use report, not extracted targets: it also includes the embedded uninstaller.
|
||||
$ok = "All $($report.Count) checked binaries are signed by SignPath Foundation."
|
||||
Add-GateEvidence "VERDICT: PASSED — $ok"
|
||||
Add-GateSummary "PASSED — $ok"
|
||||
@@ -2329,28 +2048,14 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Verify release remains draft after artifact upload
|
||||
# Why: electron-builder `--publish always` can create a public release
|
||||
# as soon as this platform uploads. Re-draft immediately, then fail, so
|
||||
# /releases/latest never keeps serving a missing Windows exe.
|
||||
# Why bash: the Windows matrix defaults to pwsh, which does not expand
|
||||
# "$TAG" into argv.
|
||||
# Re-draft immediately if publish:always exposed a partial release, then fail so latest cannot keep missing Windows assets. Use bash for $TAG expansion.
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ needs.cut.outputs.tag }}
|
||||
run: node config/scripts/assert-github-release-is-draft.mjs "${{ needs.cut.outputs.tag }}"
|
||||
|
||||
# Why post-pack for Linux: electron-builder packs and uploads in one
|
||||
# `--publish always` invocation. The previous step re-drafts if that
|
||||
# upload flipped the GitHub release public; this telemetry check still
|
||||
# blocks `publish-release` from undrafting a bad binary.
|
||||
#
|
||||
# Why this guards against: a misconfigured CI run where
|
||||
# `ORCA_POSTHOG_WRITE_KEY` is unset or the tag fails to classify
|
||||
# would otherwise produce a binary with `BUILD_IDENTITY = null` and
|
||||
# `WRITE_KEY = null`, which silently disables transport
|
||||
# (`IS_OFFICIAL_BUILD === false`) — the exact failure mode flagged
|
||||
# in PR #1385's deferred follow-up.
|
||||
# Check Linux telemetry after inseparable pack/upload, then block final undraft. Reject null identity/key transport from missing configuration (#1385); prior step re-drafts premature publication.
|
||||
- name: Verify telemetry constants present in app.asar
|
||||
run: node config/scripts/verify-telemetry-constants.mjs
|
||||
|
||||
@@ -2371,9 +2076,7 @@ jobs:
|
||||
needs.release-javascript.result == 'success' &&
|
||||
(needs.orcad-template.result == 'success' ||
|
||||
(needs.orcad-template.result == 'skipped' && needs.cut.outputs.ships_orcad_template == 'false'))
|
||||
# Why: SignPath requires every job in this signing workflow to be
|
||||
# GitHub-hosted. The actual mac build runs in release-mac-build.yml so
|
||||
# Blacksmith stays outside Windows artifact provenance.
|
||||
# Keep every SignPath workflow job GitHub-hosted; separate release-mac-build allows Blacksmith outside Windows provenance.
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
@@ -2496,22 +2199,14 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Verify release assets complete
|
||||
# Why: publish-release is the only intended draft -> published
|
||||
# transition. Refuse to un-draft until every updater manifest and
|
||||
# referenced installer asset is present on GitHub.
|
||||
# Only undraft after every updater manifest and referenced installer exists.
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ needs.cut.outputs.tag }}
|
||||
run: node config/scripts/verify-release-required-assets.mjs "$TAG"
|
||||
|
||||
- name: Publish release
|
||||
# Why: derive `--prerelease` from the tag shape (not from whatever
|
||||
# electron-builder left the release flagged as). On 2026-04-27,
|
||||
# electron-builder's publish step flipped `prerelease` back to
|
||||
# `false` on -rc.N releases, which caused an RC to be marked as
|
||||
# GitHub's "latest" release and broke release-cut.yml's math.
|
||||
# Re-asserting here means the final release state is determined
|
||||
# by the tag — a ground truth electron-builder can't rewrite.
|
||||
# Reassert prerelease from tag: on 2026-04-27 builder flipped RC to false/latest and broke release math; builder output is not authoritative.
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ needs.cut.outputs.tag }}
|
||||
|
||||
@@ -47,9 +47,7 @@ jobs:
|
||||
build-mac:
|
||||
needs: qualification-template
|
||||
if: ${{ !cancelled() && (!inputs.qualification_only || needs.qualification-template.result == 'success') && github.repository == 'stablyai/orca' }}
|
||||
# Why: this workflow is outside the SignPath signing run, so Blacksmith
|
||||
# cannot enter Windows artifact provenance while mac notarization gets the
|
||||
# faster runner.
|
||||
# Keep Blacksmith mac notarization outside SignPath’s GitHub-hosted Windows provenance.
|
||||
runs-on: blacksmith-6vcpu-macos-15
|
||||
timeout-minutes: 60
|
||||
# actions: read fetches the release-cut run's artifacts.
|
||||
@@ -97,8 +95,7 @@ jobs:
|
||||
pnpm-lock.yaml
|
||||
mobile/pnpm-lock.yaml
|
||||
|
||||
# Why: packaging bundles the pinned Node into the macOS terminal host. Keyed on the pin alone
|
||||
# with no restore-keys, so a pin bump starts empty instead of carrying old archives forward.
|
||||
# Cache bundled host Node by exact pin, without restore keys.
|
||||
- name: Locate the pinned Node runtime cache
|
||||
shell: bash
|
||||
run: echo "ORCA_NODE_RUNTIME_CACHE_DIR=$HOME/Library/Caches/orca-node-runtime" >>"$GITHUB_ENV"
|
||||
@@ -109,8 +106,7 @@ jobs:
|
||||
path: ~/Library/Caches/orca-node-runtime
|
||||
key: orca-node-runtime-mac-${{ hashFiles('src/shared/node-runtime-pin.ts') }}
|
||||
|
||||
# Cache the Electron binary + electron-builder tool downloads (notarytool,
|
||||
# winCodeSign, nsis, squirrel, AppImage). Saves ~30-90s per job, incl. mac.
|
||||
# Cache Electron/builder tool downloads to save ~30–90s.
|
||||
- name: Cache electron-builder downloads
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
@@ -121,12 +117,7 @@ jobs:
|
||||
restore-keys: |
|
||||
electron-builder-mac-
|
||||
|
||||
# Why: pnpm install triggers electron's postinstall, which downloads the
|
||||
# Electron binary from GitHub release assets. GitHub's download CDN
|
||||
# occasionally returns 504s that fail the whole release. Retry on
|
||||
# failure so transient network errors don't require a manual re-run.
|
||||
# Why both CPUs: the mac config packages x64 and arm64 from this arm64
|
||||
# runner, so the install must carry both variants of the native optional deps.
|
||||
# Retry transient Electron CDN 504s; install both x64/arm64 optional natives for dual macOS packages.
|
||||
- name: Install dependencies
|
||||
uses: nick-fields/retry@v4
|
||||
with:
|
||||
@@ -135,8 +126,7 @@ jobs:
|
||||
retry_wait_seconds: 30
|
||||
command: pnpm install --frozen-lockfile --cpu=current,x64,arm64
|
||||
|
||||
# Why here: electron-builder's beforePack requires out/mobile-web, and the bundle
|
||||
# build resolves React Native and Expo from mobile/node_modules.
|
||||
# Build required out/mobile-web using mobile’s React Native/Expo install.
|
||||
- uses: ./.github/actions/install-mobile-dependencies
|
||||
|
||||
- name: Verify macOS signing environment
|
||||
@@ -148,14 +138,11 @@ jobs:
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
# Why: `plutil -lint` accepts duplicate plist keys, but `codesign`
|
||||
# rejects duplicate entitlements after the expensive app build.
|
||||
# Check duplicate entitlements before build: plutil lint accepts duplicates but codesign rejects them.
|
||||
- name: Verify macOS entitlements
|
||||
run: pnpm verify:macos-entitlements
|
||||
|
||||
# Why: telemetry's transport gate (`src/main/telemetry/client.ts:IS_OFFICIAL_BUILD`)
|
||||
# requires the build identity to be the literal string `stable` or `rc`,
|
||||
# substituted by electron-vite's `define` block at build time.
|
||||
# Set literal stable/rc telemetry identity through Vite define.
|
||||
- name: Classify release tag for telemetry build identity
|
||||
id: tag-classify
|
||||
shell: bash
|
||||
@@ -163,9 +150,7 @@ jobs:
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Why the optional trailing identifier: suffixed side-branch RCs
|
||||
# (vX.Y.Z-rc.N.perf) are rc-channel prerelease builds — same telemetry
|
||||
# identity as plain RCs.
|
||||
# Suffixed vX.Y.Z-rc.N.perf builds keep the same rc-channel telemetry identity.
|
||||
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+(\.[0-9A-Za-z]+)?$ ]]; then
|
||||
identity=rc
|
||||
elif [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
@@ -216,16 +201,13 @@ jobs:
|
||||
|
||||
- name: Gate runtime file-watcher process isolation
|
||||
run: |
|
||||
# Why: #8212 is a native-process crash contract. Prove both the Node
|
||||
# host and the exact Electron runtime survive SIGSEGV before packaging.
|
||||
# #8212: require Node and the exact Electron runtime to survive native SIGSEGV before packaging.
|
||||
node config/scripts/runtime-file-watcher-fault-harness.mjs
|
||||
ELECTRON_RUN_AS_NODE=1 pnpm exec electron config/scripts/runtime-file-watcher-fault-harness.mjs
|
||||
|
||||
- name: Abort if the parent release-cut run was cancelled
|
||||
if: ${{ !inputs.qualification_only }}
|
||||
# Why: this workflow is dispatched separately, so cancelling release-cut
|
||||
# does not stop mac `--publish always`. A cancelled parent left v1.4.206
|
||||
# public with only a partial mac upload.
|
||||
# Watch parent cancellation: separately dispatched mac upload otherwise continues (v1.4.206 went public with partial assets).
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PARENT_RUN: ${{ inputs.release_run_id }}
|
||||
@@ -256,18 +238,14 @@ jobs:
|
||||
|
||||
- name: Verify release remains draft after artifact upload
|
||||
if: ${{ !inputs.qualification_only }}
|
||||
# Why: re-draft immediately if electron-builder flipped the GitHub
|
||||
# release public, then fail. Checking without restoring leaves
|
||||
# /releases/latest serving a missing Windows exe.
|
||||
# Re-draft premature builder publication before failing; latest must not keep an incomplete Windows release.
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: node config/scripts/assert-github-release-is-draft.mjs "${{ inputs.tag }}"
|
||||
|
||||
# Why post-publish for macOS: electron-builder packs and uploads in a
|
||||
# single `--publish always` invocation, so there is no cheap insertion
|
||||
# point between pack and upload without splitting those steps.
|
||||
# Check after publish because builder’s single pack/upload invocation has no cheap interposition point.
|
||||
- name: Verify telemetry constants present in app.asar
|
||||
run: node config/scripts/verify-telemetry-constants.mjs
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
name: Terminal IME E2E
|
||||
|
||||
on:
|
||||
# Why workflow_call and not pull_request: pr.yml owns the path filter that decides when an IME
|
||||
# change is worth a real ibus session. A pull_request trigger here would run it on every PR.
|
||||
# Let pr.yml own costly real-IBus path routing; a separate pull_request trigger would run every PR.
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -23,7 +22,7 @@ env:
|
||||
jobs:
|
||||
linux-x11:
|
||||
name: Linux X11 terminal IME
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
@@ -63,9 +62,7 @@ jobs:
|
||||
--workers=1
|
||||
|
||||
- name: Run native IBus Hangul exact-byte tests
|
||||
# Why not the default success(): the synthetic step above runs first, so its failure used
|
||||
# to skip this one entirely — the real-IME half reported nothing on exactly the PRs that
|
||||
# broke IME code. The two signals are independent and both belong in the log.
|
||||
# Run native half despite synthetic failure: independent signals must both appear, rather than default success() hiding native evidence.
|
||||
if: '!cancelled()'
|
||||
env:
|
||||
SKIP_BUILD: '1'
|
||||
@@ -82,7 +79,7 @@ jobs:
|
||||
|
||||
linux-wayland:
|
||||
name: Linux Wayland Hangul terminating digit
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
name: Unit plan
|
||||
|
||||
# Why its own workflow: the caller's `needs` gate the whole called workflow, so while this lived
|
||||
# inside unit-tests.yml it waited on static analysis and typecheck before it could even start —
|
||||
# and the shards then waited on it. Planning needs neither (its inputs are the checkout, a git
|
||||
# diff against HEAD^1, the import graph, and the checked-in timing baseline), so hoisting it out
|
||||
# lets it overlap the gate instead of queueing behind it. Measured: a median 93s off the shard
|
||||
# matrix's start.
|
||||
# Plan independently of static/type gates so checkout/diff/import-graph/timing work overlaps them; moving it out of unit-tests advanced shards by median 93s.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
name: Unit selection evidence
|
||||
|
||||
# Why its own workflow: this job is advisory -- `continue-on-error` on both the job and its
|
||||
# comparison step, so it can never fail a PR. But a caller's `needs: test` waits for every job in
|
||||
# the called workflow, so while it lived in unit-tests.yml it held `verify` for ~36s after the
|
||||
# last shard finished. Called as a sibling instead, it still runs on every PR and still uploads
|
||||
# its review artifact; it just no longer sits on the critical path.
|
||||
# Call advisory evidence beside unit-tests, preserving artifacts without delaying verify ~36s after shards; job/comparison remain continue-on-error.
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
name: Windows Crash-Survival E2E
|
||||
|
||||
# Why: proves the detached daemon-host relocation makes terminal sessions SURVIVE
|
||||
# a CRASH of Orca's main process (GitHub #7742), the companion guarantee to the
|
||||
# update-survival harness. Builds an (unsigned) installer FROM THIS BRANCH,
|
||||
# silent-installs it, opens a terminal, force-kills ONLY the app main (no
|
||||
# tree-kill), and asserts the daemon + shell stay alive, a relaunch adopts the
|
||||
# same daemon and shell, and post-crash input causes no pwsh FailFast. Targeted
|
||||
# manual runs keep it as a durable regression proof. A CI runner is the only
|
||||
# safe place to install — see the relocation post-mortem.
|
||||
# #7742: build/install unsigned branch artifact on disposable CI, crash only main and require same daemon/shell survival, adoption and input without FailFast. Companion update proof tests installer survival; see relocation post-mortem.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
name: Windows packaged orcad serve switch E2E
|
||||
|
||||
# Why: proves D7 on a real Windows install. The installed desktop app forks its terminal daemon
|
||||
# from the relocated %LOCALAPPDATA%\Orca\daemon-host; `orca serve` then serves the same profile
|
||||
# on orcad and must adopt that daemon, and the desktop must reattach the terminal afterwards and
|
||||
# still write its settings under orcad's data-root ACL. Unpackaged e2e cannot exercise the
|
||||
# relocation, which only packaged builds do. A CI runner is the only safe place to install.
|
||||
# D7 packaged Windows proof: relocated desktop daemon survives orcad serve adoption and desktop reattach, with settings writes after data-root ACL. Run install only on disposable CI; unpackaged E2E cannot exercise relocation.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
name: Windows Update-Survival E2E
|
||||
|
||||
# Why: proves what happens to terminal sessions across a real Windows app update
|
||||
# by installing one released version, updating to another, and asserting on the
|
||||
# packaged artifacts (daemon survival, terminal interactivity, zero console
|
||||
# flashes). This MUST run on a disposable CI Windows: electron-builder's oneClick
|
||||
# installer uninstalls the registry-registered copy of the app before every
|
||||
# install, so running the harness on a machine with a real Orca install would
|
||||
# delete it. A fresh runner has no Orca registered, so it is the only safe home
|
||||
# for install-based testing. Manual dispatch only.
|
||||
# Install/update two released Windows builds and prove daemon/input/no-flash behavior on disposable CI only. oneClick uninstalls registry-selected app, so a real workstation install could be deleted.
|
||||
|
||||
# Why: workflow_dispatch only works once a workflow is on the default branch.
|
||||
# To exercise this from the unmerged feature branch WITHOUT touching main, a
|
||||
# push trigger scoped to that exact branch runs the workflow from the branch's
|
||||
# own tree. The paths filter keeps it to harness/workflow edits so ordinary
|
||||
# commits don't spend 30 min of Windows runner time. Push runs have no inputs,
|
||||
# so every parameter falls back to a default below.
|
||||
# Feature-branch push enables pre-merge workflow testing before dispatch exists on default; path-filter it to avoid 30min runs on unrelated changes. Push inputs use defaults.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -113,10 +101,7 @@ jobs:
|
||||
"FROM_EXE=$($from.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"TO_EXE=$($to.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
# Why: no --install-dir here. A CI runner has no real Orca registered, so
|
||||
# the harness installs to the default per-user location and owns it — the
|
||||
# isolated-install machinery exists only to (imperfectly) protect a real
|
||||
# developer install, which does not apply on a throwaway runner.
|
||||
# Own default per-user install on disposable CI; no real install needs the imperfect isolated-mode protection.
|
||||
- name: Run update-survival harness
|
||||
id: harness
|
||||
shell: pwsh
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
name: Windows Update-Survival E2E (branch build)
|
||||
|
||||
# Why: proves the Phase 1 daemon-host relocation actually makes terminal
|
||||
# sessions SURVIVE a Windows update. Builds an (unsigned) installer FROM THIS
|
||||
# BRANCH, then runs the win-update-e2e harness with --expect survival: install,
|
||||
# open a terminal, silently update over it, relaunch, and assert the daemon PID
|
||||
# is unchanged and the pre-update terminal is still interactive with no console
|
||||
# flashing. Runs from the feature branch via push (workflow_dispatch only works
|
||||
# on the default branch); main is never touched. A CI runner is the only safe
|
||||
# place to install/update — see the relocation post-mortem.
|
||||
# Build/install/update unsigned branch artifact with --expect survival; require same daemon and interactive original terminal without flashes. Feature push tests pre-default-branch workflow; run only on disposable CI (relocation post-mortem).
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -78,10 +71,7 @@ jobs:
|
||||
with:
|
||||
install: false
|
||||
|
||||
# Why: cache the built installer keyed on the inputs that affect it, so a
|
||||
# harness-only edit skips the ~20 min electron-builder build. The daemon
|
||||
# relocation code lives under src/, so changing it correctly rebuilds.
|
||||
# Hash before installation creates native build artifacts under native/.
|
||||
# Cache installer by product inputs so harness-only changes skip ~20min build; hash before native install generates artifacts.
|
||||
- name: Cache branch installer
|
||||
id: cache-installer
|
||||
uses: actions/cache/restore@v4
|
||||
|
||||
@@ -1,18 +1,4 @@
|
||||
# Windows inner-binary signing rehearsal.
|
||||
#
|
||||
# Why: SignPath cannot deep-sign inside NSIS installers, so shipping signed
|
||||
# inner binaries (Orca.exe, node-pty *.node, DLLs — see issue #7785) requires
|
||||
# a two-request flow: sign the unpacked PE files first, then build the NSIS
|
||||
# installer from the signed tree, then sign the installer. The NSIS uninstaller
|
||||
# rides that same first request — it is captured through electron-builder's sign
|
||||
# hook and swapped back in during the rebuild — so it adds no third approval.
|
||||
# This workflow rehearses that entire flow from a branch, end to end, without
|
||||
# publishing anything — so the release pipeline on main is never at risk.
|
||||
#
|
||||
# Runs only via manual dispatch. Use the test-signing policy for iteration
|
||||
# (auto-approved test certificate) and release-signing to rehearse the
|
||||
# production flow (requires manual SignPath approvals). Rehearsed successfully
|
||||
# with test-signing (run 28987534795) and release-signing (run 28988432001).
|
||||
# Rehearse #7785 inner-PE/uninstaller signing then rebuilt installer signing without publication. SignPath cannot deep-sign NSIS; captured uninstaller shares first request. Manual test-signing auto-approves; release-signing needs production review. Proven runs 28987534795/28988432001.
|
||||
name: Windows signing rehearsal
|
||||
|
||||
on:
|
||||
@@ -93,16 +79,11 @@ jobs:
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
# Why a full --win build and not --dir: the NSIS uninstaller only exists
|
||||
# inside the installer build, and it is the file the MDE update cluster
|
||||
# flags. --dir would never produce it, so the rehearsal would not rehearse
|
||||
# the uninstaller leg at all. This mirrors release-cut's first Windows pass.
|
||||
# Build full --win: --dir never creates the NSIS uninstaller needed for MDE evidence.
|
||||
- name: Package Windows app and export the NSIS uninstaller
|
||||
shell: pwsh
|
||||
env:
|
||||
# runner.temp, never the workspace: the all-negation `files` list in
|
||||
# config/electron-builder.config.cjs packs whatever is left in the
|
||||
# checkout root into app.asar.
|
||||
# Keep capture under runner.temp; all-negation files would include checkout leftovers in app.asar.
|
||||
ORCA_WIN_UNINSTALLER_EXPORT_PATH: ${{ runner.temp }}\uninstaller-signing\unsigned\orca-uninstaller.exe
|
||||
run: |
|
||||
node config/scripts/ensure-native-runtime.mjs --runtime=electron
|
||||
@@ -116,9 +97,7 @@ jobs:
|
||||
throw "The sign hook did not export the NSIS uninstaller to $env:ORCA_WIN_UNINSTALLER_EXPORT_PATH"
|
||||
}
|
||||
|
||||
# Why: only unsigned PE files go to SignPath. Files that already carry a
|
||||
# valid signature (Microsoft's OpenConsole.exe, signed vendor DLLs) must
|
||||
# keep their original signer, so they are excluded from the request.
|
||||
# Sign only unsigned PE files; retain vendor signatures such as Microsoft OpenConsole.
|
||||
- name: Stage unsigned PE files for inner signing
|
||||
id: stage-inner
|
||||
shell: pwsh
|
||||
@@ -156,11 +135,8 @@ jobs:
|
||||
Write-Host "Skipped $($skipped.Count) already-signed files:"
|
||||
$skipped | ForEach-Object { Write-Host " $_" }
|
||||
|
||||
# Why kept out of inner-signing-list.txt: that list drives the copy-back
|
||||
# into dist/win-unpacked, and the uninstaller does not live there — it is
|
||||
# re-injected through the electron-builder sign hook during the rebuild.
|
||||
# No catch here, unlike the release job: the rehearsal exists to prove
|
||||
# the flow, so a staging failure must fail it loudly.
|
||||
# The uninstaller is re-injected by the sign hook, outside win-unpacked’s copy-back list.
|
||||
# Fail staging loudly: rehearsal proves the flow and needs no release-job recovery catch.
|
||||
$exportedUninstaller = Join-Path $env:RUNNER_TEMP 'uninstaller-signing\unsigned\orca-uninstaller.exe'
|
||||
$uninstallerStagePath = Join-Path $stage.FullName 'uninstaller\orca-uninstaller.exe'
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $uninstallerStagePath) | Out-Null
|
||||
@@ -206,9 +182,7 @@ jobs:
|
||||
New-Item -ItemType Directory -Path signed-inner -Force
|
||||
Expand-Archive -Path signed-inner.zip -DestinationPath signed-inner -Force
|
||||
|
||||
# Why: copy back strictly by the staged list so a layout mismatch in the
|
||||
# returned artifact fails loudly instead of silently shipping a mix of
|
||||
# signed and unsigned binaries.
|
||||
# Copy back strictly by staged list so mismatches fail instead of shipping mixed signing.
|
||||
- name: Restore signed inner binaries into unpacked app
|
||||
shell: pwsh
|
||||
env:
|
||||
@@ -343,10 +317,7 @@ jobs:
|
||||
$latestYml = $latestYml -replace '(?m)^ blockMapSize: \d+$', " blockMapSize: $($blockmap.Length)"
|
||||
Set-Content -Path 'dist/latest.yml' -Value $latestYml -NoNewline
|
||||
|
||||
# Why: this is the pass/fail heart of the rehearsal — the same evidence
|
||||
# check that will later gate releases (see PR #7170). Test certificates
|
||||
# do not chain to a trusted root, so Status=Valid is only required for
|
||||
# release-signing runs; test-signing runs require a present signature.
|
||||
# Use production evidence gate (#7170); require trusted Status=Valid only for release signing, signature presence for test certs.
|
||||
- name: Verify signatures end to end
|
||||
shell: pwsh
|
||||
env:
|
||||
@@ -357,10 +328,7 @@ jobs:
|
||||
$advisories = New-Object System.Collections.Generic.List[string]
|
||||
$requireValid = $env:SIGNING_POLICY -eq 'release-signing'
|
||||
|
||||
# -Advisory records a problem without failing the run. It exists for
|
||||
# exactly one file (resources\elevate.exe, below) and must not be
|
||||
# widened casually: the point of this workflow is to fail when signing
|
||||
# is broken.
|
||||
# Advisory is only for resources\elevate.exe; all other signing failures must fail this rehearsal.
|
||||
function Test-Signature([string]$label, [string]$path, [switch]$Advisory) {
|
||||
$signature = Get-AuthenticodeSignature -FilePath $path
|
||||
$subject = if ($null -eq $signature.SignerCertificate) { '<none>' } else { $signature.SignerCertificate.Subject }
|
||||
@@ -386,9 +354,7 @@ jobs:
|
||||
|
||||
Test-Signature 'installer: orca-windows-setup.exe' 'dist/orca-windows-setup.exe'
|
||||
|
||||
# Extract the signed installer and verify the files a user actually
|
||||
# gets on disk — including the exact file from issue #7785.
|
||||
# electron-builder 26.9+ removed the bundled 7zip-bin package (#6487).
|
||||
# Extract/verify shipped bytes, including #7785. Builder 26.9+ removed bundled 7zip-bin (#6487).
|
||||
$7zaOutput = node config/scripts/resolve-7za-path.mjs
|
||||
$7zaExitCode = $LASTEXITCODE
|
||||
if ($7zaExitCode -ne 0) {
|
||||
@@ -402,13 +368,8 @@ jobs:
|
||||
& $7za x 'dist/orca-windows-setup.exe' '-oextracted-app' -y | Out-Null
|
||||
|
||||
$root = Resolve-Path 'extracted-app'
|
||||
# The receipt only proves the import leg ran; it cannot prove what NSIS
|
||||
# embedded, because the uninstaller lives in a compressed NSIS data
|
||||
# section rather than the app 7z payload above and the bundled 7za has
|
||||
# no NSIS handler. So the rehearsal - unlike the release job, which
|
||||
# must not mutate the runner it publishes from - goes all the way: it
|
||||
# installs the installer silently and inspects the uninstaller the
|
||||
# installer actually wrote to disk. That is the file MDE flags.
|
||||
# The receipt proves import, not NSIS embedding: the uninstaller lies outside the app 7z payload.
|
||||
# Unlike publishing release jobs, rehearsal can install silently and inspect the actual MDE-flagged file.
|
||||
$signedUninstaller = Join-Path $env:RUNNER_TEMP 'uninstaller-signing\signed\orca-uninstaller.exe'
|
||||
$receipt = "$signedUninstaller.embedded-sha256"
|
||||
if (-not (Test-Path -LiteralPath $receipt)) {
|
||||
@@ -417,10 +378,7 @@ jobs:
|
||||
Test-Signature 'relayed: orca-uninstaller.exe' $signedUninstaller
|
||||
}
|
||||
|
||||
# Why a full 7-Zip attempt first: it is non-invasive. The runner image
|
||||
# ships the complete 7z.exe, which - unlike the reduced 7za - has an
|
||||
# NSIS handler. If it cannot read the section either, fall back to a
|
||||
# real silent install.
|
||||
# Try full 7z.exe’s NSIS handler non-invasively first; reduced 7za lacks it. Fall back to a real silent install.
|
||||
$installedUninstaller = $null
|
||||
$installedVia = $null
|
||||
$expectedDigest = if (Test-Path -LiteralPath $receipt) { (Get-Content -LiteralPath $receipt -Raw).Trim() } else { $null }
|
||||
@@ -430,14 +388,8 @@ jobs:
|
||||
& $full7z x -tnsis 'dist/orca-windows-setup.exe' '-onsis-extract' -y 2>&1 | Out-Null
|
||||
$installedUninstaller = Get-ChildItem -Path nsis-extract -Recurse -File -Filter 'Uninstall*.exe' -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1
|
||||
# Why the digest guard before trusting this route: 7-Zip's NSIS
|
||||
# handler emits partial or garbled output on some NSIS builds, and a
|
||||
# truncated extract would score NotSigned and fail the rehearsal as
|
||||
# "the shipped uninstaller is unsigned" when nothing is wrong. Only
|
||||
# trust it when it reproduces the bytes the relay embedded; otherwise
|
||||
# fall through to the install route, which is ground truth. A name
|
||||
# miss (the handler labelling the entry by its source name) falls
|
||||
# through the same way.
|
||||
# NSIS extraction can be partial/garbled: trust 7z only when its digest matches embedded bytes,
|
||||
# otherwise use ground-truth installation. Missing/misnamed entries also fall through.
|
||||
if ($null -ne $installedUninstaller -and $null -ne $expectedDigest -and
|
||||
(Get-FileHash -LiteralPath $installedUninstaller.FullName -Algorithm SHA256).Hash.ToLowerInvariant() -ne $expectedDigest) {
|
||||
Write-Host "7-Zip's NSIS output did not match the relayed digest; falling back to a silent install."
|
||||
@@ -452,19 +404,13 @@ jobs:
|
||||
}
|
||||
|
||||
if ($null -eq $installedUninstaller) {
|
||||
# Nothing here is published, so mutating this runner is free.
|
||||
# Why -PassThru and a bounded wait rather than -Wait: a bare -Wait on
|
||||
# an installer that ever prompts hangs to the job's 360-minute cap.
|
||||
# No artifacts are published, so installation is safe here. Bound -PassThru waits; -Wait could hang on a prompt for 360 minutes.
|
||||
$installerProcess = Start-Process -FilePath (Resolve-Path 'dist/orca-windows-setup.exe') -ArgumentList '/S' -PassThru
|
||||
if (-not $installerProcess.WaitForExit(300000)) {
|
||||
$installerProcess | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
$failures.Add('the silent install did not exit within 5 minutes; it is likely prompting')
|
||||
}
|
||||
# Why a poll rather than one Stop-Process: the oneClick installer
|
||||
# launches the app as it finishes, so Orca.exe can appear *after* the
|
||||
# installer process exits. A single silenced Stop-Process would miss
|
||||
# it and leave Orca plus orca-terminal-daemon.exe holding handles
|
||||
# under %LOCALAPPDATA%\Programs for the rest of the job.
|
||||
# Poll app shutdown: oneClick can launch Orca after installer exit, leaving app/daemon handles in LOCALAPPDATA\Programs.
|
||||
for ($attempt = 0; $attempt -lt 20; $attempt++) {
|
||||
$running = @(Get-Process -Name 'Orca' -ErrorAction SilentlyContinue)
|
||||
if ($running.Count -gt 0) {
|
||||
@@ -484,12 +430,8 @@ jobs:
|
||||
if ($null -eq $installedUninstaller) {
|
||||
$failures.Add('could not obtain the uninstaller the installer ships; neither 7-Zip nor a silent install produced it')
|
||||
} else {
|
||||
# Why this digest comparison is the point of the whole rehearsal:
|
||||
# unlike the release job's, it hashes a file NSIS itself wrote out
|
||||
# rather than the file the hook copied, so it is the only check that
|
||||
# proves the shipped installer embedded the SignPath-signed bytes. On
|
||||
# the 7-Zip route the guard above already forced equality; on the
|
||||
# install route this is the first time it is tested.
|
||||
# Hash NSIS-written bytes to prove the installer embedded SignPath-signed input; the release hook’s
|
||||
# input-to-input comparison cannot prove this. The 7z route already requires digest equality.
|
||||
if ($null -ne $expectedDigest) {
|
||||
$shippedDigest = (Get-FileHash -LiteralPath $installedUninstaller.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($shippedDigest -ne $expectedDigest) {
|
||||
@@ -509,34 +451,11 @@ jobs:
|
||||
$failures.Add("missing from installer payload: $relative")
|
||||
continue
|
||||
}
|
||||
# Why elevate.exe alone is advisory: app-builder-lib re-copies the
|
||||
# pristine cached elevate.exe over resources\elevate.exe on EVERY nsis
|
||||
# pack - AppPackageHelper.packArch calls elevateHelper.copy() before
|
||||
# buildAppPackage (nsisUtil.js), and CopyElevateHelper.copy does
|
||||
# `copyFile(elevatePath, outFile, false)` then `signIf(outFile)`, which
|
||||
# signs nothing because this build configures no certificate. So the
|
||||
# signed copy restored into win-unpacked is clobbered by the rebuild.
|
||||
# This predates the uninstaller relay and is not caused by it: with no
|
||||
# `sign` hook, signIf already returned false at "no signing info
|
||||
# identified" (windowsSignToolManager.js), so no signtool call was
|
||||
# displaced. release-cut.yml mitigates it separately by pre-seeding the
|
||||
# electron-builder cache ("Replace cached elevate.exe with the signed
|
||||
# copy"); this workflow has no such step, which is why the clobber is
|
||||
# visible here and not there. Mirroring that step here would not help:
|
||||
# it only swaps when the copy is already Valid and SignPath-signed, so
|
||||
# it no-ops under the test certificate.
|
||||
#
|
||||
# DO NOT relax that Valid + SignPath-signed guard to make this
|
||||
# rehearsal go green. This workflow and release-cut.yml share the
|
||||
# cache key `electron-builder-win-<lockfile hash>`, and that guard is
|
||||
# the only thing stopping a test certificate from being seeded into
|
||||
# the cache a real release restores from. Shipping users a binary
|
||||
# signed by "Test certificate for 'Orca agent ide [OSS]'" is worse
|
||||
# than shipping it unsigned.
|
||||
#
|
||||
# Fixing elevate.exe belongs in its own PR - it is a UAC elevation
|
||||
# helper, and it deserves more scrutiny than a footnote in an
|
||||
# uninstaller change.
|
||||
# Only elevate.exe is advisory: NSIS pack recopies pristine cache bytes over signed win-unpacked output.
|
||||
# With no certificate, CopyElevateHelper.signIf signs nothing; this predates the uninstaller relay.
|
||||
# release-cut pre-seeds only Valid, SignPath-signed cache bytes; test certificates deliberately fail that guard.
|
||||
# DO NOT relax it: rehearsal shares electron-builder-win-<lockfile hash> with real releases and would poison
|
||||
# their cache with “Test certificate for Orca agent ide [OSS]”. Fix the UAC helper separately with proper scrutiny.
|
||||
if ($relative -eq 'resources\elevate.exe') {
|
||||
Test-Signature "installed: $relative" $path -Advisory
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user