TAN-831: persist credential lifecycle revisions and distinguish OAuth refresh #1282
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: Engine CI | |
| on: | |
| workflow_dispatch: | |
| pull_request: | |
| paths: | |
| - "engine/**" | |
| - "crates/**" | |
| - "Cargo.toml" | |
| - "Cargo.lock" | |
| - ".config/nextest.toml" | |
| - ".github/actions/setup-rust-ci/**" | |
| - ".github/workflows/engine-ci.yml" | |
| - "scripts/build-linux-release-engine.sh" | |
| - "scripts/linux-release-builder.Dockerfile" | |
| - "packages/tandem-control-panel/docker/engine.Dockerfile" | |
| - "scripts/bump-version.sh" | |
| - "scripts/ci-engine-scope.sh" | |
| - "scripts/ci-engine-pin-scope.mjs" | |
| - "scripts/ci-engine-pin-scope.test.mjs" | |
| - "scripts/verify-container-hardening.mjs" | |
| push: | |
| branches: ["feat/engine"] | |
| paths: | |
| - "engine/**" | |
| - "crates/**" | |
| - "Cargo.toml" | |
| - "Cargo.lock" | |
| - ".config/nextest.toml" | |
| - ".github/actions/setup-rust-ci/**" | |
| - ".github/workflows/engine-ci.yml" | |
| - "scripts/build-linux-release-engine.sh" | |
| - "scripts/linux-release-builder.Dockerfile" | |
| - "packages/tandem-control-panel/docker/engine.Dockerfile" | |
| - "scripts/bump-version.sh" | |
| - "scripts/ci-engine-scope.sh" | |
| - "scripts/ci-engine-pin-scope.mjs" | |
| - "scripts/ci-engine-pin-scope.test.mjs" | |
| - "scripts/verify-container-hardening.mjs" | |
| # Superseded PR commits share a group; pushes, schedules and manual runs are independent. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| jobs: | |
| classify-changes: | |
| name: Classify Engine Changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| run-engine-jobs: ${{ steps.scope.outputs.run-engine-jobs }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Determine engine CI scope | |
| id: scope | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| PUSH_BEFORE_SHA: ${{ github.event.before }} | |
| CURRENT_SHA: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then | |
| echo "run-engine-jobs=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [[ "$EVENT_NAME" == "pull_request" ]]; then | |
| changed_files="$(git diff --name-only "$PR_BASE_SHA" "$PR_HEAD_SHA")" | |
| elif [[ "$PUSH_BEFORE_SHA" =~ ^0+$ ]]; then | |
| changed_files="$(git diff-tree --no-commit-id --name-only -r "$CURRENT_SHA")" | |
| else | |
| changed_files="$(git diff --name-only "$PUSH_BEFORE_SHA" "$CURRENT_SHA")" | |
| fi | |
| printf '%s\n' "$changed_files" | |
| run_engine_jobs="$(printf '%s\n' "$changed_files" | bash scripts/ci-engine-scope.sh)" | |
| echo "run-engine-jobs=$run_engine_jobs" >> "$GITHUB_OUTPUT" | |
| engine-checks: | |
| needs: classify-changes | |
| if: needs.classify-changes.outputs.run-engine-jobs == 'true' | |
| strategy: | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - name: Setup shared Rust CI | |
| uses: ./.github/actions/setup-rust-ci | |
| with: | |
| cache-profile: engine-minimal | |
| components: rustfmt, clippy | |
| - name: Setup Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: 20 | |
| - name: Cargo check | |
| run: cargo check -p tandem-ai --no-default-features | |
| - name: Cargo test | |
| run: cargo test -p tandem-ai --no-default-features | |
| # Tenant-isolation and storage suites that previously never ran in CI. | |
| # The tandem-memory suite was silently dead on main (broken include!) | |
| # until 2026-06; this step keeps these crates from going dark again. | |
| - name: Cargo test governance-critical crates | |
| if: matrix.os == 'ubuntu-latest' | |
| run: | | |
| cargo test -p tandem-tools --lib | |
| cargo test -p tandem-memory --lib | |
| cargo test -p tandem-runtime --lib | |
| cargo test -p tandem-core --lib provider_auth_store:: | |
| - name: Cargo clippy | |
| if: matrix.os == 'ubuntu-latest' | |
| run: cargo clippy -p tandem-ai --no-default-features -- -D warnings | |
| - name: Cargo fmt | |
| if: matrix.os == 'ubuntu-latest' | |
| run: cargo fmt --check | |
| browser-sidecar-check: | |
| name: Browser Sidecar Check | |
| needs: classify-changes | |
| if: needs.classify-changes.outputs.run-engine-jobs == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - name: Setup shared Rust CI | |
| uses: ./.github/actions/setup-rust-ci | |
| with: | |
| cache-profile: engine-browser | |
| - name: Check engine with browser feature | |
| run: cargo check -p tandem-ai --features browser | |
| - name: Check server with browser feature | |
| run: cargo check -p tandem-server --features browser --lib | |
| - name: Assert browser server governance wiring | |
| run: | | |
| cargo test -p tandem-server --features browser server_context_installs_deny_capable_policy_and_real_ledger --lib | |
| cargo test -p tandem-server --features browser governance_routes_fail_closed_without_premium_governance --lib | |
| - name: Check browser sidecar | |
| run: cargo check -p tandem-browser | |
| - name: Test browser sidecar | |
| run: cargo test -p tandem-browser | |
| # The pinned standard + enterprise release build now runs once in Security | |
| # Assurance; its immutable standard candidate is also used by Container engine. | |
| # TAN-227: one-command end-to-end health check over the governed runtime | |
| # path (session prompt round-trip, approval gate, policy denial + audit, | |
| # memory round-trip) against an isolated in-process server with the local | |
| # echo provider — no network access or API keys. | |
| engine-smoke: | |
| name: Runtime Smoke Test (tandem-engine smoke) | |
| needs: classify-changes | |
| if: needs.classify-changes.outputs.run-engine-jobs == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - name: Setup shared Rust CI | |
| uses: ./.github/actions/setup-rust-ci | |
| with: | |
| cache-profile: engine-default | |
| - name: Build tandem-engine | |
| run: cargo build -p tandem-ai --bin tandem-engine | |
| - name: Run runtime smoke test (in-process) | |
| run: ./target/debug/tandem-engine smoke --json --timeout-secs 120 | |
| server-runtime-gates: | |
| name: Server Runtime Fast Gates | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| env: | |
| CARGO_PROFILE_DEV_DEBUG: "line-tables-only" | |
| CARGO_PROFILE_TEST_DEBUG: "line-tables-only" | |
| RUST_MIN_STACK: "16777216" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - name: Setup shared Rust CI | |
| uses: ./.github/actions/setup-rust-ci | |
| with: | |
| cache-profile: workspace-browser-premium | |
| - name: Build tandem-server test binary once | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cargo test -p tandem-server --features premium-governance \ | |
| --lib --tests --no-run --message-format=json > tandem-server-test-messages.json | |
| python - <<'PY' | |
| import json | |
| import os | |
| import pathlib | |
| exe = None | |
| for line in pathlib.Path("tandem-server-test-messages.json").read_text().splitlines(): | |
| try: | |
| msg = json.loads(line) | |
| except json.JSONDecodeError: | |
| continue | |
| if msg.get("reason") != "compiler-artifact": | |
| continue | |
| if not msg.get("profile", {}).get("test"): | |
| continue | |
| target = msg.get("target", {}) | |
| if target.get("name") == "tandem_server" and msg.get("executable"): | |
| exe = msg["executable"] | |
| if not exe: | |
| raise SystemExit("no tandem_server test binary found") | |
| with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as fh: | |
| fh.write(f"TEST_BIN={exe}\n") | |
| PY | |
| - name: Run workflow invariant subset | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| "$TEST_BIN" 'app::state::tests::automations::recover_in_flight_runs_does_not_relock_workspace_for_paused_runs' --exact --nocapture | |
| "$TEST_BIN" 'http::tests::global::automation_v2_run_projects_backlog_tasks_into_context_blackboard' --exact --nocapture | |
| "$TEST_BIN" 'app::state::tests::automations::workflow_policy::mcp_grounded_citations_artifact_passes_without_local_reads_or_websearch' --exact --nocapture | |
| "$TEST_BIN" 'app::state::tests::automations::workflow_policy::materialized_current_attempt_output_does_not_report_missing_output_requirement' --exact --nocapture | |
| "$TEST_BIN" 'app::state::tests::server_context_installs_deny_capable_policy_and_real_ledger' --exact --nocapture | |
| # TAN-214: golden email approval workflow (compose -> gate -> send) | |
| "$TEST_BIN" 'app::state::tests::automations::email_approval_golden' --nocapture | |
| # TAN-574: module filters catch additions to these suites without | |
| # extending the hand-picked workflow invariant list. | |
| - name: Run stateful runtime suite | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| "$TEST_BIN" stateful_runtime --nocapture | |
| "$TEST_BIN" automation_webhook_stateful --nocapture | |
| workspace-tests: | |
| name: Workspace Tests (nextest) | |
| runs-on: ubuntu-latest | |
| needs: | |
| - server-runtime-gates | |
| timeout-minutes: 60 | |
| env: | |
| # The full-workspace debug target tree is ~21 GB with default debuginfo, | |
| # uncomfortably close to a hosted runner's free disk. Line tables keep | |
| # panic locations in backtraces at a fraction of the size & link memory. | |
| CARGO_PROFILE_DEV_DEBUG: "line-tables-only" | |
| CARGO_PROFILE_TEST_DEBUG: "line-tables-only" | |
| # Unoptimized debug builds produce very large nested futures for the | |
| # deepest coder/task-runtime handler chains; the default 2 MiB test | |
| # thread stack overflows on them (e.g. | |
| # coder_issue_triage_execute_next_drives_task_runtime_to_completion). | |
| # 16 MiB keeps those tests running instead of aborting with SIGABRT. | |
| RUST_MIN_STACK: "16777216" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| # Hosted runners ship ~25 GB of preinstalled toolchains we never use; | |
| # reclaim them so the workspace build fits. | |
| - name: Free runner disk space | |
| run: | | |
| df -h / | |
| sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ | |
| /usr/local/.ghcup /opt/hostedtoolcache/CodeQL | |
| sudo docker image prune --all --force >/dev/null 2>&1 || true | |
| df -h / | |
| - name: Setup shared Rust CI | |
| uses: ./.github/actions/setup-rust-ci | |
| with: | |
| cache-profile: workspace-browser-premium | |
| - name: Install cargo-nextest | |
| uses: taiki-e/install-action@10ddf82bb4948219b68187f154decde56c89ee88 # nextest | |
| # Replaces the hand-picked deep-gate test list (TAN-220): every test in | |
| # the workspace now guards PRs instead of a curated subset that rots. | |
| # The desktop crate (package `tandem`) is excluded — it needs Tauri | |
| # system deps and has its own job in ci.yml. premium-governance is | |
| # enabled so the policy-engine test suite runs in CI for the first time. | |
| - name: Run workspace test suite | |
| env: | |
| # Isolate canonical data paths for the whole run: tests that don't | |
| # set their own TANDEM_HOME would otherwise write to the runner's | |
| # real data dir and collide across test processes (TAN-619). | |
| TANDEM_HOME: ${{ runner.temp }}/tandem-test-home | |
| run: | | |
| cargo nextest run --workspace --exclude tandem \ | |
| --features tandem-ai/browser,tandem-server/premium-governance \ | |
| --profile ci | |
| # Listing after the suite reuses the compiled test binaries. This makes | |
| # migration/orchestration discovery auditable without another Rust build | |
| # or a second execution of the same tests. | |
| - name: Verify migration and orchestration test discovery | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cargo nextest list --workspace --exclude tandem \ | |
| --features tandem-ai/browser,tandem-server/premium-governance \ | |
| --profile ci --message-format json > "$RUNNER_TEMP/nextest-tests.json" | |
| grep -Fq 'migration_write_failures_roll_back_every_imported_record_type' \ | |
| "$RUNNER_TEMP/nextest-tests.json" | |
| grep -Fq 'completed_migration_keeps_sqlite_authoritative_when_legacy_files_change' \ | |
| "$RUNNER_TEMP/nextest-tests.json" | |
| grep -Fq 'tan_707_goal_plan_execute_verify_complete_survives_180_day_store_journey' \ | |
| "$RUNNER_TEMP/nextest-tests.json" | |
| grep -Fq 'tan_707_named_replan_edge_and_day_180_limit_are_explicit' \ | |
| "$RUNNER_TEMP/nextest-tests.json" | |
| - name: Verify focused security tests were discovered and executed | |
| run: | | |
| node scripts/verify-security-retest-matrix.mjs \ | |
| --nextest-list "$RUNNER_TEMP/nextest-tests.json" \ | |
| --junit target/nextest/ci/junit.xml | |
| - name: Verify governance audit tests were discovered and executed | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python - <<'PY' | |
| import pathlib | |
| import xml.etree.ElementTree as ET | |
| manifest = [ | |
| line.strip() | |
| for line in pathlib.Path('.github/governance-audit-critical-tests.txt').read_text().splitlines() | |
| if line.strip() and not line.lstrip().startswith('#') | |
| ] | |
| discovered = pathlib.Path("${{ runner.temp }}/nextest-tests.json").read_text() | |
| junit_path = pathlib.Path('target/nextest/ci/junit.xml') | |
| if not junit_path.exists(): | |
| raise SystemExit('nextest junit report is missing') | |
| root = ET.parse(junit_path).getroot() | |
| executed = '\n'.join( | |
| f"{case.attrib.get('classname', '')}::{case.attrib.get('name', '')}" | |
| for case in root.iter('testcase') | |
| if case.find('skipped') is None | |
| ) | |
| missing_discovery = [name for name in manifest if name not in discovered] | |
| missing_execution = [name for name in manifest if name not in executed] | |
| if missing_discovery or missing_execution: | |
| raise SystemExit( | |
| f"governance audit coverage gap; missing discovery={missing_discovery}, " | |
| f"missing execution={missing_execution}" | |
| ) | |
| summary = pathlib.Path("${{ runner.temp }}/governance-coverage.md") | |
| summary.write_text( | |
| '# Governance mode coverage\n\n' | |
| '- premium-governance: all audit-critical tests discovered and executed\n' | |
| '- local/default: fail-closed governance route test executed in Browser Sidecar Check\n' | |
| ) | |
| PY | |
| - name: Reject allow-all policy outside isolated guard tests | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| matches="$(rg -n 'AllowAllToolDispatchPolicy' --glob '*.rs' \ | |
| | grep -v '^crates/tandem-tools/src/tool_dispatcher.rs:' \ | |
| | grep -v '^crates/tandem-server/src/app/state/tests/mod.rs:' || true)" | |
| test -z "$matches" || { echo "$matches"; exit 1; } | |
| direct_mcp_calls="$(rg -n -U '\.mcp\s*\.\s*call_tool' \ | |
| crates/tandem-server/src/http/coder_parts/part05.rs \ | |
| crates/tandem-server/src/pack_builder_parts \ | |
| crates/tandem-server/src/benchmarking/mod.rs \ | |
| crates/tandem-server/src/incident_monitor_github.rs \ | |
| crates/tandem-server/src/incident_monitor_linear.rs \ | |
| crates/tandem-server/src/incident_monitor_mcp.rs \ | |
| crates/tandem-server/src/incident_monitor_webhook.rs || true)" | |
| test -z "$direct_mcp_calls" || { echo "$direct_mcp_calls"; exit 1; } | |
| - name: Report disk usage | |
| if: always() | |
| run: df -h / | |
| - name: Upload junit report | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: nextest-junit | |
| path: | | |
| target/nextest/ci/junit.xml | |
| ${{ runner.temp }}/nextest-tests.json | |
| ${{ runner.temp }}/governance-coverage.md | |
| if-no-files-found: ignore | |
| retention-days: 14 |