Skip to content

fix(ci): stabilize UI harnesses and Corepack setup #1912

fix(ci): stabilize UI harnesses and Corepack setup

fix(ci): stabilize UI harnesses and Corepack setup #1912

Workflow file for this run

name: CI — Tests
on:
pull_request:
push:
branches:
- dev
- preview
- main
workflow_call:
inputs:
select_jobs_explicitly:
description: "Honor run_* inputs instead of running the ordinary CI defaults"
required: false
default: false
type: boolean
runner_pool:
description: "Runner pool for approved non-secret Linux test lanes"
required: false
default: github
type: string
run_ui_e2e:
required: false
default: false
type: boolean
ui_e2e_specs:
description: "Optional comma-separated repo-relative UI E2E spec paths; blank runs all shards"
required: false
default: ''
type: string
run_mobile_e2e_android:
required: false
default: false
type: boolean
run_mobile_e2e_ios:
required: false
default: false
type: boolean
run_ui:
required: false
default: true
type: boolean
run_server:
required: false
default: true
type: boolean
run_server_db_contract:
required: false
default: true
type: boolean
run_cli:
required: false
default: true
type: boolean
run_stack:
required: false
default: true
type: boolean
run_typecheck:
required: false
default: true
type: boolean
run_cli_daemon_e2e:
required: false
default: true
type: boolean
run_e2e_core:
required: false
default: true
type: boolean
run_e2e_core_slow:
required: false
default: false
type: boolean
checkout_sha:
required: false
default: ''
type: string
run_providers:
required: false
default: false
type: boolean
providers_preset:
required: false
default: all
type: string
providers_tier:
required: false
default: smoke
type: string
providers_opencode:
required: false
default: "1.1.52|c7c1e622aff3d457358385e28dff33ffa8401cb9067e865a9f35a2f94e899490"
type: string
providers_flags:
required: false
default: '{"strict_keys":false,"flake_retry":true,"update_baselines":false}'
type: string
providers_models:
required: false
default: '{"default":"","overrides":""}'
type: string
run_stress:
required: false
default: false
type: boolean
run_release_contracts:
required: false
default: false
type: boolean
run_installers_smoke:
required: false
default: true
type: boolean
installers_channel:
required: false
default: stable
type: string
installers_source:
required: false
default: published-channel
type: string
installers_ref:
required: false
default: ''
type: string
run_binary_smoke:
required: false
default: true
type: boolean
run_cli_update_continuity:
required: false
default: false
type: boolean
cli_update_to_source:
required: false
default: local-build
type: string
cli_update_to_ref:
required: false
default: '.'
type: string
run_daemon_continuity:
required: false
default: false
type: boolean
run_session_continuity:
required: false
default: false
type: boolean
run_release_assets_docker:
required: false
default: false
type: boolean
relay_upgrade_to_source:
required: false
default: local-build
type: string
relay_upgrade_to_ref:
required: false
default: '.'
type: string
run_self_host_systemd:
required: false
default: false
type: boolean
run_self_host_launchd:
required: false
default: false
type: boolean
run_self_host_schtasks:
required: false
default: false
type: boolean
run_self_host_daemon:
required: false
default: false
type: boolean
stress_config:
required: false
default: '{"repeat":"5","seed":""}'
type: string
permissions:
contents: read
concurrency:
group: tests-${{ github.workflow }}-${{ github.ref }}
# Preserve the active collector so late lanes can report their failures. GitHub retains at most
# one pending run per group, so newer pushes replace stale pending work without discarding the
# evidence already being collected by the running workflow.
cancel-in-progress: false
env:
# Speed up CI: UI postinstall vendors large web-only assets (Monaco/Kokoro/Skia web).
# Tests and native builds do not require these assets.
HAPPIER_UI_VENDOR_WEB_ASSETS: "0"
jobs:
ci_plan:
name: Resolve affected source-CI lanes
permissions:
contents: read
pull-requests: read
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
# A protected-branch push is admissible as whole-source release evidence, so its established
# fast source set must cover the complete head. Pull requests retain path-selected feedback,
# while release/deep-only lanes below remain selected by their actual changed seams.
run_ui: ${{ github.event_name == 'push' || steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.changes.outputs.ui == 'true' || steps.unmatched.outputs.ui == 'true' }}
run_server: ${{ github.event_name == 'push' || steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.changes.outputs.server == 'true' || steps.unmatched.outputs.server == 'true' }}
run_cli: ${{ github.event_name == 'push' || steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.changes.outputs.cli == 'true' || steps.unmatched.outputs.cli == 'true' }}
run_stack: ${{ github.event_name == 'push' || steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.changes.outputs.stack == 'true' || steps.unmatched.outputs.stack == 'true' }}
run_shared_packages: ${{ github.event_name == 'push' || steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.unmatched.outputs.shared_packages == 'true' }}
run_ui_e2e: ${{ steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.changes.outputs.ui_e2e == 'true' }}
run_server_db_contract: ${{ steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.changes.outputs.server_db_contract == 'true' }}
run_release_contracts: ${{ steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.changes.outputs.release_contracts == 'true' }}
run_installers_smoke: ${{ steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.changes.outputs.installers_smoke == 'true' }}
run_binary_smoke: ${{ steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.changes.outputs.binary_smoke == 'true' }}
run_cli_daemon_e2e: ${{ steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.changes.outputs.cli_daemon_e2e == 'true' }}
run_e2e_core: ${{ github.event_name == 'push' || steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.changes.outputs.e2e_core == 'true' }}
run_typecheck: ${{ github.event_name == 'push' || steps.unmatched.outputs.all == 'true' || steps.changes.outputs.all == 'true' || steps.changes.outputs.typecheck == 'true' }}
steps:
- name: Checkout
if: ${{ !inputs.select_jobs_explicitly }}
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
fetch-depth: 2
- name: Classify affected source-CI lanes once
id: changes
if: ${{ !inputs.select_jobs_explicitly }}
uses: dorny/paths-filter@0e4a8c6effa4802afeda77dc8d303f8176d7dfad # v3
with:
list-files: json
filters: |
changed:
- '**'
documentation:
- '**/*.md'
- '**/*.mdx'
all:
- 'package.json'
- 'yarn.lock'
- '.yarnrc*'
- '.node-version'
- 'tsconfig*.json'
- '.github/actions/enable-corepack-yarn/**'
- '.github/actions/install-yarn-dependencies/**'
- 'scripts/workspaces/**'
ui:
- 'apps/ui/**'
- 'apps/bootstrap/**'
- 'package.json'
- 'yarn.lock'
- 'tsconfig*.json'
- 'vitest.config.ts'
- 'scripts/ci/install_sapling_ubuntu22.sh'
- 'scripts/testing/featureTestGating*'
- 'scripts/generateBuiltInPrompts.mjs'
- 'scripts/generateBuiltInPrompts.test.mjs'
- 'skills/happier-diagnose/**'
server:
- 'apps/server/**'
- 'package.json'
- 'yarn.lock'
- 'tsconfig*.json'
- 'scripts/testing/featureTestGating*'
cli:
- 'apps/cli/**'
- 'scripts/install/**'
- 'package.json'
- 'yarn.lock'
- 'tsconfig*.json'
- 'scripts/ci/install_sapling_ubuntu22.sh'
- 'scripts/testing/featureTestGating*'
- 'scripts/ensureCliCommonDistModule.mjs'
- 'scripts/ensureCliCommonDistModule.test.mjs'
stack:
- 'apps/stack/**'
- 'scripts/install/**'
- 'package.json'
- 'yarn.lock'
- 'tsconfig*.json'
ui_e2e:
- 'apps/ui/**'
- 'apps/server/**'
- 'apps/cli/**'
- 'packages/tests/**'
- 'packages/protocol/**'
- 'package.json'
- 'yarn.lock'
server_db_contract:
- 'apps/server/prisma/**'
- 'apps/server/sources/migrations/**'
- 'apps/server/sources/storage/**'
- '**/schema.prisma'
- 'scripts/testing/featureTestGating*'
release_contracts:
- '.github/**'
- 'scripts/release/**'
- 'scripts/pipeline/**'
- 'packages/tests/src/testkit/process/**'
installers_smoke:
- '.github/actions/bootstrap-minisign/**'
- 'apps/ui/src-tauri/**'
- 'scripts/install/**'
- 'scripts/release/installers/**'
- 'scripts/pipeline/release/lib/minisign*'
- 'scripts/pipeline/tauri/**'
binary_smoke:
- 'apps/cli/**'
- 'apps/server/**'
- 'packages/cli-common/**'
- 'packages/protocol/**'
- 'scripts/pipeline/release/**'
cli_daemon_e2e:
- 'apps/cli/src/cli/commands/service/**'
- 'apps/cli/src/daemon/**'
- 'packages/cli-common/src/process/**'
- 'packages/cli-common/src/service/**'
- 'scripts/install/**'
e2e_core:
- 'apps/cli/**'
- 'apps/server/**'
- 'packages/protocol/**'
- 'packages/tests/**'
- 'package.json'
- 'yarn.lock'
- 'scripts/ci/install_sapling_ubuntu22.sh'
- 'scripts/testing/featureTestGating*'
typecheck:
- 'apps/**'
- 'packages/**'
- 'scripts/**/*.ts'
- 'scripts/**/*.tsx'
- name: Fail closed for unclassified executable source
id: unmatched
if: ${{ !inputs.select_jobs_explicitly }}
env:
CHANGED_PATHS_JSON: ${{ steps.changes.outputs.changed_files }}
DOCUMENTATION_PATHS_JSON: ${{ steps.changes.outputs.documentation_files }}
CLASSIFIED_PATHS_ALL: ${{ steps.changes.outputs.all_files }}
CLASSIFIED_PATHS_UI: ${{ steps.changes.outputs.ui_files }}
CLASSIFIED_PATHS_SERVER: ${{ steps.changes.outputs.server_files }}
CLASSIFIED_PATHS_CLI: ${{ steps.changes.outputs.cli_files }}
CLASSIFIED_PATHS_STACK: ${{ steps.changes.outputs.stack_files }}
CLASSIFIED_PATHS_UI_E2E: ${{ steps.changes.outputs.ui_e2e_files }}
CLASSIFIED_PATHS_SERVER_DB_CONTRACT: ${{ steps.changes.outputs.server_db_contract_files }}
CLASSIFIED_PATHS_RELEASE_CONTRACTS: ${{ steps.changes.outputs.release_contracts_files }}
CLASSIFIED_PATHS_INSTALLERS_SMOKE: ${{ steps.changes.outputs.installers_smoke_files }}
CLASSIFIED_PATHS_BINARY_SMOKE: ${{ steps.changes.outputs.binary_smoke_files }}
CLASSIFIED_PATHS_CLI_DAEMON_E2E: ${{ steps.changes.outputs.cli_daemon_e2e_files }}
CLASSIFIED_PATHS_E2E_CORE: ${{ steps.changes.outputs.e2e_core_files }}
CLASSIFIED_PATHS_TYPECHECK: ${{ steps.changes.outputs.typecheck_files }}
run: node scripts/ci/classify-source-ci-paths.mjs --github-output "$GITHUB_OUTPUT"
ui-e2e:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_ui_e2e) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_ui_e2e == 'true') }}
needs: [ci_plan, trusted_ref_guard]
name: UI E2E (Playwright) / shard ${{ matrix.shard }}/18
strategy:
fail-fast: false
matrix:
shard: ${{ fromJSON(inputs.ui_e2e_specs != '' && '[1]' || '[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]') }}
runs-on: ${{ needs.trusted_ref_guard.outputs.ubuntu_2204 }}
timeout-minutes: 120
env:
# UI E2E boots Expo web; keep web assets available to avoid CI-only runtime surprises.
HAPPIER_UI_VENDOR_WEB_ASSETS: "1"
UI_E2E_SPECS: ${{ inputs.ui_e2e_specs }}
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
persist-credentials: false
- name: Setup Node (GitHub Actions)
if: env.ACT != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Node (act)
if: env.ACT == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Install sqlite3 (if missing)
run: |
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 --version
exit 0
fi
sudo apt-get update
sudo apt-get install -y sqlite3
sqlite3 --version
- name: Run UI E2E
run: |
set -euo pipefail
if [ -z "$UI_E2E_SPECS" ]; then
yarn -s test:e2e:ui -- --shard=${{ matrix.shard }}/18
exit 0
fi
IFS=',' read -r -a requested_specs <<< "$UI_E2E_SPECS"
specs=()
for raw_spec in "${requested_specs[@]}"; do
spec="${raw_spec#"${raw_spec%%[![:space:]]*}"}"
spec="${spec%"${spec##*[![:space:]]}"}"
case "$spec" in
packages/tests/suites/ui-e2e/*.spec.ts) ;;
*) echo "::error::Invalid UI E2E spec path: $spec" >&2; exit 1 ;;
esac
if [ ! -f "$spec" ]; then
echo "::error::UI E2E spec does not exist: $spec" >&2
exit 1
fi
specs+=("$spec")
done
if [ "${#specs[@]}" -eq 0 ]; then
echo "::error::ui_e2e_specs did not contain any spec paths" >&2
exit 1
fi
yarn -s test:e2e:ui -- "${specs[@]}"
- name: Upload UI E2E artifacts (Playwright)
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ui-e2e-playwright-artifacts-shard-${{ matrix.shard }}-of-18
compression-level: 0
path: |
packages/tests/.project/logs/e2e/ui-playwright
.project/logs/e2e/*ui-e2e*/**/*.log
.project/logs/e2e/*ui-e2e*/**/*.jsonl
if-no-files-found: ignore
mobile-e2e-android:
# Native E2E is intentionally opt-in for now (not a PR check).
if: ${{ inputs.select_jobs_explicitly && inputs.run_mobile_e2e_android }}
name: Mobile E2E (Maestro) (android) (opt-in)
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
# Keep this lane mobile-focused (avoid downloading heavy web-only assets).
HAPPIER_INSTALL_SCOPE: "ui,protocol,agents,tests"
HAPPIER_UI_VENDOR_WEB_ASSETS: "0"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile
- name: Install Maestro
run: |
bash scripts/ci/install_maestro.sh
- name: Run Mobile E2E (android)
run: yarn -s test:e2e:mobile
- name: Upload Mobile E2E artifacts (Maestro)
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: mobile-e2e-maestro-android-artifacts
path: |
packages/tests/.project/logs/e2e/mobile-maestro
if-no-files-found: ignore
mobile-e2e-ios:
# Native E2E is intentionally opt-in for now (not a PR check).
if: ${{ inputs.select_jobs_explicitly && inputs.run_mobile_e2e_ios }}
name: Mobile E2E (Maestro) (ios) (opt-in)
runs-on: macos-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
HAPPIER_INSTALL_SCOPE: "ui,protocol,agents,tests"
HAPPIER_UI_VENDOR_WEB_ASSETS: "0"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile
- name: Install Maestro
run: |
bash scripts/ci/install_maestro.sh
- name: Run Mobile E2E (ios)
run: yarn -s test:e2e:mobile:ios
- name: Upload Mobile E2E artifacts (Maestro)
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: mobile-e2e-maestro-ios-artifacts
path: |
packages/tests/.project/logs/e2e/mobile-maestro
if-no-files-found: ignore
ui-unit:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_ui) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_ui == 'true') }}
needs: [ci_plan, trusted_ref_guard]
name: UI Unit Tests / part ${{ matrix.part }}/4
strategy:
fail-fast: false
matrix:
part: [1, 2, 3, 4]
runs-on: ${{ needs.trusted_ref_guard.outputs.ubuntu_2204 }}
timeout-minutes: 120
env:
HAPPIER_UI_VITEST_PART: ${{ matrix.part }}
HAPPIER_UI_VITEST_PARTS: "4"
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node (GitHub Actions)
if: env.ACT != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Node (act)
if: env.ACT == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install Sapling
run: |
bash scripts/ci/install_sapling_ubuntu22.sh
- name: Verify Sapling CLI
run: sl --version
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile
- name: Run protocol unit tests
id: protocol-tests
if: matrix.part == 1
continue-on-error: true
run: yarn workspace @happier-dev/protocol test
- name: Run unit tests
id: unit-tests
continue-on-error: true
run: yarn workspace @happier-dev/app test:unit
- name: Require all UI unit test lanes
if: always()
env:
UI_UNIT_PART: ${{ matrix.part }}
PROTOCOL_TESTS_OUTCOME: ${{ steps.protocol-tests.outcome }}
UNIT_TESTS_OUTCOME: ${{ steps.unit-tests.outcome }}
run: |
if { [ "$UI_UNIT_PART" = "1" ] && [ "$PROTOCOL_TESTS_OUTCOME" != "success" ]; } || [ "$UNIT_TESTS_OUTCOME" != "success" ]; then
echo "One or more UI unit test lanes failed: protocol=$PROTOCOL_TESTS_OUTCOME unit=$UNIT_TESTS_OUTCOME" >&2
exit 1
fi
ui-integration:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_ui) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_ui == 'true') }}
needs: [ci_plan, trusted_ref_guard]
name: UI Integration Tests
runs-on: ${{ needs.trusted_ref_guard.outputs.ubuntu_2204 }}
timeout-minutes: 240
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node (GitHub Actions)
if: env.ACT != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Node (act)
if: env.ACT == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install Sapling
run: |
bash scripts/ci/install_sapling_ubuntu22.sh
- name: Verify Sapling CLI
run: sl --version
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile
- name: Run integration tests
run: yarn workspace @happier-dev/app test:integration
ui:
if: ${{ always() && ((inputs.select_jobs_explicitly && inputs.run_ui) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_ui == 'true')) }}
needs: [ci_plan, ui-unit, ui-integration]
name: UI Tests (unit + integration)
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- name: Require all UI test lanes
env:
UNIT_TESTS_OUTCOME: ${{ needs.ui-unit.result }}
INTEGRATION_TESTS_OUTCOME: ${{ needs.ui-integration.result }}
run: |
if [ "$UNIT_TESTS_OUTCOME" != "success" ] || [ "$INTEGRATION_TESTS_OUTCOME" != "success" ]; then
echo "One or more UI test lanes failed: unit=$UNIT_TESTS_OUTCOME integration=$INTEGRATION_TESTS_OUTCOME" >&2
exit 1
fi
shared-packages-unit:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_ui) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_shared_packages == 'true') }}
needs: [ci_plan, trusted_ref_guard]
name: Shared Package Unit Tests
runs-on: ${{ needs.trusted_ref_guard.outputs.ubuntu_2204 }}
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node (GitHub Actions)
if: env.ACT != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Node (act)
if: env.ACT == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.5
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Test privacy-kit
id: privacy_kit
continue-on-error: true
run: yarn workspace privacy-kit test
- name: Test privacy-kit Bun runtime
id: privacy_kit_bun
continue-on-error: true
run: yarn workspace privacy-kit test:runtime:bun
- name: Test transfers
id: transfers
continue-on-error: true
run: yarn workspace @happier-dev/transfers test
- name: Test agents
id: agents
continue-on-error: true
run: yarn workspace @happier-dev/agents test
- name: Test CLI common
id: cli_common
continue-on-error: true
run: yarn workspace @happier-dev/cli-common test
- name: Test connection supervisor
id: connection_supervisor
continue-on-error: true
run: yarn workspace @happier-dev/connection-supervisor test
- name: Test bootstrap
id: bootstrap
continue-on-error: true
run: yarn workspace @happier-dev/bootstrap test
- name: Test relay server
id: relay_server
continue-on-error: true
run: yarn --cwd packages/relay-server test
- name: Verify generated built-in prompts
id: built_in_prompts
continue-on-error: true
run: node --test scripts/generateBuiltInPrompts.test.mjs
- name: Require all shared package checks
id: require-shared-package-checks
if: ${{ always() }}
env:
PRIVACY_KIT_OUTCOME: ${{ steps.privacy_kit.outcome }}
PRIVACY_KIT_BUN_OUTCOME: ${{ steps.privacy_kit_bun.outcome }}
TRANSFERS_OUTCOME: ${{ steps.transfers.outcome }}
AGENTS_OUTCOME: ${{ steps.agents.outcome }}
CLI_COMMON_OUTCOME: ${{ steps.cli_common.outcome }}
CONNECTION_SUPERVISOR_OUTCOME: ${{ steps.connection_supervisor.outcome }}
BOOTSTRAP_OUTCOME: ${{ steps.bootstrap.outcome }}
RELAY_SERVER_OUTCOME: ${{ steps.relay_server.outcome }}
BUILT_IN_PROMPTS_OUTCOME: ${{ steps.built_in_prompts.outcome }}
run: |
failed=0
for result in \
"$PRIVACY_KIT_OUTCOME" \
"$PRIVACY_KIT_BUN_OUTCOME" \
"$TRANSFERS_OUTCOME" \
"$AGENTS_OUTCOME" \
"$CLI_COMMON_OUTCOME" \
"$CONNECTION_SUPERVISOR_OUTCOME" \
"$BOOTSTRAP_OUTCOME" \
"$RELAY_SERVER_OUTCOME" \
"$BUILT_IN_PROMPTS_OUTCOME"; do
if [ "$result" != "success" ]; then
failed=1
fi
done
if [ "$failed" -ne 0 ]; then
echo "One or more shared package checks failed; inspect the individual steps above." >&2
exit 1
fi
server:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_server) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_server == 'true') }}
needs: [ci_plan, trusted_ref_guard]
name: Server Tests (unit + integration)
runs-on: ${{ needs.trusted_ref_guard.outputs.ubuntu_2404 }}
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node (GitHub Actions)
if: env.ACT != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Node (act)
if: env.ACT == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run unit tests
id: unit-tests
continue-on-error: true
run: yarn --cwd apps/server test:unit
- name: Run integration tests
id: integration-tests
continue-on-error: true
run: yarn --cwd apps/server test:integration
- name: Require all server test lanes
if: always()
env:
UNIT_TESTS_OUTCOME: ${{ steps.unit-tests.outcome }}
INTEGRATION_TESTS_OUTCOME: ${{ steps.integration-tests.outcome }}
run: |
if [ "$UNIT_TESTS_OUTCOME" != "success" ] || [ "$INTEGRATION_TESTS_OUTCOME" != "success" ]; then
echo "One or more server test lanes failed: unit=$UNIT_TESTS_OUTCOME integration=$INTEGRATION_TESTS_OUTCOME" >&2
exit 1
fi
server-db-contract:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_server_db_contract) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_server_db_contract == 'true') }}
needs: [ci_plan, trusted_ref_guard]
name: Server DB Contract (Postgres)
runs-on: ${{ needs.trusted_ref_guard.outputs.ubuntu_2204 }}
timeout-minutes: 25
services:
postgres:
image: postgres:17
env:
POSTGRES_DB: happier
POSTGRES_USER: happier
POSTGRES_PASSWORD: happier
ports:
- 5432/tcp
options: >-
--health-cmd="pg_isready -U happier -d happier"
--health-interval=10s
--health-timeout=5s
--health-retries=10
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node (GitHub Actions)
if: env.ACT != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Node (act)
if: env.ACT == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Migrate (Postgres)
env:
DATABASE_URL: postgresql://happier:happier@127.0.0.1:${{ job.services.postgres.ports[5432] }}/happier?sslmode=disable
run: yarn --cwd apps/server -s prisma migrate deploy
- name: Run db contract suite (Postgres)
env:
HAPPIER_DB_PROVIDER: postgres
DATABASE_URL: postgresql://happier:happier@127.0.0.1:${{ job.services.postgres.ports[5432] }}/happier?sslmode=disable
run: yarn --cwd apps/server test:server:db-contract
cli:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_cli) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_cli == 'true') }}
needs: [ci_plan, trusted_ref_guard]
name: CLI Tests (unit + integration) / part ${{ matrix.part }}/2
permissions:
contents: read
strategy:
fail-fast: false
matrix:
part: [1, 2]
runs-on: ${{ needs.trusted_ref_guard.outputs.ubuntu_2204 }}
timeout-minutes: 60
env:
# Some CLI tests fetch/inspect GitHub Release assets. Prefer the built-in
# GitHub Actions token rather than requiring extra secrets.
GH_TOKEN: ${{ github.token }}
HAPPIER_GITHUB_TOKEN: ${{ github.token }}
HAPPIER_CLI_TMUX_INTEGRATION: "1"
HAPPIER_CLI_DAEMON_REATTACH_INTEGRATION: "1"
HAPPIER_NO_BROWSER_OPEN: "1"
HAPPIER_CLI_VITEST_PART: ${{ matrix.part }}
HAPPIER_CLI_VITEST_PARTS: "2"
TMPDIR: /tmp
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node (GitHub Actions)
if: env.ACT != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Node (act)
if: env.ACT == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install system dependencies
run: |
export DEBIAN_FRONTEND=noninteractive
APT_FLAGS="-o Acquire::Retries=3 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30"
if [ "${ACT:-}" = "true" ] || [ -d /var/run/act ]; then
. /etc/os-release || true
CODENAME="${VERSION_CODENAME:-noble}"
ARCH="$(dpkg --print-architecture)"
BASE_URL="http://ports.ubuntu.com/ubuntu-ports"
if [ "${ARCH}" = "amd64" ]; then BASE_URL="http://archive.ubuntu.com/ubuntu"; fi
if command -v sudo >/dev/null 2>&1; then
sudo rm -f /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources || true
echo "deb ${BASE_URL} ${CODENAME} main universe" | sudo tee /etc/apt/sources.list >/dev/null
else
rm -f /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources || true
echo "deb ${BASE_URL} ${CODENAME} main universe" > /etc/apt/sources.list
fi
fi
if command -v sudo >/dev/null 2>&1; then
sudo apt-get ${APT_FLAGS} update
sudo apt-get ${APT_FLAGS} install -y --no-install-recommends tmux
else
apt-get ${APT_FLAGS} update
apt-get ${APT_FLAGS} install -y --no-install-recommends tmux
fi
tmux -V
- name: Install Sapling
run: |
bash scripts/ci/install_sapling_ubuntu22.sh
- name: Verify Sapling CLI
run: sl --version
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Disable remote logging for CI (avoid hanging test process)
run: |
set -euo pipefail
if [ -f apps/cli/.env.integration-test ]; then
sed -i 's/^DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING=.*/DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING=/' apps/cli/.env.integration-test
sed -i 's/^DEBUG=.*/DEBUG=/' apps/cli/.env.integration-test
fi
- name: Run unit tests and import-cycle guard
id: unit-tests
continue-on-error: true
run: yarn workspace @happier-dev/cli test:unit
- name: Test root CLI common dist admission owner
id: cli-common-dist
if: ${{ matrix.part == 1 && !cancelled() }}
continue-on-error: true
run: node --test scripts/ensureCliCommonDistModule.test.mjs
- name: Run integration tests
id: integration-tests
if: ${{ !cancelled() }}
continue-on-error: true
run: yarn workspace @happier-dev/cli test:integration
- name: Require all cli test lanes
if: always()
env:
CLI_PART: ${{ matrix.part }}
UNIT_TESTS_OUTCOME: ${{ steps.unit-tests.outcome }}
CLI_COMMON_DIST_OUTCOME: ${{ steps.cli-common-dist.outcome }}
INTEGRATION_TESTS_OUTCOME: ${{ steps.integration-tests.outcome }}
run: |
if [ "$UNIT_TESTS_OUTCOME" != "success" ] || [ "$INTEGRATION_TESTS_OUTCOME" != "success" ] || { [ "$CLI_PART" = "1" ] && [ "$CLI_COMMON_DIST_OUTCOME" != "success" ]; }; then
echo "One or more cli test lanes failed: unit=$UNIT_TESTS_OUTCOME common_dist=$CLI_COMMON_DIST_OUTCOME integration=$INTEGRATION_TESTS_OUTCOME" >&2
exit 1
fi
stack:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_stack) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_stack == 'true') }}
needs: [ci_plan, trusted_ref_guard]
name: Stack Tests (unit + integration)
permissions:
contents: read
runs-on: ${{ needs.trusted_ref_guard.outputs.ubuntu_2404 }}
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run unit tests
id: unit-tests
continue-on-error: true
run: yarn --cwd apps/stack test:unit
- name: Run integration tests
id: integration-tests
continue-on-error: true
run: yarn --cwd apps/stack test:integration
- name: Require all stack test lanes
if: always()
env:
UNIT_TESTS_OUTCOME: ${{ steps.unit-tests.outcome }}
INTEGRATION_TESTS_OUTCOME: ${{ steps.integration-tests.outcome }}
run: |
if [ "$UNIT_TESTS_OUTCOME" != "success" ] || [ "$INTEGRATION_TESTS_OUTCOME" != "success" ]; then
echo "One or more stack test lanes failed: unit=$UNIT_TESTS_OUTCOME integration=$INTEGRATION_TESTS_OUTCOME" >&2
exit 1
fi
release-contracts:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_release_contracts) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_release_contracts == 'true') }}
needs: [ci_plan]
name: Release Contracts
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run release contract tests
id: release-contracts
continue-on-error: true
run: yarn -s test:release:contracts
- name: Verify installer release projection
id: release-sync-installers
if: ${{ !cancelled() }}
continue-on-error: true
run: node scripts/pipeline/run.mjs release-sync-installers --check
- name: Require all release contract checks
if: always()
env:
RELEASE_CONTRACTS_OUTCOME: ${{ steps.release-contracts.outcome }}
RELEASE_SYNC_INSTALLERS_OUTCOME: ${{ steps.release-sync-installers.outcome }}
run: |
if [ "$RELEASE_CONTRACTS_OUTCOME" != "success" ] || [ "$RELEASE_SYNC_INSTALLERS_OUTCOME" != "success" ]; then
echo "One or more release contract checks failed: contracts=$RELEASE_CONTRACTS_OUTCOME installer_projection=$RELEASE_SYNC_INSTALLERS_OUTCOME" >&2
exit 1
fi
installers-smoke-linux:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_installers_smoke) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_installers_smoke == 'true') }}
needs: [ci_plan]
name: Installer Smoke (Linux)
runs-on: ubuntu-latest
timeout-minutes: 20
env:
INSTALLERS_CHANNEL: ${{ inputs.select_jobs_explicitly && inputs.installers_channel || ((github.event_name == 'push' && github.ref_name == 'main') || (github.event_name == 'pull_request' && github.base_ref == 'main')) && 'stable' || 'dev' }}
INSTALLERS_SOURCE: ${{ inputs.select_jobs_explicitly && inputs.installers_source || ((github.event_name == 'push' && github.ref_name == 'main') || (github.event_name == 'pull_request' && github.base_ref == 'main')) && 'published-channel' || 'local-build' }}
INSTALLERS_REF: ${{ inputs.select_jobs_explicitly && (inputs.installers_ref != '' && inputs.installers_ref || inputs.installers_channel) || ((github.event_name == 'push' && github.ref_name == 'main') || (github.event_name == 'pull_request' && github.base_ref == 'main')) && 'stable' || '.' }}
INSTALLERS_RELEASE_CHANNEL: ${{ inputs.select_jobs_explicitly && inputs.installers_channel || ((github.event_name == 'push' && github.ref_name == 'main') || (github.event_name == 'pull_request' && github.base_ref == 'main')) && 'stable' || 'dev' }}
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.5
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run unified installer smoke
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
node scripts/pipeline/run.mjs release-validate \
--suite installers-smoke \
--platform linux \
--source "${INSTALLERS_SOURCE}" \
--ref "${INSTALLERS_REF}" \
--release-channel "${INSTALLERS_RELEASE_CHANNEL}"
installers-smoke-macos:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_installers_smoke) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_installers_smoke == 'true') }}
needs: [ci_plan]
name: Installer Smoke (macOS)
runs-on: macos-latest
timeout-minutes: 25
env:
INSTALLERS_CHANNEL: ${{ inputs.select_jobs_explicitly && inputs.installers_channel || ((github.event_name == 'push' && github.ref_name == 'main') || (github.event_name == 'pull_request' && github.base_ref == 'main')) && 'stable' || 'dev' }}
INSTALLERS_SOURCE: ${{ inputs.select_jobs_explicitly && inputs.installers_source || ((github.event_name == 'push' && github.ref_name == 'main') || (github.event_name == 'pull_request' && github.base_ref == 'main')) && 'published-channel' || 'local-build' }}
INSTALLERS_REF: ${{ inputs.select_jobs_explicitly && (inputs.installers_ref != '' && inputs.installers_ref || inputs.installers_channel) || ((github.event_name == 'push' && github.ref_name == 'main') || (github.event_name == 'pull_request' && github.base_ref == 'main')) && 'stable' || '.' }}
INSTALLERS_RELEASE_CHANNEL: ${{ inputs.select_jobs_explicitly && inputs.installers_channel || ((github.event_name == 'push' && github.ref_name == 'main') || (github.event_name == 'pull_request' && github.base_ref == 'main')) && 'stable' || 'dev' }}
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.5
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run unified installer smoke
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
node scripts/pipeline/run.mjs release-validate \
--suite installers-smoke \
--platform darwin \
--source "${INSTALLERS_SOURCE}" \
--ref "${INSTALLERS_REF}" \
--release-channel "${INSTALLERS_RELEASE_CHANNEL}"
installers-smoke-windows:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_installers_smoke) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_installers_smoke == 'true') }}
needs: [ci_plan]
name: Installer Smoke (Windows)
runs-on: windows-latest
timeout-minutes: 45
env:
INSTALLERS_CHANNEL: ${{ inputs.select_jobs_explicitly && inputs.installers_channel || ((github.event_name == 'push' && github.ref_name == 'main') || (github.event_name == 'pull_request' && github.base_ref == 'main')) && 'stable' || 'dev' }}
INSTALLERS_SOURCE: ${{ inputs.select_jobs_explicitly && inputs.installers_source || ((github.event_name == 'push' && github.ref_name == 'main') || (github.event_name == 'pull_request' && github.base_ref == 'main')) && 'published-channel' || 'local-build' }}
INSTALLERS_REF: ${{ inputs.select_jobs_explicitly && (inputs.installers_ref != '' && inputs.installers_ref || inputs.installers_channel) || ((github.event_name == 'push' && github.ref_name == 'main') || (github.event_name == 'pull_request' && github.base_ref == 'main')) && 'stable' || '.' }}
INSTALLERS_RELEASE_CHANNEL: ${{ inputs.select_jobs_explicitly && inputs.installers_channel || ((github.event_name == 'push' && github.ref_name == 'main') || (github.event_name == 'pull_request' && github.base_ref == 'main')) && 'stable' || 'dev' }}
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.5
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run unified installer smoke
shell: pwsh
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
node scripts/pipeline/run.mjs release-validate `
--suite installers-smoke `
--platform win32 `
--source "$env:INSTALLERS_SOURCE" `
--ref "$env:INSTALLERS_REF" `
--release-channel "$env:INSTALLERS_RELEASE_CHANNEL"
binary-smoke:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_binary_smoke) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_binary_smoke == 'true') }}
needs: [ci_plan, trusted_ref_guard]
name: Binary Smoke (isolated cwd)
runs-on: ${{ needs.trusted_ref_guard.outputs.ubuntu_2404 }}
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ inputs.checkout_sha != '' && inputs.checkout_sha || github.sha }}
persist-credentials: false
- name: Verify exact requested checkout SHA
if: ${{ inputs.checkout_sha != '' }}
env:
CHECKOUT_SHA: ${{ inputs.checkout_sha }}
run: |
set -euo pipefail
actual_sha="$(git rev-parse HEAD)"
if [ "$actual_sha" != "$CHECKOUT_SHA" ]; then
echo "Checked out '$actual_sha', expected '$CHECKOUT_SHA'." >&2
exit 1
fi
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.5
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run isolated binary smoke tests
run: |
node scripts/pipeline/run.mjs release-validate \
--suite binary-smoke \
--platform linux \
--source local-build \
--ref "."
cli-update-continuity:
if: ${{ inputs.run_cli_update_continuity }}
needs: [trusted_ref_guard]
name: Release Validation — CLI Update Continuity (Linux)
runs-on: ubuntu-latest
timeout-minutes: 120
env:
CLI_UPDATE_FROM_CHANNEL: ${{ inputs.installers_channel }}
CLI_UPDATE_TO_SOURCE: ${{ inputs.cli_update_to_source }}
CLI_UPDATE_TO_REF: ${{ inputs.cli_update_to_ref }}
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ inputs.checkout_sha != '' && inputs.checkout_sha || github.sha }}
persist-credentials: false
- name: Verify exact requested checkout SHA
if: ${{ inputs.checkout_sha != '' }}
env:
CHECKOUT_SHA: ${{ inputs.checkout_sha }}
run: |
set -euo pipefail
actual_sha="$(git rev-parse HEAD)"
if [ "$actual_sha" != "$CHECKOUT_SHA" ]; then
echo "Checked out '$actual_sha', expected '$CHECKOUT_SHA'." >&2
exit 1
fi
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run unified CLI update continuity validation
env:
CI: "1"
run: |
set -euo pipefail
node scripts/pipeline/run.mjs release-validate \
--suite cli-update \
--platform linux \
--from-source published-channel \
--from-ref "${CLI_UPDATE_FROM_CHANNEL}" \
--to-source "${CLI_UPDATE_TO_SOURCE}" \
--to-ref "${CLI_UPDATE_TO_REF}"
- name: Upload CLI update continuity artifacts (on failure)
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: cli-update-continuity-artifacts
path: .project/logs/e2e
if-no-files-found: ignore
daemon-continuity:
if: ${{ inputs.run_daemon_continuity }}
needs: [trusted_ref_guard]
name: Release Validation — Daemon Continuity (Linux)
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ inputs.checkout_sha != '' && inputs.checkout_sha || github.sha }}
persist-credentials: false
- name: Verify exact requested checkout SHA
if: ${{ inputs.checkout_sha != '' }}
env:
CHECKOUT_SHA: ${{ inputs.checkout_sha }}
run: |
set -euo pipefail
actual_sha="$(git rev-parse HEAD)"
if [ "$actual_sha" != "$CHECKOUT_SHA" ]; then
echo "Checked out '$actual_sha', expected '$CHECKOUT_SHA'." >&2
exit 1
fi
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run unified daemon continuity validation
env:
CI: "1"
run: |
set -euo pipefail
node scripts/pipeline/run.mjs release-validate \
--suite daemon-continuity \
--platform linux \
--source local-build \
--ref "."
- name: Upload daemon continuity artifacts (on failure)
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: daemon-continuity-artifacts
path: .project/logs/e2e
if-no-files-found: ignore
session-continuity:
if: ${{ inputs.run_session_continuity }}
needs: [trusted_ref_guard]
name: Release Validation — Session Continuity (Linux)
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ inputs.checkout_sha != '' && inputs.checkout_sha || github.sha }}
persist-credentials: false
- name: Verify exact requested checkout SHA
if: ${{ inputs.checkout_sha != '' }}
env:
CHECKOUT_SHA: ${{ inputs.checkout_sha }}
run: |
set -euo pipefail
actual_sha="$(git rev-parse HEAD)"
if [ "$actual_sha" != "$CHECKOUT_SHA" ]; then
echo "Checked out '$actual_sha', expected '$CHECKOUT_SHA'." >&2
exit 1
fi
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run unified session continuity validation
env:
CI: "1"
run: |
set -euo pipefail
node scripts/pipeline/run.mjs release-validate \
--suite session-continuity \
--platform linux \
--source local-build \
--ref "."
- name: Upload session continuity artifacts (on failure)
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: session-continuity-artifacts
path: .project/logs/e2e
if-no-files-found: ignore
release-assets-docker:
if: ${{ inputs.select_jobs_explicitly && inputs.run_release_assets_docker }}
name: Release Assets Docker (Linux)
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ inputs.checkout_sha != '' && inputs.checkout_sha || github.sha }}
fetch-depth: 1
persist-credentials: false
- name: Verify exact requested checkout
env:
CHECKOUT_SHA: ${{ inputs.checkout_sha }}
run: |
set -euo pipefail
if [ -n "$CHECKOUT_SHA" ]; then
test "$(git rev-parse HEAD)" = "$CHECKOUT_SHA"
fi
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.5
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run unified Docker release-assets validation
env:
INSTALLERS_CHANNEL: ${{ inputs.installers_channel }}
RELAY_UPGRADE_TO_SOURCE: ${{ inputs.relay_upgrade_to_source }}
RELAY_UPGRADE_TO_REF: ${{ inputs.relay_upgrade_to_ref }}
run: |
set -euo pipefail
node scripts/pipeline/run.mjs release-validate \
--suite docker-release-assets \
--platform linux \
--from-source published-channel \
--from-ref "${INSTALLERS_CHANNEL}" \
--to-source "${RELAY_UPGRADE_TO_SOURCE}" \
--to-ref "${RELAY_UPGRADE_TO_REF}"
self-host-systemd-e2e:
if: ${{ inputs.select_jobs_explicitly && inputs.run_self_host_systemd }}
needs: [trusted_ref_guard]
name: Self-Host E2E (Linux systemd)
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ inputs.checkout_sha != '' && inputs.checkout_sha || github.sha }}
fetch-depth: 1
persist-credentials: false
- name: Verify exact requested checkout SHA
if: ${{ inputs.checkout_sha != '' }}
env:
CHECKOUT_SHA: ${{ inputs.checkout_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$CHECKOUT_SHA"
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.5
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run real systemd self-host E2E
run: |
set -euo pipefail
node --test apps/stack/scripts/self_host_systemd.real.integration.test.mjs
self-host-launchd-e2e:
if: ${{ inputs.select_jobs_explicitly && inputs.run_self_host_launchd }}
needs: [trusted_ref_guard]
name: Self-Host E2E (macOS launchd)
runs-on: macos-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ inputs.checkout_sha != '' && inputs.checkout_sha || github.sha }}
fetch-depth: 1
persist-credentials: false
- name: Verify exact requested checkout SHA
if: ${{ inputs.checkout_sha != '' }}
env:
CHECKOUT_SHA: ${{ inputs.checkout_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$CHECKOUT_SHA"
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.5
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run real launchd self-host E2E
run: |
set -euo pipefail
node --test apps/stack/scripts/self_host_launchd.real.integration.test.mjs
self-host-schtasks-e2e:
if: ${{ inputs.select_jobs_explicitly && inputs.run_self_host_schtasks }}
needs: [trusted_ref_guard]
name: Self-Host E2E (Windows schtasks)
runs-on: windows-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ inputs.checkout_sha != '' && inputs.checkout_sha || github.sha }}
fetch-depth: 1
persist-credentials: false
- name: Verify exact requested checkout SHA
if: ${{ inputs.checkout_sha != '' }}
shell: bash
env:
CHECKOUT_SHA: ${{ inputs.checkout_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$CHECKOUT_SHA"
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.5
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run real schtasks self-host E2E
run: node --test apps/stack/scripts/self_host_schtasks.real.integration.test.mjs
self-host-daemon-e2e:
if: ${{ inputs.select_jobs_explicitly && inputs.run_self_host_daemon }}
needs: [trusted_ref_guard]
name: Self-Host + Daemon E2E
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ inputs.checkout_sha != '' && inputs.checkout_sha || github.sha }}
fetch-depth: 1
persist-credentials: false
- name: Verify exact requested checkout SHA
if: ${{ inputs.checkout_sha != '' }}
env:
CHECKOUT_SHA: ${{ inputs.checkout_sha }}
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "$CHECKOUT_SHA"
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.5
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run real self-host + daemon E2E
run: node --test apps/stack/scripts/self_host_daemon.real.integration.test.mjs
typecheck:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_typecheck) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_typecheck == 'true') }}
needs: [ci_plan, trusted_ref_guard]
name: Typecheck
runs-on: ${{ needs.trusted_ref_guard.outputs.ubuntu_2404 }}
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run governance self-tests
id: wiring-self
continue-on-error: true
run: yarn test:wiring:self
- name: Run policy self-tests
id: policy-self
if: ${{ !cancelled() }}
continue-on-error: true
run: yarn test:policy:self
- name: Run governance validator
id: wiring-validator
if: ${{ !cancelled() }}
continue-on-error: true
run: yarn test:wiring
- name: Run test inventory report
id: inventory
if: ${{ !cancelled() }}
continue-on-error: true
run: yarn test:inventory
- name: Run migration inventory report
id: migration-inventory
if: ${{ !cancelled() }}
continue-on-error: true
run: yarn test:migration:inventory
- name: Run typecheck
id: workspace-typecheck
if: ${{ !cancelled() }}
continue-on-error: true
run: yarn typecheck
- name: Require all governance and typecheck checks
if: always()
env:
WIRING_SELF_OUTCOME: ${{ steps.wiring-self.outcome }}
POLICY_SELF_OUTCOME: ${{ steps.policy-self.outcome }}
WIRING_VALIDATOR_OUTCOME: ${{ steps.wiring-validator.outcome }}
INVENTORY_OUTCOME: ${{ steps.inventory.outcome }}
MIGRATION_INVENTORY_OUTCOME: ${{ steps.migration-inventory.outcome }}
WORKSPACE_TYPECHECK_OUTCOME: ${{ steps.workspace-typecheck.outcome }}
run: |
failed=0
for result in \
"$WIRING_SELF_OUTCOME" \
"$POLICY_SELF_OUTCOME" \
"$WIRING_VALIDATOR_OUTCOME" \
"$INVENTORY_OUTCOME" \
"$MIGRATION_INVENTORY_OUTCOME" \
"$WORKSPACE_TYPECHECK_OUTCOME"; do
if [ "$result" != "success" ]; then
failed=1
fi
done
if [ "$failed" -ne 0 ]; then
echo "One or more governance or typecheck checks failed; inspect the individual steps above." >&2
exit 1
fi
cli-daemon-e2e:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_cli_daemon_e2e) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_cli_daemon_e2e == 'true') }}
needs: [ci_plan]
name: CLI + Server (light/sqlite) E2E
runs-on: ubuntu-latest
timeout-minutes: 120
env:
PORT: "3005"
HAPPIER_SERVER_URL: http://localhost:3005
HAPPIER_WEBAPP_URL: http://localhost:3005
HAPPIER_HOME_DIR: /tmp/happier-dev-test
DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING: "true"
HAPPIER_SERVER_LIGHT_DATA_DIR: /tmp/happier-server-light
HAPPIER_DB_PROVIDER: sqlite
HAPPY_DB_PROVIDER: sqlite
# Fall back to a deterministic non-secret for forks / untrusted PRs (no access to repo secrets).
HANDY_MASTER_SECRET: ${{ secrets.CI_MASTER_SECRET != '' && secrets.CI_MASTER_SECRET || 'happier-ci-master-secret-not-a-real-secret' }}
HAPPIER_NO_BROWSER_OPEN: "1"
TMPDIR: /tmp
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node (GitHub Actions)
if: env.ACT != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Node (act)
if: env.ACT == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install system dependencies
run: |
export DEBIAN_FRONTEND=noninteractive
APT_FLAGS="-o Acquire::Retries=3 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30"
if [ "${ACT:-}" = "true" ] || [ -d /var/run/act ]; then
. /etc/os-release || true
CODENAME="${VERSION_CODENAME:-noble}"
ARCH="$(dpkg --print-architecture)"
BASE_URL="http://ports.ubuntu.com/ubuntu-ports"
if [ "${ARCH}" = "amd64" ]; then BASE_URL="http://archive.ubuntu.com/ubuntu"; fi
if command -v sudo >/dev/null 2>&1; then
sudo rm -f /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources || true
echo "deb ${BASE_URL} ${CODENAME} main universe" | sudo tee /etc/apt/sources.list >/dev/null
else
rm -f /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources || true
echo "deb ${BASE_URL} ${CODENAME} main universe" > /etc/apt/sources.list
fi
fi
if command -v sudo >/dev/null 2>&1; then
sudo apt-get ${APT_FLAGS} update
sudo apt-get ${APT_FLAGS} install -y --no-install-recommends tmux
else
apt-get ${APT_FLAGS} update
apt-get ${APT_FLAGS} install -y --no-install-recommends tmux
fi
tmux -V
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Prepare light DB (SQLite) + apply migrations
run: yarn --cwd apps/server -s migrate:sqlite:deploy
- name: Build CLI
run: yarn workspace @happier-dev/cli build
- name: Run daemon integration suite (with light server)
run: |
set -euo pipefail
mkdir -p "${HAPPIER_SERVER_LIGHT_DATA_DIR}"
nohup yarn --cwd apps/server start:light > "/tmp/happier-server-light.log" 2>&1 &
SERVER_PID="$(jobs -p | tail -n 1 || true)"
if [ -z "${SERVER_PID}" ]; then
echo "Failed to start server process"
tail -n 200 "/tmp/happier-server-light.log" || true
exit 1
fi
echo "${SERVER_PID}" > "/tmp/happier-server-light.pid"
cleanup() {
if [ -n "${SERVER_PID:-}" ]; then
kill "${SERVER_PID}" || true
fi
}
trap cleanup EXIT
for i in $(seq 1 60); do
if curl -fsS "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
# Require health to be stable (avoid flaking on a transient accept before the server is ready).
sleep 1
curl -fsS "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1
echo "Server is healthy (stable)"
break
fi
sleep 1
done
if ! curl -fsS "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
echo "Server failed to become healthy within timeout"
tail -n 200 "/tmp/happier-server-light.log" || true
exit 1
fi
# Create a real account + token via the normal `/v1/auth` flow (ed25519 signature),
# then write the credentials file expected by `.env.integration-test`.
node scripts/pipeline/run.mjs testing-create-auth-credentials
yarn --cwd apps/cli -s vitest run --config vitest.integration.config.ts src/daemon/daemon.integration.test.ts
- name: Dump server logs (on failure)
if: failure()
run: tail -n 300 "/tmp/happier-server-light.log" || true
- name: Dump daemon logs (on failure)
if: failure()
run: |
set -euo pipefail
ROOT="${HAPPIER_HOME_DIR:-$HOME/.happier-dev-test}"
echo "HAPPIER_HOME_DIR=${ROOT}"
echo "--- daemon.state.json candidates (redacted) ---"
find "$ROOT" -maxdepth 4 -type f -name 'daemon.state.json' -print | head -n 10 || true
find "$ROOT" -maxdepth 4 -type f -name 'daemon.state.json' -print | head -n 10 | while read -r f; do
node --input-type=module - <<'NODE' "$f" || true
import fs from "node:fs";
const file = process.argv[1];
const raw = fs.readFileSync(file, "utf8");
const data = JSON.parse(raw);
const redacted = {
pid: data?.pid,
httpPort: data?.httpPort,
startedAt: data?.startedAt,
startedWithCliVersion: data?.startedWithCliVersion,
daemonLogPath: data?.daemonLogPath,
};
console.log(`--- ${file} (redacted) ---`);
console.log(JSON.stringify(redacted, null, 2));
NODE
done
echo "--- logs (match scan) ---"
MATCH_PATTERN="SESSION_WEBHOOK_TIMEOUT|CHILD_EXITED_BEFORE_WEBHOOK|SPAWN_FAILED|Unauthorized|EADDRINUSE|ECONN|fetch failed|Request failed|HTTP 5[0-9]{2}|Deferred awaiter resolution|Current tracked sessions before webhook|Registered externally-started session"
find "$ROOT" -maxdepth 6 -type f \( -name '*-daemon.log' -o -name '*.log' \) -print0 2>/dev/null \
| xargs -0 -r grep -nE "${MATCH_PATTERN}" \
| tail -n 400 || true
echo "--- logs (matched files tail) ---"
find "$ROOT" -maxdepth 6 -type f \( -name '*-daemon.log' -o -name '*.log' \) -print0 2>/dev/null \
| xargs -0 -r grep -lE "${MATCH_PATTERN}" \
| head -n 20 | while read -r f; do
echo "=== tail: $f ==="
tail -n 200 "$f" || true
echo
done
echo "--- logs (tail) ---"
find "$ROOT" -maxdepth 6 -type f \( -name '*-daemon.log' -o -name '*.log' \) -print 2>/dev/null \
| xargs -r ls -1t | head -n 16 | while read -r f; do
echo "=== tail: $f ==="
echo "--- error scan: $f ---"
# Avoid relying on ripgrep being installed; grep is always available.
grep -nE "SESSION_WEBHOOK_TIMEOUT|SPAWN_FAILED|Unauthorized|EADDRINUSE|ECONN|fetch failed|Request failed|HTTP 5[0-9]{2}" "$f" | tail -n 200 || true
echo
tail -n 200 "$f" || true
echo
done
e2e-core:
if: ${{ (inputs.select_jobs_explicitly && inputs.run_e2e_core) || (!inputs.select_jobs_explicitly && needs.ci_plan.outputs.run_e2e_core == 'true') }}
needs: [ci_plan, trusted_ref_guard]
name: Core E2E (fast, packages/tests, server-light, sqlite)
runs-on: ${{ needs.trusted_ref_guard.outputs.ubuntu_2204 }}
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node (GitHub Actions)
if: env.ACT != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Node (act)
if: env.ACT == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install Sapling
run: |
bash scripts/ci/install_sapling_ubuntu22.sh
- name: Verify Sapling CLI
run: sl --version
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run core e2e suite
env:
CI: "1"
HAPPIER_E2E_SAVE_ARTIFACTS: "0"
HAPPIER_E2E_DB_PROVIDER: sqlite
run: yarn test:e2e:core:fast
- name: Upload e2e artifacts (on failure)
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: core-e2e-artifacts-fast-sqlite
path: .project/logs/e2e
e2e-core-slow:
if: ${{ inputs.select_jobs_explicitly && inputs.run_e2e_core_slow }}
needs: [trusted_ref_guard]
name: Core E2E (slow, packages/tests, server-light, sqlite)
runs-on: ${{ needs.trusted_ref_guard.outputs.ubuntu_2204 }}
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ inputs.checkout_sha != '' && inputs.checkout_sha || github.sha }}
- name: Verify exact requested checkout SHA
if: ${{ inputs.checkout_sha != '' }}
env:
CHECKOUT_SHA: ${{ inputs.checkout_sha }}
run: |
set -euo pipefail
actual_sha="$(git rev-parse HEAD)"
if [ "$actual_sha" != "$CHECKOUT_SHA" ]; then
echo "Checked out '$actual_sha', expected '$CHECKOUT_SHA'." >&2
exit 1
fi
- name: Setup Node (GitHub Actions)
if: env.ACT != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Setup Node (act)
if: env.ACT == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install Sapling
run: |
bash scripts/ci/install_sapling_ubuntu22.sh
- name: Verify Sapling CLI
run: sl --version
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Prepare immutable server-v0.2.1 artifact
run: |
set -euo pipefail
artifact_dir="${RUNNER_TEMP}/happier-server-v0.2.1"
echo "HAPPIER_E2E_RELEASED_SERVER_V0_2_1_DIR=${artifact_dir}" >> "$GITHUB_ENV"
HAPPIER_E2E_RELEASED_SERVER_V0_2_1_DIR="${artifact_dir}" \
yarn workspace @happier-dev/tests prepare:compat:released-server-v0.2.1
- name: Install exact-compatibility Playwright browser
run: npx playwright install --with-deps chromium
- name: Run exact server-v0.2.1 compatibility gate
run: yarn workspace @happier-dev/tests test:compat:released-server-v0.2.1
- name: Run core e2e suite (slow)
env:
CI: "1"
HAPPIER_E2E_SAVE_ARTIFACTS: "0"
HAPPIER_E2E_DB_PROVIDER: sqlite
run: yarn test:e2e:core:slow
- name: Upload e2e artifacts (on failure)
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: core-e2e-artifacts-slow-sqlite
path: .project/logs/e2e
trusted_ref_guard:
name: Trusted workflow ref guard
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
ubuntu_2204: ${{ steps.runner_pool.outputs.ubuntu_2204 }}
ubuntu_2404: ${{ steps.runner_pool.outputs.ubuntu_2404 }}
steps:
- name: Admit trusted workflow control ref
env:
CALLER_REPOSITORY: ${{ github.repository }}
EVENT_NAME: ${{ github.event_name }}
EVENT_REF: ${{ github.ref }}
RUNNER_POOL: ${{ inputs.runner_pool || 'github' }}
WORKFLOW_REPOSITORY: ${{ job.workflow_repository }}
WORKFLOW_REF: ${{ job.workflow_ref }}
WORKFLOW_FILE: tests.yml
run: |
set -euo pipefail
test "$CALLER_REPOSITORY" = "$WORKFLOW_REPOSITORY"
admitted=false
case "$WORKFLOW_REF" in
"$WORKFLOW_REPOSITORY/.github/workflows/$WORKFLOW_FILE@refs/heads/dev"|\
"$WORKFLOW_REPOSITORY/.github/workflows/$WORKFLOW_FILE@refs/heads/preview"|\
"$WORKFLOW_REPOSITORY/.github/workflows/$WORKFLOW_FILE@refs/heads/main")
if [[ "$EVENT_NAME" != "pull_request" ]]; then
admitted=true
fi
;;
*)
expected_pr_workflow_ref="$WORKFLOW_REPOSITORY/.github/workflows/$WORKFLOW_FILE@$EVENT_REF"
if [[ "$EVENT_NAME" == "pull_request" &&
"$RUNNER_POOL" == "github" &&
"$EVENT_REF" =~ ^refs/pull/[1-9][0-9]*/merge$ &&
"$WORKFLOW_REF" == "$expected_pr_workflow_ref" ]]; then
admitted=true
fi
;;
esac
if [[ "$admitted" != "true" ]]; then
echo "Untrusted workflow control ref: $WORKFLOW_REF" >&2
exit 1
fi
- name: Validate exact checkout SHA
if: ${{ inputs.checkout_sha != '' }}
env:
CHECKOUT_SHA: ${{ inputs.checkout_sha }}
run: |
set -euo pipefail
if ! [[ "$CHECKOUT_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "checkout_sha must be exactly 40 lowercase hexadecimal characters when supplied." >&2
exit 1
fi
- name: Resolve runner pool
id: runner_pool
env:
RUNNER_POOL: ${{ inputs.runner_pool || 'github' }}
run: |
set -euo pipefail
case "$RUNNER_POOL" in
github)
ubuntu_2204=ubuntu-22.04
ubuntu_2404=ubuntu-latest
;;
blacksmith-linux-4vcpu)
ubuntu_2204=blacksmith-4vcpu-ubuntu-2204
ubuntu_2404=blacksmith-4vcpu-ubuntu-2404
;;
blacksmith-linux-8vcpu)
ubuntu_2204=blacksmith-8vcpu-ubuntu-2204
ubuntu_2404=blacksmith-8vcpu-ubuntu-2404
;;
*)
echo "Unsupported runner_pool: $RUNNER_POOL" >&2
exit 1
;;
esac
{
echo "ubuntu_2204=$ubuntu_2204"
echo "ubuntu_2404=$ubuntu_2404"
} >> "$GITHUB_OUTPUT"
release_actor_guard:
needs: [trusted_ref_guard]
if: ${{ inputs.select_jobs_explicitly && inputs.run_providers }}
name: Release actor guard
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
persist-credentials: false
- name: Authorize release actor
uses: ./.github/actions/release-actor-guard
with:
team_slug: release-admins
app_id: ${{ secrets.RELEASE_BOT_APP_ID }}
private_key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
providers:
needs: [release_actor_guard]
if: ${{ inputs.select_jobs_explicitly && inputs.run_providers }}
name: Providers (contract matrix)
runs-on: ubuntu-latest
environment: providers-ci
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Install provider CLIs
env:
PRESET: ${{ inputs.providers_preset }}
OPENCODE_SPEC: ${{ inputs.providers_opencode }}
run: |
set -euo pipefail
need_claude=0
need_codex=0
need_opencode=0
need_cursor=0
case "$PRESET" in
all) need_claude=1; need_codex=1; need_opencode=1; need_cursor=1 ;;
claude) need_claude=1 ;;
codex) need_codex=1 ;;
opencode) need_opencode=1 ;;
cursor) need_cursor=1 ;;
*) echo "Unknown providers_preset: $PRESET" >&2; exit 2 ;;
esac
if [ "$need_claude" = "1" ]; then
npm install -g @anthropic-ai/claude-code
claude --version
fi
if [ "$need_codex" = "1" ]; then
npm install -g @openai/codex
codex --version
fi
if [ "$need_opencode" = "1" ]; then
# Avoid `curl ... | bash` by downloading a pinned, checksummed release artifact.
opencode_version="${OPENCODE_SPEC%%|*}"
opencode_sha256=""
if [ "$opencode_version" != "$OPENCODE_SPEC" ]; then
opencode_sha256="${OPENCODE_SPEC#*|}"
fi
version="${opencode_version#v}"
asset="opencode-linux-x64-baseline.tar.gz"
url="https://github.com/anomalyco/opencode/releases/download/v${version}/${asset}"
install_dir="$HOME/.opencode/bin"
mkdir -p "$install_dir"
tmp_dir="$(mktemp -d)"
curl -fsSL -o "$tmp_dir/$asset" "$url"
if [ -n "${opencode_sha256:-}" ]; then
echo "${opencode_sha256} $tmp_dir/$asset" | sha256sum -c -
fi
tar -xzf "$tmp_dir/$asset" -C "$tmp_dir"
mv -f "$tmp_dir/opencode" "$install_dir/opencode"
chmod 755 "$install_dir/opencode"
rm -rf "$tmp_dir"
echo "$install_dir" >> "$GITHUB_PATH"
export PATH="$install_dir:$PATH"
opencode --version
fi
if [ "$need_cursor" = "1" ]; then
if ! command -v cursor-agent >/dev/null 2>&1; then
echo "Cursor provider CI requires a preinstalled cursor-agent from the runner image or a pinned setup step" >&2
exit 1
fi
cursor-agent --version
fi
- name: Validate provider auth env
env:
PRESET: ${{ inputs.providers_preset }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
run: |
set -euo pipefail
need_openai=0
need_anthropic=0
need_cursor=0
case "$PRESET" in
all) need_openai=1; need_anthropic=1; need_cursor=1 ;;
codex) need_openai=1 ;;
opencode) need_openai=1 ;;
claude) need_anthropic=1 ;;
cursor) need_cursor=1 ;;
*) echo "Unknown providers_preset: $PRESET" >&2; exit 2 ;;
esac
if [ "$need_openai" = "1" ] && [ -z "${OPENAI_API_KEY:-}" ] && [ -z "${CODEX_API_KEY:-}" ]; then
echo "Missing provider secrets: set OPENAI_API_KEY (or CODEX_API_KEY) to run preset=$PRESET" >&2
exit 1
fi
if [ "$need_anthropic" = "1" ] && [ -z "${ANTHROPIC_API_KEY:-}" ]; then
echo "Missing provider secrets: set ANTHROPIC_API_KEY to run preset=$PRESET" >&2
exit 1
fi
if [ "$need_cursor" = "1" ] && [ -z "${CURSOR_API_KEY:-}" ]; then
echo "Missing provider secrets: set CURSOR_API_KEY to run preset=$PRESET" >&2
exit 1
fi
- name: Run provider contract suite
env:
CI: "1"
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
HAPPIER_E2E_PROVIDER_MODEL: ${{ fromJSON(inputs.providers_models).default }}
HAPPY_E2E_PROVIDER_MODEL: ${{ fromJSON(inputs.providers_models).default }}
HAPPIER_E2E_PROVIDER_MODELS: ${{ fromJSON(inputs.providers_models).overrides }}
HAPPY_E2E_PROVIDER_MODELS: ${{ fromJSON(inputs.providers_models).overrides }}
run: |
set -euo pipefail
flags=()
if [ "${{ fromJSON(inputs.providers_flags).strict_keys }}" = "true" ]; then flags+=("--strict-keys"); fi
if [ "${{ fromJSON(inputs.providers_flags).flake_retry }}" != "true" ]; then flags+=("--no-flake-retry"); fi
if [ "${{ fromJSON(inputs.providers_flags).update_baselines }}" = "true" ]; then flags+=("--update-baselines"); fi
yarn workspace @happier-dev/tests providers:run "${{ inputs.providers_preset }}" "${{ inputs.providers_tier }}" "${flags[@]}"
- name: Upload provider artifacts (on failure)
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: providers-artifacts
path: .project/logs/e2e
stress:
if: ${{ inputs.run_stress }}
name: Stress (repeat/chaos)
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 22.x
cache: yarn
cache-dependency-path: yarn.lock
- name: Enable Corepack
uses: ./.github/actions/enable-corepack-yarn
- name: Install dependencies
env:
YARN_PRODUCTION: "false"
npm_config_production: "false"
uses: ./.github/actions/install-yarn-dependencies
with:
args: --frozen-lockfile --ignore-engines
- name: Run stress suite
env:
CI: "1"
HAPPIER_E2E_REPEAT: ${{ fromJSON(inputs.stress_config).repeat }}
HAPPIER_E2E_SEED: ${{ fromJSON(inputs.stress_config).seed }}
HAPPIER_E2E_FLAKE_RETRY: "1"
run: yarn test:stress
- name: Upload stress artifacts (on failure)
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: stress-artifacts
path: .project/logs/e2e
ci_summary:
name: CI result summary (all lanes)
if: always()
needs: [ci_plan, ui-e2e, mobile-e2e-android, mobile-e2e-ios, ui-unit, ui-integration, ui, shared-packages-unit, server, server-db-contract, cli, stack, release-contracts, installers-smoke-linux, installers-smoke-macos, installers-smoke-windows, binary-smoke, cli-update-continuity, daemon-continuity, session-continuity, release-assets-docker, self-host-systemd-e2e, self-host-launchd-e2e, self-host-schtasks-e2e, self-host-daemon-e2e, typecheck, cli-daemon-e2e, e2e-core, e2e-core-slow, trusted_ref_guard, release_actor_guard, providers, stress]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Collect every lane conclusion
env:
NEEDS_JSON: ${{ toJSON(needs) }}
SELECT_JOBS_EXPLICITLY: ${{ inputs.select_jobs_explicitly }}
REQUEST_RUN_UI_E2E: ${{ inputs.run_ui_e2e }}
REQUEST_RUN_MOBILE_E2E_ANDROID: ${{ inputs.run_mobile_e2e_android }}
REQUEST_RUN_MOBILE_E2E_IOS: ${{ inputs.run_mobile_e2e_ios }}
REQUEST_RUN_UI: ${{ inputs.run_ui }}
REQUEST_RUN_SERVER: ${{ inputs.run_server }}
REQUEST_RUN_SERVER_DB_CONTRACT: ${{ inputs.run_server_db_contract }}
REQUEST_RUN_CLI: ${{ inputs.run_cli }}
REQUEST_RUN_STACK: ${{ inputs.run_stack }}
REQUEST_RUN_RELEASE_CONTRACTS: ${{ inputs.run_release_contracts }}
REQUEST_RUN_INSTALLERS_SMOKE: ${{ inputs.run_installers_smoke }}
REQUEST_RUN_BINARY_SMOKE: ${{ inputs.run_binary_smoke }}
REQUEST_RUN_CLI_UPDATE_CONTINUITY: ${{ inputs.run_cli_update_continuity }}
REQUEST_RUN_DAEMON_CONTINUITY: ${{ inputs.run_daemon_continuity }}
REQUEST_RUN_SESSION_CONTINUITY: ${{ inputs.run_session_continuity }}
REQUEST_RUN_RELEASE_ASSETS_DOCKER: ${{ inputs.run_release_assets_docker }}
REQUEST_RUN_SELF_HOST_SYSTEMD: ${{ inputs.run_self_host_systemd }}
REQUEST_RUN_SELF_HOST_LAUNCHD: ${{ inputs.run_self_host_launchd }}
REQUEST_RUN_SELF_HOST_SCHTASKS: ${{ inputs.run_self_host_schtasks }}
REQUEST_RUN_SELF_HOST_DAEMON: ${{ inputs.run_self_host_daemon }}
REQUEST_RUN_TYPECHECK: ${{ inputs.run_typecheck }}
REQUEST_RUN_CLI_DAEMON_E2E: ${{ inputs.run_cli_daemon_e2e }}
REQUEST_RUN_E2E_CORE: ${{ inputs.run_e2e_core }}
REQUEST_RUN_E2E_CORE_SLOW: ${{ inputs.run_e2e_core_slow }}
REQUEST_RUN_PROVIDERS: ${{ inputs.run_providers }}
REQUEST_RUN_STRESS: ${{ inputs.run_stress }}
CI_RUN_ID: ${{ github.run_id }}
CI_SOURCE_SHA: ${{ inputs.checkout_sha != '' && inputs.checkout_sha || github.sha }}
CI_WORKFLOW: ${{ github.workflow }}
run: |
node --input-type=module <<'NODE'
import { writeFileSync } from 'node:fs';
const needs = JSON.parse(process.env.NEEDS_JSON);
const lanes = Object.entries(needs).map(([id, value]) => ({
id,
result: value?.result ?? null,
conclusion: value?.conclusion ?? null,
outputs: value?.outputs ?? {},
}));
const requestedLaneGroups = {
REQUEST_RUN_UI_E2E: ['ui-e2e'],
REQUEST_RUN_MOBILE_E2E_ANDROID: ['mobile-e2e-android'],
REQUEST_RUN_MOBILE_E2E_IOS: ['mobile-e2e-ios'],
REQUEST_RUN_UI: ['ui-unit', 'ui-integration', 'ui', 'shared-packages-unit'],
REQUEST_RUN_SERVER: ['server'],
REQUEST_RUN_SERVER_DB_CONTRACT: ['server-db-contract'],
REQUEST_RUN_CLI: ['cli'],
REQUEST_RUN_STACK: ['stack'],
REQUEST_RUN_RELEASE_CONTRACTS: ['release-contracts'],
REQUEST_RUN_INSTALLERS_SMOKE: ['installers-smoke-linux', 'installers-smoke-macos', 'installers-smoke-windows'],
REQUEST_RUN_BINARY_SMOKE: ['binary-smoke'],
REQUEST_RUN_CLI_UPDATE_CONTINUITY: ['cli-update-continuity'],
REQUEST_RUN_DAEMON_CONTINUITY: ['daemon-continuity'],
REQUEST_RUN_SESSION_CONTINUITY: ['session-continuity'],
REQUEST_RUN_RELEASE_ASSETS_DOCKER: ['release-assets-docker'],
REQUEST_RUN_SELF_HOST_SYSTEMD: ['self-host-systemd-e2e'],
REQUEST_RUN_SELF_HOST_LAUNCHD: ['self-host-launchd-e2e'],
REQUEST_RUN_SELF_HOST_SCHTASKS: ['self-host-schtasks-e2e'],
REQUEST_RUN_SELF_HOST_DAEMON: ['self-host-daemon-e2e'],
REQUEST_RUN_TYPECHECK: ['typecheck'],
REQUEST_RUN_CLI_DAEMON_E2E: ['cli-daemon-e2e'],
REQUEST_RUN_E2E_CORE: ['e2e-core'],
REQUEST_RUN_E2E_CORE_SLOW: ['e2e-core-slow'],
REQUEST_RUN_PROVIDERS: ['release_actor_guard', 'providers'],
REQUEST_RUN_STRESS: ['stress'],
};
const classifierLaneGroups = {
run_ui: ['ui-unit', 'ui-integration', 'ui'],
run_shared_packages: ['shared-packages-unit'],
run_server: ['server'],
run_cli: ['cli'],
run_stack: ['stack'],
run_ui_e2e: ['ui-e2e'],
run_server_db_contract: ['server-db-contract'],
run_release_contracts: ['release-contracts'],
run_installers_smoke: ['installers-smoke-linux', 'installers-smoke-macos', 'installers-smoke-windows'],
run_binary_smoke: ['binary-smoke'],
run_cli_daemon_e2e: ['cli-daemon-e2e'],
run_e2e_core: ['e2e-core'],
run_typecheck: ['typecheck'],
};
const explicitSelection = process.env.SELECT_JOBS_EXPLICITLY === 'true';
const selectedLaneIds = explicitSelection
? Object.entries(requestedLaneGroups)
.filter(([input]) => process.env[input] === 'true')
.flatMap(([, ids]) => ids)
: Object.entries(classifierLaneGroups)
.filter(([output]) => needs.ci_plan?.outputs?.[output] === 'true')
.flatMap(([, ids]) => ids);
const requestedLaneIds = [
'ci_plan',
'trusted_ref_guard',
...selectedLaneIds,
];
const laneById = new Map(lanes.map((lane) => [lane.id, lane]));
const requestedSkips = requestedLaneIds
.filter((id) => !laneById.has(id) || laneById.get(id)?.result === 'skipped')
.map((id) => ({
...(laneById.get(id) ?? { id, result: null, conclusion: null, outputs: {} }),
reason: 'requested lane was skipped',
}));
// Only success and intentionally skipped lanes are acceptable. Treat every other or
// unknown result (timed_out, startup_failure, stale, action_required, null, ...) as a
// failure so the collector can never report a false-green release attestation.
const failures = [
...lanes.filter(({ result }) => result !== 'success' && result !== 'skipped'),
...requestedSkips,
];
for (const lane of lanes) console.log(`${lane.id} => ${lane.result ?? 'unknown'}`);
const summary = {
schemaVersion: 1,
runId: process.env.CI_RUN_ID ?? null,
sourceSha: process.env.CI_SOURCE_SHA ?? null,
workflow: process.env.CI_WORKFLOW ?? null,
lanes,
failures,
};
writeFileSync('ci-summary.json', `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
if (failures.length > 0) {
console.error('Non-success CI lanes:');
for (const lane of failures) {
console.error(`- ${lane.id} => ${lane.result ?? 'unknown'}${lane.reason ? ` (${lane.reason})` : ''}`);
}
process.exitCode = 1;
}
NODE
- name: Upload machine-readable CI summary
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ci-lane-summary
path: ci-summary.json
if-no-files-found: error