fix(ci): stabilize UI harnesses and Corepack setup #1912
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |