Skip to content

TAN-831: persist credential lifecycle revisions and distinguish OAuth refresh #1282

TAN-831: persist credential lifecycle revisions and distinguish OAuth refresh

TAN-831: persist credential lifecycle revisions and distinguish OAuth refresh #1282

Workflow file for this run

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