From 74ff0e4a2c5c9747302e57385908f185f7b2f8a2 Mon Sep 17 00:00:00 2001 From: ranade-oss Date: Tue, 4 Aug 2026 16:50:57 -0400 Subject: [PATCH] Consolidate ROSS automation workflows --- .github/actionlint.yaml | 3 - .github/workflows/agent-pr-reconciler.yml | 285 +++++++++++++++ .../apply-upstream-sync-batch-size.yml | 60 ---- .github/workflows/baseline.yml | 2 + .../workflows/coordinate-upstream-mike.yml | 37 ++ .github/workflows/deploy-public-beta-ross.yml | 334 ------------------ .../dispatch-escalated-mike-on-request.yml | 34 -- .../dispatch-unverified-agent-heads.yml | 148 -------- .../final-controlled-beta-evidence.yml | 57 --- ...aseline.yml => handle-baseline-result.yml} | 165 ++++++++- .github/workflows/merge-verified-agent-pr.yml | 166 --------- .../reconcile-verified-agent-merges.yml | 145 -------- .github/workflows/release-candidate.yml | 84 ----- .../run-all-upstream-mike-synchronizers.yml | 32 -- .../sync-upstream-mike-escalated.yml | 3 + .github/workflows/sync-upstream-mike.yml | 5 +- .../verify-and-deploy-public-beta.yml | 15 + CONTRIBUTING.md | 2 +- config/final-completion.v1.json | 2 +- config/release-manifest.v1.json | 11 +- .../deployment/public-beta-combined-update.md | 5 +- docs/final/final-release-procedure.md | 10 +- docs/final/owner-action-sheet.md | 8 +- docs/operations/release-runbook.md | 14 +- docs/public-beta-deployment.md | 13 +- docs/releases/release-completion-runbook.md | 20 +- docs/ross-170-verification.md | 2 +- reports/final-completion-dossier.md | 2 +- reports/release-manifest-v1.json | 49 +-- .../dispatch-unverified-agent-heads.test.mjs | 10 +- tests/baseline/private-deployment.test.mjs | 13 +- .../ross-automation-consolidation.test.mjs | 63 ++++ tests/baseline/ross-ci-toolchain.test.mjs | 12 +- tests/baseline/ross-deliverable-f.test.mjs | 10 +- tests/baseline/ross-deliverable-g.test.mjs | 17 +- ...ross-full-catalogue-public-update.test.mjs | 11 +- tests/baseline/ross-hosted-runtime.test.mjs | 2 +- .../ross-production-readiness.test.mjs | 10 +- .../supabase-upload-scan-pipeline.test.mjs | 17 +- website/app/page-content.ts | 4 +- 40 files changed, 683 insertions(+), 1199 deletions(-) create mode 100644 .github/workflows/agent-pr-reconciler.yml delete mode 100644 .github/workflows/apply-upstream-sync-batch-size.yml create mode 100644 .github/workflows/coordinate-upstream-mike.yml delete mode 100644 .github/workflows/deploy-public-beta-ross.yml delete mode 100644 .github/workflows/dispatch-escalated-mike-on-request.yml delete mode 100644 .github/workflows/dispatch-unverified-agent-heads.yml delete mode 100644 .github/workflows/final-controlled-beta-evidence.yml rename .github/workflows/{repair-failed-baseline.yml => handle-baseline-result.yml} (68%) delete mode 100644 .github/workflows/merge-verified-agent-pr.yml delete mode 100644 .github/workflows/reconcile-verified-agent-merges.yml delete mode 100644 .github/workflows/release-candidate.yml delete mode 100644 .github/workflows/run-all-upstream-mike-synchronizers.yml create mode 100644 tests/baseline/ross-automation-consolidation.test.mjs diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 6f69372c3a..f6a329441b 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -1,7 +1,4 @@ paths: - .github/workflows/deploy-public-beta-ross.yml: - ignore: - - 'constant expression "false" in condition' .github/workflows/staging-debug-release-train.yml: ignore: - 'shellcheck reported issue in this script: SC2016:.+' diff --git a/.github/workflows/agent-pr-reconciler.yml b/.github/workflows/agent-pr-reconciler.yml new file mode 100644 index 0000000000..e04d019db1 --- /dev/null +++ b/.github/workflows/agent-pr-reconciler.yml @@ -0,0 +1,285 @@ +name: Reconcile agent pull requests + +on: + schedule: + - cron: "0 * * * *" + workflow_dispatch: + push: + branches: [main] + +permissions: + actions: read + contents: read + issues: write + pull-requests: write + +concurrency: + group: reconcile-agent-pull-requests + cancel-in-progress: false + +jobs: + dispatch: + permissions: + actions: write + contents: read + issues: write + pull-requests: read + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Dispatch Baseline for unverified exact heads + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 + with: + script: | + const { owner, repo } = context.repo; + const trustedAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]); + + const { data: workflow } = await github.rest.actions.getWorkflow({ + owner, + repo, + workflow_id: "baseline.yml", + }); + + const reportFailure = async (pr, run) => { + const marker = ``; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: pr.number, + per_page: 100, + }); + if (comments.some((comment) => comment.body?.includes(marker))) return; + + const jobs = await github.paginate( + github.rest.actions.listJobsForWorkflowRun, + { + owner, + repo, + run_id: run.id, + per_page: 100, + }, + ); + const failures = jobs + .filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped") + .map((job) => { + const steps = (job.steps || []) + .filter((step) => step.conclusion === "failure") + .map((step) => step.name); + return `- **${job.name}** (${job.conclusion})${steps.length ? ` — ${steps.join(", ")}` : ""}`; + }); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr.number, + body: [ + marker, + `Baseline failed for exact head \`${pr.head.sha}\`.`, + "", + failures.length ? failures.join("\n") : `- Workflow conclusion: **${run.conclusion}**`, + "", + `[Open workflow run](${run.html_url})`, + "", + "Bounded automatic repair runs only for eligible low-risk paths. Protected workflow, dependency, migration, deployment, security, legal, governance, or release changes require focused review.", + ].join("\n"), + }); + }; + + const pullRequests = await github.paginate(github.rest.pulls.list, { + owner, + repo, + state: "open", + base: "main", + per_page: 100, + }); + + for (const pr of pullRequests) { + const markedSyncBot = + pr.user?.login === "github-actions[bot]" && + pr.head.ref.startsWith("agent/upstream-sync-") && + pr.body?.includes("Automated-Upstream-Mike-Sync: true"); + const eligible = + !pr.draft && + pr.head.repo?.full_name === `${owner}/${repo}` && + pr.head.ref.startsWith("agent/") && + (trustedAssociations.has(pr.author_association) || markedSyncBot); + + if (!eligible) continue; + + const runs = await github.paginate( + github.rest.actions.listWorkflowRuns, + { + owner, + repo, + workflow_id: workflow.id, + branch: pr.head.ref, + per_page: 100, + }, + ); + + const exactHeadRun = runs.find( + (run) => + run.head_sha === pr.head.sha && + !( + run.status === "completed" && + run.conclusion === "action_required" + ), + ); + if (exactHeadRun) { + core.info( + `PR #${pr.number} already has Baseline run ${exactHeadRun.id} for ${pr.head.sha} (${exactHeadRun.status}/${exactHeadRun.conclusion || "none"}).`, + ); + if ( + exactHeadRun.status === "completed" && + exactHeadRun.conclusion && + exactHeadRun.conclusion !== "success" && + exactHeadRun.conclusion !== "skipped" && + exactHeadRun.conclusion !== "neutral" + ) { + await reportFailure(pr, exactHeadRun); + } + continue; + } + + await github.rest.actions.createWorkflowDispatch({ + owner, + repo, + workflow_id: workflow.id, + ref: pr.head.ref, + }); + core.notice( + `Dispatched Baseline for PR #${pr.number} exact head ${pr.head.sha}.`, + ); + } + reconcile: + permissions: + actions: read + contents: write + pull-requests: write + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Merge eligible exact-head verified pull requests + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 + with: + script: | + const { owner, repo } = context.repo; + const trustedAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]); + + const pulls = await github.paginate(github.rest.pulls.list, { + owner, + repo, + state: "open", + base: "main", + per_page: 100, + }); + + const candidates = pulls.filter((pr) => { + const markedSyncBot = + pr.user?.login === "github-actions[bot]" && + pr.head.ref.startsWith("agent/upstream-sync-") && + pr.body?.includes("Automated-Upstream-Mike-Sync: true"); + return ( + !pr.draft && + pr.head.repo?.full_name === `${owner}/${repo}` && + pr.head.ref.startsWith("agent/") && + (trustedAssociations.has(pr.author_association) || markedSyncBot) + ); + }); + + const gateQuery = ` + query($owner: String!, $repo: String!, $number: Int!, $after: String) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + state + isDraft + headRefOid + reviewDecision + mergeable + reviewThreads(first: 100, after: $after) { + nodes { isResolved } + pageInfo { hasNextPage endCursor } + } + } + } + } + `; + + const readGate = async (number) => { + let after = null; + let pullRequest = null; + let unresolved = false; + do { + const result = await github.graphql(gateQuery, { + owner, + repo, + number, + after, + }); + pullRequest = result.repository.pullRequest; + unresolved ||= pullRequest.reviewThreads.nodes.some( + (thread) => !thread.isResolved, + ); + after = pullRequest.reviewThreads.pageInfo.hasNextPage + ? pullRequest.reviewThreads.pageInfo.endCursor + : null; + } while (after); + return { pullRequest, unresolved }; + }; + + for (const pr of candidates) { + const runs = await github.paginate( + github.rest.actions.listWorkflowRunsForRepo, + { + owner, + repo, + branch: pr.head.ref, + status: "completed", + per_page: 100, + }, + ); + const verified = runs.some( + (run) => + run.name === "Baseline verification" && + run.conclusion === "success" && + run.head_sha === pr.head.sha && + (run.event === "pull_request" || run.event === "workflow_dispatch"), + ); + if (!verified) { + core.info(`PR #${pr.number} has no successful exact-head Baseline.`); + continue; + } + + let gate; + for (let attempt = 1; attempt <= 6; attempt += 1) { + gate = await readGate(pr.number); + if (gate.pullRequest.mergeable !== "UNKNOWN") break; + if (attempt < 6) await new Promise((resolve) => setTimeout(resolve, 10000)); + } + + const node = gate.pullRequest; + const blocked = + node.state !== "OPEN" || + node.isDraft || + node.headRefOid !== pr.head.sha || + node.reviewDecision === "CHANGES_REQUESTED" || + node.mergeable !== "MERGEABLE" || + gate.unresolved; + if (blocked) { + core.info(`PR #${pr.number} still has a review, head, or mergeability blocker.`); + continue; + } + + try { + await github.rest.pulls.merge({ + owner, + repo, + pull_number: pr.number, + merge_method: "squash", + sha: pr.head.sha, + }); + core.notice(`Reconciled and merged PR #${pr.number} at verified head ${pr.head.sha}.`); + } catch (error) { + core.warning(`PR #${pr.number} was eligible but merge failed: ${error.message}`); + } + } diff --git a/.github/workflows/apply-upstream-sync-batch-size.yml b/.github/workflows/apply-upstream-sync-batch-size.yml deleted file mode 100644 index b533db2398..0000000000 --- a/.github/workflows/apply-upstream-sync-batch-size.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Apply upstream sync batch-size fix - -on: - push: - branches: [main] - paths: - - .github/workflows/apply-upstream-sync-batch-size.yml - -permissions: - contents: write - -concurrency: - group: apply-upstream-sync-batch-size - cancel-in-progress: false - -# This one-shot workflow is externally triggered because Actions-token merges do -# not recursively start push workflows. -jobs: - apply: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Check out exact main - uses: actions/checkout@v7 - with: - ref: main - fetch-depth: 0 - - - name: Reduce classification batch from twenty to eight - run: | - set -euo pipefail - python - <<'PY' - from pathlib import Path - - path = Path('.github/workflows/sync-upstream-mike.yml') - text = path.read_text(encoding='utf-8') - old = 'const batch = candidates.slice(0, 20);' - new = 'const batch = candidates.slice(0, 8);' - if text.count(old) != 1: - raise SystemExit(f'Expected exactly one batch-size target, found {text.count(old)}') - if new in text: - raise SystemExit('Eight-item batch size is already installed') - path.write_text(text.replace(old, new), encoding='utf-8') - PY - - - name: Validate the exact workflow change - uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 - - - name: Commit corrected synchronizer - run: | - set -euo pipefail - test "$(git diff --name-only | wc -l)" -eq 1 - test "$(git diff --name-only)" = '.github/workflows/sync-upstream-mike.yml' - git diff --check - git diff --exit-code -- . ':!.github/workflows/sync-upstream-mike.yml' - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add .github/workflows/sync-upstream-mike.yml - git commit -m 'Reduce upstream Mike classification batches to eight' - git push origin HEAD:main diff --git a/.github/workflows/baseline.yml b/.github/workflows/baseline.yml index bc7487ccb7..32c3fe7517 100644 --- a/.github/workflows/baseline.yml +++ b/.github/workflows/baseline.yml @@ -5,6 +5,8 @@ on: pull_request: push: branches: [main] + paths-ignore: + - reports/release-manifest-v1.json permissions: contents: read diff --git a/.github/workflows/coordinate-upstream-mike.yml b/.github/workflows/coordinate-upstream-mike.yml new file mode 100644 index 0000000000..3feb9a05c1 --- /dev/null +++ b/.github/workflows/coordinate-upstream-mike.yml @@ -0,0 +1,37 @@ +name: Coordinate upstream Mike synchronization + +on: + push: + branches: [main] + paths: + - docs/upstream-sync-request.json + workflow_dispatch: + +permissions: + actions: write + contents: read + +concurrency: + group: coordinate-upstream-mike + cancel-in-progress: false + +jobs: + coordinate: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Dispatch low-risk and escalated Mike queues + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 + with: + script: | + const { owner, repo } = context.repo; + for (const workflow_id of ["sync-upstream-mike.yml", "sync-upstream-mike-escalated.yml"]) { + await github.rest.actions.createWorkflowDispatch({ + owner, + repo, + workflow_id, + ref: "main", + }); + core.notice(`Dispatched ${workflow_id} from current main.`); + } + core.notice("Upstream Mike coordination is complete; each queue retains its own bounded classification, proposal, and merge gates."); diff --git a/.github/workflows/deploy-public-beta-ross.yml b/.github/workflows/deploy-public-beta-ross.yml deleted file mode 100644 index a57c8b0022..0000000000 --- a/.github/workflows/deploy-public-beta-ross.yml +++ /dev/null @@ -1,334 +0,0 @@ -name: "Legacy: deploy previously governed public beta" - -on: - workflow_dispatch: - inputs: - release_id: - description: Legacy governed ID using ross-public-beta-YYYYMMDD-rcN - required: true - type: string - fly_organization: - description: Fly.io organization slug shown in the Fly dashboard - required: true - default: personal - type: string - api_app_name: - description: Existing or new Fly.io name for the ROSS API - required: true - default: ross-ranadeoss-api - type: string - web_app_name: - description: Existing or new Fly.io name for the public ROSS app - required: true - default: ross-ranadeoss-public - type: string - file_worker_app_name: - description: Existing or new private Fly app for malware scanning and conversion - required: true - default: ross-ranadeoss-file-worker - type: string - confirm_auth_configuration: - description: I enabled Supabase email sign-up and Confirm email, and configured the app redirect URL - required: true - default: false - type: boolean - confirm_public_beta_approval: - description: I have the recorded go-live approval for this exact release candidate - required: true - default: false - type: boolean - confirm_upload_scan_infrastructure: - description: I applied the scan migration, made the Supabase bucket private, and configured the alert webhook - required: true - default: false - type: boolean - -permissions: - contents: read - -concurrency: - group: deploy-public-ross-beta - cancel-in-progress: false - -jobs: - blocked: - name: Legacy public deployment is disabled - runs-on: ubuntu-latest - steps: - - name: Use the ROSS release train - shell: bash - run: | - echo "This deployment path is permanently blocked. Run ROSS release train instead." >&2 - exit 1 - - preflight: - name: Validate legacy governed release before approval - if: ${{ false }} - runs-on: ubuntu-latest - timeout-minutes: 5 - env: - INPUT_RELEASE_ID: ${{ inputs.release_id }} - VERIFY_RELEASE_TAG: "true" - steps: - - name: Check out approved candidate - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Validate identifier, governed records, and immutable tag - run: | - node scripts/validate-release-id.mjs "$INPUT_RELEASE_ID" - node scripts/verify-final-release-id.mjs - - deploy: - name: Deploy verified-account ROSS beta to Toronto - needs: preflight - runs-on: ubuntu-latest - timeout-minutes: 45 - environment: public-beta - env: - FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - ROSS_SUPABASE_URL: ${{ secrets.ROSS_SUPABASE_URL }} - ROSS_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.ROSS_SUPABASE_PUBLISHABLE_KEY }} - ROSS_SUPABASE_SECRET_KEY: ${{ secrets.ROSS_SUPABASE_SECRET_KEY }} - ROSS_S3_ENDPOINT_URL: ${{ secrets.ROSS_S3_ENDPOINT_URL }} - ROSS_S3_REGION: ${{ secrets.ROSS_S3_REGION }} - ROSS_S3_ACCESS_KEY_ID: ${{ secrets.ROSS_S3_ACCESS_KEY_ID }} - ROSS_S3_SECRET_ACCESS_KEY: ${{ secrets.ROSS_S3_SECRET_ACCESS_KEY }} - ROSS_SECURITY_ALERT_WEBHOOK_URL: ${{ secrets.ROSS_SECURITY_ALERT_WEBHOOK_URL }} - ROSS_SECURITY_ALERT_WEBHOOK_SECRET: ${{ secrets.ROSS_SECURITY_ALERT_WEBHOOK_SECRET }} - API_APP: ${{ inputs.api_app_name }} - WEB_APP: ${{ inputs.web_app_name }} - WORKER_APP: ${{ inputs.file_worker_app_name }} - FLY_ORG: ${{ inputs.fly_organization }} - INPUT_RELEASE_ID: ${{ inputs.release_id }} - VERIFY_RELEASE_TAG: "true" - PUBLIC_WEBSITE_URL: https://ross-ontario.augustmaat.chatgpt.site - POLICY_VERSION: 2026-07-17-public-beta - steps: - - name: Check out approved candidate - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Validate operator confirmations - shell: bash - run: | - set -euo pipefail - if [ "${{ inputs.confirm_auth_configuration }}" != "true" ]; then - echo "Supabase email sign-up, Confirm email, and redirect configuration must be confirmed." >&2 - exit 1 - fi - if [ "${{ inputs.confirm_public_beta_approval }}" != "true" ]; then - echo "Recorded public-beta go-live approval is required." >&2 - exit 1 - fi - if [ "${{ inputs.confirm_upload_scan_infrastructure }}" != "true" ]; then - echo "Upload scan infrastructure must be applied and verified before deployment." >&2 - exit 1 - fi - - - name: Verify immutable release and final completion gate - run: | - node scripts/verify-final-release-id.mjs - npm run final:check - - - name: Validate deployment inputs and secrets - shell: bash - run: | - set -euo pipefail - for name in \ - FLY_API_TOKEN \ - ROSS_SUPABASE_URL \ - ROSS_SUPABASE_PUBLISHABLE_KEY \ - ROSS_SUPABASE_SECRET_KEY \ - ROSS_S3_ENDPOINT_URL \ - ROSS_S3_REGION \ - ROSS_S3_ACCESS_KEY_ID \ - ROSS_S3_SECRET_ACCESS_KEY \ - ROSS_SECURITY_ALERT_WEBHOOK_URL \ - ROSS_SECURITY_ALERT_WEBHOOK_SECRET; do - if [ -z "${!name:-}" ]; then - echo "Required GitHub Actions secret is missing: ${name}" >&2 - exit 1 - fi - done - for value in "$API_APP" "$WEB_APP" "$WORKER_APP"; do - if ! [[ "$value" =~ ^[a-z0-9][a-z0-9-]{2,61}[a-z0-9]$ ]]; then - echo "Fly app names must use 4-63 lowercase letters, numbers, or hyphens." >&2 - exit 1 - fi - done - if [ "$API_APP" = "$WEB_APP" ]; then - echo "The API and web app names must be different." >&2 - exit 1 - fi - if [ "$WORKER_APP" = "$API_APP" ] || [ "$WORKER_APP" = "$WEB_APP" ]; then - echo "The private file worker must use its own Fly app." >&2 - exit 1 - fi - - - uses: superfly/flyctl-actions/setup-flyctl@master - with: - version: 0.4.49 - - - name: Create Fly applications when absent - shell: bash - run: | - set -euo pipefail - if ! flyctl status --app "$API_APP" >/dev/null 2>&1; then - flyctl apps create "$API_APP" --org "$FLY_ORG" - fi - if ! flyctl status --app "$WEB_APP" >/dev/null 2>&1; then - flyctl apps create "$WEB_APP" --org "$FLY_ORG" - fi - if ! flyctl status --app "$WORKER_APP" >/dev/null 2>&1; then - flyctl apps create "$WORKER_APP" --org "$FLY_ORG" - fi - - - name: Configure private file worker - shell: bash - run: | - set -euo pipefail - FILE_WORKER_SHARED_SECRET="$(openssl rand -hex 32)" - echo "::add-mask::${FILE_WORKER_SHARED_SECRET}" - echo "FILE_WORKER_SHARED_SECRET=${FILE_WORKER_SHARED_SECRET}" >> "$GITHUB_ENV" - - flyctl secrets set --stage --app "$WORKER_APP" \ - "FILE_WORKER_SHARED_SECRET=${FILE_WORKER_SHARED_SECRET}" \ - "FILE_WORKER_STORAGE_ORIGINS=${ROSS_S3_ENDPOINT_URL}" - - - name: Deploy private scan and conversion worker - run: >- - bash scripts/fly-deploy-with-retry.sh . - --config deploy/fly/file-worker.toml - --app "$WORKER_APP" - --remote-only - --no-cache - --flycast - --ha=false - - - name: Configure public-beta API secrets - shell: bash - run: | - set -euo pipefail - WEB_URL="https://${WEB_APP}.fly.dev" - API_URL="https://${API_APP}.fly.dev" - WORKER_URL="http://${WORKER_APP}.flycast" - - flyctl secrets set --stage --app "$API_APP" \ - "SUPABASE_URL=${ROSS_SUPABASE_URL}" \ - "SUPABASE_SECRET_KEY=${ROSS_SUPABASE_SECRET_KEY}" \ - "R2_ENDPOINT_URL=${ROSS_S3_ENDPOINT_URL}" \ - "R2_REGION=${ROSS_S3_REGION}" \ - "R2_ACCESS_KEY_ID=${ROSS_S3_ACCESS_KEY_ID}" \ - "R2_SECRET_ACCESS_KEY=${ROSS_S3_SECRET_ACCESS_KEY}" \ - "R2_BUCKET_NAME=ross-private-files" \ - "FILE_WORKER_URL=${WORKER_URL}" \ - "FILE_WORKER_SHARED_SECRET=${FILE_WORKER_SHARED_SECRET}" \ - "SECURITY_ALERT_WEBHOOK_URL=${ROSS_SECURITY_ALERT_WEBHOOK_URL}" \ - "SECURITY_ALERT_WEBHOOK_SECRET=${ROSS_SECURITY_ALERT_WEBHOOK_SECRET}" \ - "ROSS_UPLOAD_SCAN_REQUIRED=true" \ - "ROSS_ENV=staging" \ - "ROSS_HOSTED_MODE=controlled-beta" \ - "ROSS_REQUIRE_VERIFIED_EMAIL=true" \ - "HOSTED_MODEL_PROVIDERS=openai" \ - "ROSS_DATA_BOUNDARY_VERSION=${POLICY_VERSION}" \ - "ROSS_RELEASE_ID=${INPUT_RELEASE_ID}" \ - "RATE_LIMIT_GENERAL_MAX=180" \ - "RATE_LIMIT_CHAT_MAX=20" \ - "RATE_LIMIT_UPLOAD_MAX=20" \ - "RATE_LIMIT_EXPORT_MAX=5" \ - "DOWNLOAD_TOKEN_TTL_SECONDS=86400" \ - "CORS_ALLOWED_ORIGINS=${WEB_URL}" \ - "FRONTEND_URL=${WEB_URL}" \ - "API_PUBLIC_URL=${API_URL}" - - ensure_random_secret() { - local key="$1" - if ! flyctl secrets list --app "$API_APP" --json \ - | jq -e --arg key "$key" '.[] | select((.Name // .name) == $key)' \ - >/dev/null; then - flyctl secrets set --stage --app "$API_APP" \ - "${key}=$(openssl rand -hex 32)" - fi - } - - ensure_random_secret DOWNLOAD_SIGNING_SECRET - ensure_random_secret USER_API_KEYS_ENCRYPTION_SECRET - ensure_random_secret MCP_CONNECTORS_ENCRYPTION_SECRET - - - name: Deploy ROSS API - run: >- - bash scripts/fly-deploy-with-retry.sh . - --config deploy/fly/api.toml - --app "$API_APP" - --remote-only - --no-cache - --ha=false - - - name: Deploy public ROSS application - shell: bash - run: | - set -euo pipefail - WEB_URL="https://${WEB_APP}.fly.dev" - API_URL="https://${API_APP}.fly.dev" - bash scripts/fly-deploy-with-retry.sh . \ - --config deploy/fly/frontend.toml \ - --app "$WEB_APP" \ - --remote-only \ - --no-cache \ - --ha=false \ - --build-arg "NEXT_PUBLIC_SUPABASE_URL=${ROSS_SUPABASE_URL}" \ - --build-arg "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${ROSS_SUPABASE_PUBLISHABLE_KEY}" \ - --build-arg "NEXT_PUBLIC_API_BASE_URL=${API_URL}" \ - --build-arg "NEXT_PUBLIC_ROSS_APP_URL=${WEB_URL}" \ - --build-arg "NEXT_PUBLIC_ROSS_WEBSITE_URL=${PUBLIC_WEBSITE_URL}" \ - --build-arg "NEXT_PUBLIC_ROSS_HOSTED_MODE=controlled-beta" \ - --build-arg "NEXT_PUBLIC_ROSS_DATA_BOUNDARY_VERSION=${POLICY_VERSION}" \ - --build-arg "NEXT_PUBLIC_ROSS_TERMS_VERSION=${POLICY_VERSION}" \ - --build-arg "NEXT_PUBLIC_ROSS_PRIVACY_VERSION=${POLICY_VERSION}" \ - --build-arg "NEXT_PUBLIC_ROSS_SIGNUPS_ENABLED=true" - - - name: Verify public-beta services - shell: bash - run: | - set -euo pipefail - API_URL="https://${API_APP}.fly.dev" - WEB_URL="https://${WEB_APP}.fly.dev" - curl --fail --retry 8 --retry-delay 5 --retry-all-errors \ - "${API_URL}/health" - curl --fail --retry 8 --retry-delay 5 --retry-all-errors \ - "${WEB_URL}/signup" | grep -q "Create Account" - { - echo "## Public ROSS beta deployed" - echo - echo "- Create account: ${WEB_URL}/signup" - echo "- Login: ${WEB_URL}/login" - echo "- API health: ${API_URL}/health" - echo "- Region: Toronto (yyz)" - echo "- Email verification: API-enforced" - echo "- Anonymous uploads and AI requests: disabled" - echo "- Authenticated uploads: quarantined, scanned, and converted privately" - echo "- Hosted data boundary: synthetic or non-confidential" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Verify required Ontario legal sources - run: >- - node scripts/observe-legal-sources.mjs - --output artifacts/legal-source-health-live.json - - - name: Run deployed application smoke tests - env: - ROSS_E2E_API_URL: https://${{ env.API_APP }}.fly.dev - ROSS_E2E_APP_URL: https://${{ env.WEB_APP }}.fly.dev - run: npm run test:e2e - - - name: Upload sanitized legal-source observation - if: always() - uses: actions/upload-artifact@v7 - with: - name: public-beta-legal-source-health-${{ github.run_id }} - path: artifacts/legal-source-health-live.json - if-no-files-found: warn - retention-days: 14 diff --git a/.github/workflows/dispatch-escalated-mike-on-request.yml b/.github/workflows/dispatch-escalated-mike-on-request.yml deleted file mode 100644 index 16e5ba313b..0000000000 --- a/.github/workflows/dispatch-escalated-mike-on-request.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Dispatch escalated Mike synchronization requests - -on: - push: - branches: [main] - paths: - - docs/upstream-sync-request.json - workflow_dispatch: - -permissions: - actions: write - contents: read - -concurrency: - group: dispatch-escalated-mike-on-request - cancel-in-progress: false - -jobs: - dispatch: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Dispatch escalated synchronization queue - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - script: | - const { owner, repo } = context.repo; - await github.rest.actions.createWorkflowDispatch({ - owner, - repo, - workflow_id: "sync-upstream-mike-escalated.yml", - ref: "main", - }); - core.notice("Dispatched the medium/high-risk Mike synchronization queue."); diff --git a/.github/workflows/dispatch-unverified-agent-heads.yml b/.github/workflows/dispatch-unverified-agent-heads.yml deleted file mode 100644 index b80dccfbad..0000000000 --- a/.github/workflows/dispatch-unverified-agent-heads.yml +++ /dev/null @@ -1,148 +0,0 @@ -name: Dispatch unverified agent heads - -on: - schedule: - - cron: "*/5 * * * *" - workflow_dispatch: - push: - branches: [main] - -permissions: - actions: write - contents: read - issues: write - pull-requests: read - -concurrency: - group: dispatch-unverified-agent-heads - cancel-in-progress: false - -jobs: - dispatch: - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Dispatch Baseline for unverified exact heads - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - script: | - const { owner, repo } = context.repo; - const trustedAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]); - - const { data: workflow } = await github.rest.actions.getWorkflow({ - owner, - repo, - workflow_id: "baseline.yml", - }); - - const reportFailure = async (pr, run) => { - const marker = ``; - const comments = await github.paginate(github.rest.issues.listComments, { - owner, - repo, - issue_number: pr.number, - per_page: 100, - }); - if (comments.some((comment) => comment.body?.includes(marker))) return; - - const jobs = await github.paginate( - github.rest.actions.listJobsForWorkflowRun, - { - owner, - repo, - run_id: run.id, - per_page: 100, - }, - ); - const failures = jobs - .filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped") - .map((job) => { - const steps = (job.steps || []) - .filter((step) => step.conclusion === "failure") - .map((step) => step.name); - return `- **${job.name}** (${job.conclusion})${steps.length ? ` — ${steps.join(", ")}` : ""}`; - }); - - await github.rest.issues.createComment({ - owner, - repo, - issue_number: pr.number, - body: [ - marker, - `Baseline failed for exact head \`${pr.head.sha}\`.`, - "", - failures.length ? failures.join("\n") : `- Workflow conclusion: **${run.conclusion}**`, - "", - `[Open workflow run](${run.html_url})`, - "", - "Bounded automatic repair runs only for eligible low-risk paths. Protected workflow, dependency, migration, deployment, security, legal, governance, or release changes require focused review.", - ].join("\n"), - }); - }; - - const pullRequests = await github.paginate(github.rest.pulls.list, { - owner, - repo, - state: "open", - base: "main", - per_page: 100, - }); - - for (const pr of pullRequests) { - const markedSyncBot = - pr.user?.login === "github-actions[bot]" && - pr.head.ref.startsWith("agent/upstream-sync-") && - pr.body?.includes("Automated-Upstream-Mike-Sync: true"); - const eligible = - !pr.draft && - pr.head.repo?.full_name === `${owner}/${repo}` && - pr.head.ref.startsWith("agent/") && - (trustedAssociations.has(pr.author_association) || markedSyncBot); - - if (!eligible) continue; - - const runs = await github.paginate( - github.rest.actions.listWorkflowRuns, - { - owner, - repo, - workflow_id: workflow.id, - branch: pr.head.ref, - per_page: 100, - }, - ); - - const exactHeadRun = runs.find( - (run) => - run.head_sha === pr.head.sha && - !( - run.status === "completed" && - run.conclusion === "action_required" - ), - ); - if (exactHeadRun) { - core.info( - `PR #${pr.number} already has Baseline run ${exactHeadRun.id} for ${pr.head.sha} (${exactHeadRun.status}/${exactHeadRun.conclusion || "none"}).`, - ); - if ( - exactHeadRun.status === "completed" && - exactHeadRun.conclusion && - exactHeadRun.conclusion !== "success" && - exactHeadRun.conclusion !== "skipped" && - exactHeadRun.conclusion !== "neutral" - ) { - await reportFailure(pr, exactHeadRun); - } - continue; - } - - await github.rest.actions.createWorkflowDispatch({ - owner, - repo, - workflow_id: workflow.id, - ref: pr.head.ref, - }); - core.notice( - `Dispatched Baseline for PR #${pr.number} exact head ${pr.head.sha}.`, - ); - } diff --git a/.github/workflows/final-controlled-beta-evidence.yml b/.github/workflows/final-controlled-beta-evidence.yml deleted file mode 100644 index 9c5245ce17..0000000000 --- a/.github/workflows/final-controlled-beta-evidence.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Final controlled-beta evidence - -on: - workflow_dispatch: - inputs: - release_id: - description: Approved non-secret release identifier - required: true - -permissions: - contents: read - -jobs: - verify-final-candidate: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Check out approved candidate - uses: actions/checkout@v7 - - - name: Set up pinned Node.js and npm - uses: ./.github/actions/setup-ross-node - - - name: Install locked dependencies - run: npm run install:all - - - name: Verify immutable release identifier - env: - INPUT_RELEASE_ID: ${{ inputs.release_id }} - run: node scripts/verify-final-release-id.mjs - - - name: Run complete engineering gate - run: npm run check - - - name: Run fail-closed final completion gate - run: npm run final:check - - - name: Archive non-secret final evidence - uses: actions/upload-artifact@v7 - with: - name: ross-${{ inputs.release_id }}-final-evidence - retention-days: 90 - if-no-files-found: error - path: | - reports/final-completion-dossier.md - reports/ontario-evaluation-v1.json - reports/legal-source-health-v1.json - reports/backup-restore-exercise-2026-07-18.json - reports/release-manifest-v1.json - config/final-completion.v1.json - config/professional-validation.v1.json - config/release-approvals.v1.json - config/operations-readiness.v1.json - config/launch-readiness.v1.json - docs/release-evidence/effective-notices-owner-approval-2026-07-18.md - -# Evidence only. Deployment remains a separate, human-approved action. diff --git a/.github/workflows/repair-failed-baseline.yml b/.github/workflows/handle-baseline-result.yml similarity index 68% rename from .github/workflows/repair-failed-baseline.yml rename to .github/workflows/handle-baseline-result.yml index d002376de6..b615dec6f0 100644 --- a/.github/workflows/repair-failed-baseline.yml +++ b/.github/workflows/handle-baseline-result.yml @@ -1,4 +1,4 @@ -name: Repair failed Baseline +name: Handle Baseline result on: workflow_run: @@ -11,10 +11,163 @@ permissions: pull-requests: read concurrency: - group: repair-baseline-${{ github.event.workflow_run.head_branch || github.event.workflow_run.id }} + group: handle-baseline-${{ github.event.workflow_run.id }} cancel-in-progress: false jobs: + merge: + permissions: + contents: write + pull-requests: write + if: >- + github.event.workflow_run.conclusion == 'success' && + (github.event.workflow_run.event == 'pull_request' || + github.event.workflow_run.event == 'workflow_dispatch') + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Verify exact PR head and merge + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 + with: + script: | + const run = context.payload.workflow_run; + const { owner, repo } = context.repo; + + const resolvePullRequest = async () => { + const linked = run.pull_requests?.[0]; + if (run.event === "pull_request" && linked) { + const { data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: linked.number, + }); + return { pr, verifiedHead: linked.head.sha }; + } + + if (run.event !== "workflow_dispatch" || !run.head_branch) { + return null; + } + + const candidates = await github.paginate(github.rest.pulls.list, { + owner, + repo, + state: "open", + base: "main", + head: `${owner}:${run.head_branch}`, + per_page: 100, + }); + const matches = candidates.filter( + (pr) => + pr.head.repo?.full_name === `${owner}/${repo}` && + pr.head.ref === run.head_branch && + pr.head.sha === run.head_sha, + ); + if (matches.length !== 1) { + core.info( + `Dispatched Baseline resolved ${matches.length} exact open PRs; expected one.`, + ); + return null; + } + return { pr: matches[0], verifiedHead: run.head_sha }; + }; + + const resolved = await resolvePullRequest(); + if (!resolved) { + core.info("No exact pull request is linked to this Baseline run."); + return; + } + const { pr, verifiedHead } = resolved; + + const trustedAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]); + const markedSyncBot = + pr.user?.login === "github-actions[bot]" && + pr.head.ref.startsWith("agent/upstream-sync-") && + pr.body?.includes("Automated-Upstream-Mike-Sync: true"); + const eligible = + pr.state === "open" && + !pr.draft && + pr.head.repo?.full_name === `${owner}/${repo}` && + pr.head.ref.startsWith("agent/") && + (trustedAssociations.has(pr.author_association) || markedSyncBot) && + pr.head.sha === verifiedHead; + + if (!eligible) { + core.info("PR is not an eligible trusted same-repository agent PR at the verified head."); + return; + } + + const query = ` + query($owner: String!, $repo: String!, $number: Int!, $after: String) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + state + isDraft + headRefOid + reviewDecision + mergeable + reviewThreads(first: 100, after: $after) { + nodes { isResolved } + pageInfo { hasNextPage endCursor } + } + } + } + } + `; + + const readGate = async () => { + let after = null; + let pullRequest = null; + let unresolved = false; + + do { + const result = await github.graphql(query, { + owner, + repo, + number: pr.number, + after, + }); + pullRequest = result.repository.pullRequest; + unresolved ||= pullRequest.reviewThreads.nodes.some( + (thread) => !thread.isResolved, + ); + after = pullRequest.reviewThreads.pageInfo.hasNextPage + ? pullRequest.reviewThreads.pageInfo.endCursor + : null; + } while (after); + + return { pullRequest, unresolved }; + }; + + let gate; + for (let attempt = 1; attempt <= 6; attempt += 1) { + gate = await readGate(); + if (gate.pullRequest.mergeable !== "UNKNOWN") break; + core.info(`Mergeability is still UNKNOWN (attempt ${attempt}/6).`); + if (attempt < 6) await new Promise((resolve) => setTimeout(resolve, 10000)); + } + + const node = gate.pullRequest; + const blocked = + node.state !== "OPEN" || + node.isDraft || + node.headRefOid !== verifiedHead || + node.reviewDecision === "CHANGES_REQUESTED" || + node.mergeable !== "MERGEABLE" || + gate.unresolved; + + if (blocked) { + core.info("PR has changed or has a review/merge blocker; it will not be merged."); + return; + } + + await github.rest.pulls.merge({ + owner, + repo, + pull_number: pr.number, + merge_method: "squash", + sha: verifiedHead, + }); + core.notice(`Merged PR #${pr.number} immediately after successful final-head Baseline verification.`); qualify: if: >- github.event.workflow_run.conclusion == 'failure' && @@ -212,6 +365,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 permissions: + actions: write contents: write pull-requests: read steps: @@ -321,3 +475,10 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git commit -m "Auto-fix Baseline failure from run ${FAILED_RUN}" git push origin "HEAD:${HEAD_REF}" + + - name: Dispatch Baseline for repaired exact head + if: steps.parse.outputs.apply == 'true' + env: + GH_TOKEN: ${{ github.token }} + HEAD_REF: ${{ needs.qualify.outputs.head_ref }} + run: gh workflow run baseline.yml --ref "$HEAD_REF" diff --git a/.github/workflows/merge-verified-agent-pr.yml b/.github/workflows/merge-verified-agent-pr.yml deleted file mode 100644 index aaef8c02a8..0000000000 --- a/.github/workflows/merge-verified-agent-pr.yml +++ /dev/null @@ -1,166 +0,0 @@ -name: Merge verified agent pull requests - -on: - workflow_run: - workflows: ["Baseline verification"] - types: [completed] - -permissions: - contents: write - pull-requests: write - -concurrency: - group: merge-verified-${{ github.event.workflow_run.id }} - cancel-in-progress: false - -jobs: - merge: - if: >- - github.event.workflow_run.conclusion == 'success' && - (github.event.workflow_run.event == 'pull_request' || - github.event.workflow_run.event == 'workflow_dispatch') - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Verify exact PR head and merge - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - script: | - const run = context.payload.workflow_run; - const { owner, repo } = context.repo; - - const resolvePullRequest = async () => { - const linked = run.pull_requests?.[0]; - if (run.event === "pull_request" && linked) { - const { data: pr } = await github.rest.pulls.get({ - owner, - repo, - pull_number: linked.number, - }); - return { pr, verifiedHead: linked.head.sha }; - } - - if (run.event !== "workflow_dispatch" || !run.head_branch) { - return null; - } - - const candidates = await github.paginate(github.rest.pulls.list, { - owner, - repo, - state: "open", - base: "main", - head: `${owner}:${run.head_branch}`, - per_page: 100, - }); - const matches = candidates.filter( - (pr) => - pr.head.repo?.full_name === `${owner}/${repo}` && - pr.head.ref === run.head_branch && - pr.head.sha === run.head_sha, - ); - if (matches.length !== 1) { - core.info( - `Dispatched Baseline resolved ${matches.length} exact open PRs; expected one.`, - ); - return null; - } - return { pr: matches[0], verifiedHead: run.head_sha }; - }; - - const resolved = await resolvePullRequest(); - if (!resolved) { - core.info("No exact pull request is linked to this Baseline run."); - return; - } - const { pr, verifiedHead } = resolved; - - const trustedAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]); - const markedSyncBot = - pr.user?.login === "github-actions[bot]" && - pr.head.ref.startsWith("agent/upstream-sync-") && - pr.body?.includes("Automated-Upstream-Mike-Sync: true"); - const eligible = - pr.state === "open" && - !pr.draft && - pr.head.repo?.full_name === `${owner}/${repo}` && - pr.head.ref.startsWith("agent/") && - (trustedAssociations.has(pr.author_association) || markedSyncBot) && - pr.head.sha === verifiedHead; - - if (!eligible) { - core.info("PR is not an eligible trusted same-repository agent PR at the verified head."); - return; - } - - const query = ` - query($owner: String!, $repo: String!, $number: Int!, $after: String) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $number) { - state - isDraft - headRefOid - reviewDecision - mergeable - reviewThreads(first: 100, after: $after) { - nodes { isResolved } - pageInfo { hasNextPage endCursor } - } - } - } - } - `; - - const readGate = async () => { - let after = null; - let pullRequest = null; - let unresolved = false; - - do { - const result = await github.graphql(query, { - owner, - repo, - number: pr.number, - after, - }); - pullRequest = result.repository.pullRequest; - unresolved ||= pullRequest.reviewThreads.nodes.some( - (thread) => !thread.isResolved, - ); - after = pullRequest.reviewThreads.pageInfo.hasNextPage - ? pullRequest.reviewThreads.pageInfo.endCursor - : null; - } while (after); - - return { pullRequest, unresolved }; - }; - - let gate; - for (let attempt = 1; attempt <= 6; attempt += 1) { - gate = await readGate(); - if (gate.pullRequest.mergeable !== "UNKNOWN") break; - core.info(`Mergeability is still UNKNOWN (attempt ${attempt}/6).`); - if (attempt < 6) await new Promise((resolve) => setTimeout(resolve, 10000)); - } - - const node = gate.pullRequest; - const blocked = - node.state !== "OPEN" || - node.isDraft || - node.headRefOid !== verifiedHead || - node.reviewDecision === "CHANGES_REQUESTED" || - node.mergeable !== "MERGEABLE" || - gate.unresolved; - - if (blocked) { - core.info("PR has changed or has a review/merge blocker; it will not be merged."); - return; - } - - await github.rest.pulls.merge({ - owner, - repo, - pull_number: pr.number, - merge_method: "squash", - sha: verifiedHead, - }); - core.notice(`Merged PR #${pr.number} immediately after successful final-head Baseline verification.`); diff --git a/.github/workflows/reconcile-verified-agent-merges.yml b/.github/workflows/reconcile-verified-agent-merges.yml deleted file mode 100644 index 40f47a756d..0000000000 --- a/.github/workflows/reconcile-verified-agent-merges.yml +++ /dev/null @@ -1,145 +0,0 @@ -name: Reconcile verified agent pull request merges - -on: - schedule: - - cron: "*/5 * * * *" - workflow_dispatch: - -permissions: - actions: read - contents: write - pull-requests: write - -concurrency: - group: reconcile-verified-agent-merges - cancel-in-progress: false - -jobs: - reconcile: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Merge eligible exact-head verified pull requests - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - script: | - const { owner, repo } = context.repo; - const trustedAssociations = new Set(["OWNER", "MEMBER", "COLLABORATOR"]); - - const pulls = await github.paginate(github.rest.pulls.list, { - owner, - repo, - state: "open", - base: "main", - per_page: 100, - }); - - const candidates = pulls.filter((pr) => { - const markedSyncBot = - pr.user?.login === "github-actions[bot]" && - pr.head.ref.startsWith("agent/upstream-sync-") && - pr.body?.includes("Automated-Upstream-Mike-Sync: true"); - return ( - !pr.draft && - pr.head.repo?.full_name === `${owner}/${repo}` && - pr.head.ref.startsWith("agent/") && - (trustedAssociations.has(pr.author_association) || markedSyncBot) - ); - }); - - const gateQuery = ` - query($owner: String!, $repo: String!, $number: Int!, $after: String) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $number) { - state - isDraft - headRefOid - reviewDecision - mergeable - reviewThreads(first: 100, after: $after) { - nodes { isResolved } - pageInfo { hasNextPage endCursor } - } - } - } - } - `; - - const readGate = async (number) => { - let after = null; - let pullRequest = null; - let unresolved = false; - do { - const result = await github.graphql(gateQuery, { - owner, - repo, - number, - after, - }); - pullRequest = result.repository.pullRequest; - unresolved ||= pullRequest.reviewThreads.nodes.some( - (thread) => !thread.isResolved, - ); - after = pullRequest.reviewThreads.pageInfo.hasNextPage - ? pullRequest.reviewThreads.pageInfo.endCursor - : null; - } while (after); - return { pullRequest, unresolved }; - }; - - for (const pr of candidates) { - const runs = await github.paginate( - github.rest.actions.listWorkflowRunsForRepo, - { - owner, - repo, - branch: pr.head.ref, - status: "completed", - per_page: 100, - }, - ); - const verified = runs.some( - (run) => - run.name === "Baseline verification" && - run.conclusion === "success" && - run.head_sha === pr.head.sha && - (run.event === "pull_request" || run.event === "workflow_dispatch"), - ); - if (!verified) { - core.info(`PR #${pr.number} has no successful exact-head Baseline.`); - continue; - } - - let gate; - for (let attempt = 1; attempt <= 6; attempt += 1) { - gate = await readGate(pr.number); - if (gate.pullRequest.mergeable !== "UNKNOWN") break; - if (attempt < 6) await new Promise((resolve) => setTimeout(resolve, 10000)); - } - - const node = gate.pullRequest; - const blocked = - node.state !== "OPEN" || - node.isDraft || - node.headRefOid !== pr.head.sha || - node.reviewDecision === "CHANGES_REQUESTED" || - node.mergeable !== "MERGEABLE" || - gate.unresolved; - if (blocked) { - core.info(`PR #${pr.number} still has a review, head, or mergeability blocker.`); - continue; - } - - try { - await github.rest.pulls.merge({ - owner, - repo, - pull_number: pr.number, - merge_method: "squash", - sha: pr.head.sha, - }); - core.notice(`Reconciled and merged PR #${pr.number} at verified head ${pr.head.sha}.`); - } catch (error) { - core.warning(`PR #${pr.number} was eligible but merge failed: ${error.message}`); - } - } diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml deleted file mode 100644 index 5c28a43612..0000000000 --- a/.github/workflows/release-candidate.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Release candidate evidence - -on: - workflow_dispatch: - inputs: - candidate_id: - description: Reserved non-secret release candidate identifier - required: true - default: ross-public-beta-20260717-rc1 - -permissions: - contents: write - -jobs: - verify-candidate: - runs-on: ubuntu-latest - timeout-minutes: 25 - steps: - - name: Check out candidate - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Set up pinned Node.js and npm - uses: ./.github/actions/setup-ross-node - - - name: Install locked dependencies - run: npm run install:all - - - name: Run the complete engineering gate - run: npm run check - - - name: Run the production final-completion gate - run: npm run final:check - - - name: Confirm governed artifacts are current - run: npm run test:release-manifest - - - name: Confirm the reserved release identifier - env: - INPUT_RELEASE_ID: ${{ inputs.candidate_id }} - run: node scripts/verify-final-release-id.mjs - - - name: Archive non-secret candidate evidence - uses: actions/upload-artifact@v7 - with: - name: ross-${{ inputs.candidate_id }}-evidence - retention-days: 30 - if-no-files-found: error - path: | - reports/ontario-evaluation-v1.json - reports/legal-source-health-v1.json - reports/backup-restore-exercise-2026-07-18.json - reports/release-manifest-v1.json - reports/final-completion-dossier.md - config/final-completion.v1.json - config/professional-validation.v1.json - config/release-approvals.v1.json - config/operations-readiness.v1.json - config/launch-readiness.v1.json - docs/release-evidence/effective-notices-owner-approval-2026-07-18.md - - - name: Create or confirm the immutable release tag - shell: bash - env: - INPUT_RELEASE_ID: ${{ inputs.candidate_id }} - run: | - set -euo pipefail - git fetch --tags --force - if git rev-parse --verify --quiet "refs/tags/${INPUT_RELEASE_ID}" >/dev/null; then - TAG_COMMIT="$(git rev-list -n 1 "refs/tags/${INPUT_RELEASE_ID}")" - if [ "${TAG_COMMIT}" != "${GITHUB_SHA}" ]; then - echo "Release tag already exists on a different commit and will not be moved." >&2 - exit 1 - fi - echo "Release tag already points to the verified commit." - else - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git tag -a "${INPUT_RELEASE_ID}" -m "ROSS verified public-beta candidate ${INPUT_RELEASE_ID}" - git push origin "refs/tags/${INPUT_RELEASE_ID}" - fi -# Deliberately no deploy job. Production promotion is a separate, human-approved -# action after every fail-closed record and external review is complete. diff --git a/.github/workflows/run-all-upstream-mike-synchronizers.yml b/.github/workflows/run-all-upstream-mike-synchronizers.yml deleted file mode 100644 index c8709fc6a6..0000000000 --- a/.github/workflows/run-all-upstream-mike-synchronizers.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Run all upstream Mike synchronizers - -on: - workflow_dispatch: - -permissions: - actions: write - contents: read - -concurrency: - group: run-all-upstream-mike-synchronizers - cancel-in-progress: false - -jobs: - start: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Start self-draining low-risk and escalation queues - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - script: | - const { owner, repo } = context.repo; - await github.rest.actions.createWorkflowDispatch({ - owner, - repo, - workflow_id: "sync-upstream-mike.yml", - ref: "main", - }); - core.notice( - "Started the low-risk Mike synchronizer. Each merged batch dispatches the next low-risk batch and the escalated queue; high-risk work stops at a draft PR for one readiness decision.", - ); diff --git a/.github/workflows/sync-upstream-mike-escalated.yml b/.github/workflows/sync-upstream-mike-escalated.yml index a8926dfe0c..a8576d5f1a 100644 --- a/.github/workflows/sync-upstream-mike-escalated.yml +++ b/.github/workflows/sync-upstream-mike-escalated.yml @@ -398,6 +398,9 @@ jobs: fi pr_number="$(gh pr view "$pr_url" --json number --jq .number)" echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT" + if [ "$RISK" != "high" ]; then + gh workflow run baseline.yml --ref "$branch" + fi - name: Wait for automatic merge and continue escalation queue if: steps.normalize.outputs.risk != 'high' diff --git a/.github/workflows/sync-upstream-mike.yml b/.github/workflows/sync-upstream-mike.yml index 98ffaa247b..ad319d7b9a 100644 --- a/.github/workflows/sync-upstream-mike.yml +++ b/.github/workflows/sync-upstream-mike.yml @@ -4,10 +4,6 @@ on: schedule: - cron: "17 14 * * *" workflow_dispatch: - push: - branches: [main] - paths: - - docs/upstream-sync-request.json permissions: contents: read @@ -365,6 +361,7 @@ jobs: --body-file /tmp/pr-body.md)" pr_number="$(gh pr view "$pr_url" --json number --jq .number)" echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT" + gh workflow run baseline.yml --ref "$branch" deadline=$((SECONDS + 7200)) while [ "$SECONDS" -lt "$deadline" ]; do diff --git a/.github/workflows/verify-and-deploy-public-beta.yml b/.github/workflows/verify-and-deploy-public-beta.yml index 50a0fdb484..f169f18139 100644 --- a/.github/workflows/verify-and-deploy-public-beta.yml +++ b/.github/workflows/verify-and-deploy-public-beta.yml @@ -37,6 +37,10 @@ jobs: - name: Run complete engineering gate run: npm run check + - name: Run production final-completion gate before public promotion + if: inputs.promote_public + run: npm run final:check + - name: Build every Fly container path run: npm run preflight:fly @@ -211,6 +215,17 @@ jobs: artifacts/release-train-ledger.json artifacts/release-train-legal-source-health.json artifacts/release-train-build/*.log + reports/final-completion-dossier.md + reports/ontario-evaluation-v1.json + reports/legal-source-health-v1.json + reports/backup-restore-exercise-2026-07-18.json + reports/release-manifest-v1.json + config/final-completion.v1.json + config/professional-validation.v1.json + config/release-approvals.v1.json + config/operations-readiness.v1.json + config/launch-readiness.v1.json + docs/release-evidence/effective-notices-owner-approval-2026-07-18.md if-no-files-found: warn retention-days: 90 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06182a5e37..a71cc61839 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ Mike functionality recorded in the baseline contract. - Install all dependencies from the repository root with `npm run install:all`. - Run the baseline contract tests with `npm test`. - Run the full local verification suite with `npm run check`. -- For a release-candidate change, regenerate and verify the governed manifest ++ For a release-train change, regenerate and verify the governed manifest with `npm run build:release-manifest` and `npm run test:release-manifest`. - If deployed test URLs are available, run `ROSS_E2E_API_URL=... ROSS_E2E_APP_URL=... npm run test:e2e`. - Check `git diff` and remove unrelated changes. diff --git a/config/final-completion.v1.json b/config/final-completion.v1.json index b0548988db..3cbcdaca67 100644 --- a/config/final-completion.v1.json +++ b/config/final-completion.v1.json @@ -80,7 +80,7 @@ "id": "immutable-release-candidate", "ownerRole": "release owner", "status": "completed-with-evidence", - "sourceOfTruth": "reports/release-manifest-v1.json and .github/workflows/release-candidate.yml" + "sourceOfTruth": "reports/release-manifest-v1.json and .github/workflows/verify-and-deploy-public-beta.yml" } ] } diff --git a/config/release-manifest.v1.json b/config/release-manifest.v1.json index ba3896fe91..6d2977b0bb 100644 --- a/config/release-manifest.v1.json +++ b/config/release-manifest.v1.json @@ -5,13 +5,10 @@ "artifacts": [ ".github/actionlint.yaml", ".github/actions/setup-ross-node/action.yml", - ".github/workflows/baseline.yml", - ".github/workflows/deploy-private-ross.yml", - ".github/workflows/deploy-public-beta-ross.yml", - ".github/workflows/final-controlled-beta-evidence.yml", - ".github/workflows/refresh-release-manifest.yml", - ".github/workflows/release-candidate.yml", - ".github/workflows/staging-debug-release-train.yml", + ".github/workflows/baseline.yml", + ".github/workflows/deploy-private-ross.yml", + ".github/workflows/refresh-release-manifest.yml", + ".github/workflows/staging-debug-release-train.yml", ".github/workflows/verify-and-deploy-public-beta.yml", ".github/workflows/verify-ontario-sources.yml", "backend/migrations/20260718_01_document_scan_pipeline.sql", diff --git a/docs/deployment/public-beta-combined-update.md b/docs/deployment/public-beta-combined-update.md index 8b0d35d281..9026b785ff 100644 --- a/docs/deployment/public-beta-combined-update.md +++ b/docs/deployment/public-beta-combined-update.md @@ -43,8 +43,9 @@ See [ROSS Release Train v1](release-train-v1.md) for the one-button rehearsal, forced staging failure, three-component rollback, digest promotion, cleanup, and operator summary. -Do not run the older standalone hotfix or the blocked legacy public-deployment -workflow. +Use the ROSS release train for public promotion. The former standalone public +deployment path has been removed, so there is no alternate deployment workflow +to run. ## CanLII boundary diff --git a/docs/final/final-release-procedure.md b/docs/final/final-release-procedure.md index a76ff2a874..f228e7d09b 100644 --- a/docs/final/final-release-procedure.md +++ b/docs/final/final-release-procedure.md @@ -11,7 +11,7 @@ `npm run check`. 5. Deploy this exact commit to isolated staging and complete every evidence exercise and source observation without confidential material. -6. Download/retain the release-candidate evidence and link its immutable run. +6. Run/retain the ROSS release-train evidence and link its immutable run. 7. Run `npm run final:status` for the readable inventory, then `npm run final:check` for the fail-closed gate. @@ -22,10 +22,10 @@ approval, replace evidence with a placeholder, enable CanLII scraping, or substitute a different build. Correct the underlying issue and create a new candidate when evidence no longer matches. -If it passes, run the GitHub **Final controlled-beta evidence** workflow on the -same commit. Confirm the artifact contains the governed manifest, evaluation, -source-health, professional-validation, approval, operations, launch, and final -completion records. Record the human go/no-go decision and its time window. +If it passes, retain the release-train artifact from the same commit. Confirm the +artifact contains the governed manifest, evaluation, source-health, +professional-validation, approval, operations, launch, and final completion +records. Record the human go/no-go decision and its time window. Promote only the reviewed immutable images. Monitor authentication, storage, chat/model availability, legal-source health, errors, latency, and security diff --git a/docs/final/owner-action-sheet.md b/docs/final/owner-action-sheet.md index 3e8f24fa4e..c9f0444176 100644 --- a/docs/final/owner-action-sheet.md +++ b/docs/final/owner-action-sheet.md @@ -25,11 +25,11 @@ approved until the named evidence actually exists. domains, vendors/residency, effective notices, support/privacy contacts, public-beta registration/terms, abuse controls, and the go-live decision. 7. **Immutable candidate.** Assign one release ID everywhere, rebuild generated - records, run the release-candidate workflow, and deploy that exact candidate + records, run the ROSS release-train workflow, and deploy that exact candidate to isolated staging. -8. **Final gate.** Run `npm run final:check`. Any blocker ends the promotion - attempt. If it passes, run the GitHub **Final controlled-beta evidence** - workflow and retain its artifact and approval record. +8. **Final gate.** The release train runs `npm run final:check` before public + promotion. Any blocker ends the promotion attempt. Retain its 90-day evidence + artifact and approval record. 9. **Limited launch.** Open verified self-registration without anonymous use. Keep the hosted beta restricted to synthetic or affirmatively non-confidential material. Public search indexing and confidential or diff --git a/docs/operations/release-runbook.md b/docs/operations/release-runbook.md index 6f17058cb1..13c0604f02 100644 --- a/docs/operations/release-runbook.md +++ b/docs/operations/release-runbook.md @@ -1,18 +1,20 @@ -# Release-candidate runbook +# ROSS release-train runbook This runbook creates evidence; it does not authorize production. The release -owner must use a unique candidate identifier and keep code, schema, workflows, -legal-source policy, evaluation output, and approvals tied to the same commit. +owner must keep code, schema, workflows, legal-source policy, evaluation +output, and approvals tied to the same commit. The release train generates the +candidate identifier. ## Candidate sequence 1. Start from a reviewed commit on `main`; confirm the worktree and generated files are clean. -2. Set a unique release ID in every governed record. Do not reuse an identifier. +2. Confirm the governed records and release manifest are current. The release + train generates a new unused identifier for the exact checked-out commit. 3. Run `npm run install:all`, `npm run check`, and `npm run build:release-manifest` using the locked dependency files. -4. Run the GitHub **Release candidate evidence** workflow for the same commit. - Retain its URL, commit SHA, logs, and downloaded evidence artifact. +4. Run the GitHub **ROSS release train** workflow for the same commit. Retain + its URL, commit SHA, logs, and 90-day evidence artifact. 5. Deploy that immutable candidate to isolated staging. Never substitute a different build after review. 6. Complete the staging journey, migration dry run, backup/restore exercise, diff --git a/docs/public-beta-deployment.md b/docs/public-beta-deployment.md index 3209e9b3de..2a5883b8c0 100644 --- a/docs/public-beta-deployment.md +++ b/docs/public-beta-deployment.md @@ -43,13 +43,12 @@ environment deployment branches to `main`. 1. Complete the work in `docs/final/owner-action-sheet.md` for one immutable release candidate. -2. Run the **Final controlled-beta evidence** workflow and retain its artifact. -3. Open **Actions → Deploy public ROSS beta → Run workflow** on that exact - commit. -4. Enter the matching release ID and Fly organization/app names. -5. Confirm the Supabase auth configuration and recorded go-live approval. -6. The workflow re-runs `npm run final:check`; any incomplete or mismatched - evidence stops deployment. +2. Open **Actions → ROSS release train → Run workflow** on `main`. +3. Leave public promotion unchecked for rehearsal, or select it only after + the final completion gate and approvals are ready. +4. Confirm the Supabase auth configuration and recorded go-live approval. +5. The release train runs `npm run final:check` before public promotion and + retains the governed evidence package for 90 days. ## Post-deploy verification diff --git a/docs/releases/release-completion-runbook.md b/docs/releases/release-completion-runbook.md index 476785afa4..04fbf9d88f 100644 --- a/docs/releases/release-completion-runbook.md +++ b/docs/releases/release-completion-runbook.md @@ -20,7 +20,7 @@ a commit-derived identifier inside tracked files would create a new commit and change the hash it was intended to identify. Instead: 1. the stable name is reserved in the governed records; -2. the release-candidate workflow runs every engineering and governance gate; +2. the ROSS release-train workflow runs every engineering and governance gate; 3. only after those gates pass, the workflow creates an annotated Git tag with the reserved name on the checked-out commit; and 4. the deployment workflow proves that the tag exists and points to the exact @@ -113,26 +113,22 @@ record for H, not another development deliverable. ### 6. Create the immutable candidate -From GitHub Actions, run **Release candidate evidence** on the final merged -`main` commit. Leave the candidate input as: - -`ross-public-beta-20260717-rc1` +From GitHub Actions, run **ROSS release train** on the final merged `main` +commit. The release train generates the candidate identifier automatically. +The workflow: The workflow: -- validates that every governed record uses that identifier; - reruns the complete engineering gate; -- archives non-secret evidence; and -- creates or confirms an annotated tag with that name on the exact verified - commit. +- archives the governed non-secret evidence for 90 days; and +- creates the immutable tag only after explicit public promotion succeeds. If any gate fails, do not create the tag and do not deploy. ### 7. Launch the public beta -Run **Deploy public ROSS beta** using the same identifier. The deployment -workflow checks that the immutable tag points to the checked-out commit and -runs `npm run final:check` before any Fly.io change. +Run **ROSS release train** with public promotion explicitly enabled. Its +production final-completion gate runs before any Fly.io change. After deployment, run and record the smoke tests. If a source change is needed, do not move the tag. Reserve an `rc2` identifier and repeat the candidate diff --git a/docs/ross-170-verification.md b/docs/ross-170-verification.md index d0c503d073..e4712922a1 100644 --- a/docs/ross-170-verification.md +++ b/docs/ross-170-verification.md @@ -10,7 +10,7 @@ - SHA-256 manifest for governed release artifacts with a freshness check. - Evidence-bearing operational release gate covering CI, staging, migrations, backup/restore, rollback, observability, sources, dependencies, and incidents. -- Manual GitHub release-candidate evidence workflow with no deployment job. +- The ROSS release train produces 90-day evidence while keeping promotion behind its explicit human-approved input. - Executable release, evidence, backup/restore, rollback, source, observability, and security reporting runbooks. diff --git a/reports/final-completion-dossier.md b/reports/final-completion-dossier.md index 6f34f37ace..34757a3192 100644 --- a/reports/final-completion-dossier.md +++ b/reports/final-completion-dossier.md @@ -18,7 +18,7 @@ Generated from governed records. This report is evidence inventory, not approval | privacy-security-accessibility | independent privacy, security, and accessibility reviewers | completed-with-evidence | `config/release-approvals.v1.json#approvals` | | operational-exercises | release and operations owners | completed-with-evidence | `config/operations-readiness.v1.json#evidence` | | accountable-launch-decisions | legal operator and product owner | completed-with-evidence | `config/launch-readiness.v1.json#decisions` | -| immutable-release-candidate | release owner | completed-with-evidence | `reports/release-manifest-v1.json and .github/workflows/release-candidate.yml` | +| immutable-release-candidate | release owner | completed-with-evidence | `reports/release-manifest-v1.json and .github/workflows/verify-and-deploy-public-beta.yml` | ## Provider decision diff --git a/reports/release-manifest-v1.json b/reports/release-manifest-v1.json index b6de5bd5f5..dd32af8392 100644 --- a/reports/release-manifest-v1.json +++ b/reports/release-manifest-v1.json @@ -3,12 +3,12 @@ "releaseId": "ross-public-beta-20260717-rc1", "generatedAt": "2026-07-26T10:39:57.000Z", "algorithm": "sha256", - "artifactCount": 130, + "artifactCount": 127, "artifacts": [ { "path": ".github/actionlint.yaml", - "sha256": "532b5dd2b8fced57ebc2ef18624a238111ccfa920399c272d4e096098fe79554", - "sizeBytes": 246 + "sha256": "ceffa5bd25ae9ae637e3b1b8417012a01bf0a2c166bade718f40d7d15f9f33cb", + "sizeBytes": 134 }, { "path": ".github/actions/setup-ross-node/action.yml", @@ -17,34 +17,19 @@ }, { "path": ".github/workflows/baseline.yml", - "sha256": "4d86e77e4be8278f92f3bb772c1430321b29a65b527e596e4cece66378356c9b", - "sizeBytes": 4643 + "sha256": "0ac1e8065a021e6a4a46bda07248ab14e21812d76130f7ea1e8291f14645ff06", + "sizeBytes": 4702 }, { "path": ".github/workflows/deploy-private-ross.yml", "sha256": "c8d859646fc8143964afdd1277fc6f291c8f6efeb0904e2f397fbd518e18b228", "sizeBytes": 11614 }, - { - "path": ".github/workflows/deploy-public-beta-ross.yml", - "sha256": "8a40a7cb5689498c321e8e3e66cbce67d7a70faa9e3ea369873aaf9c04b7071f", - "sizeBytes": 12920 - }, - { - "path": ".github/workflows/final-controlled-beta-evidence.yml", - "sha256": "49ee5033f9e86962e16b76f097dfa72f76094a304f8070c2b17ebe91fa359768", - "sizeBytes": 1757 - }, { "path": ".github/workflows/refresh-release-manifest.yml", "sha256": "71ed98cf97f57617aa1fde12530a3271d43df467f68457829082f5223a7ce758", "sizeBytes": 2184 }, - { - "path": ".github/workflows/release-candidate.yml", - "sha256": "b155ce7cc7a54f884e20f6a164929d8851652793725db8e2e0df18bbc7cf0511", - "sizeBytes": 3027 - }, { "path": ".github/workflows/staging-debug-release-train.yml", "sha256": "15f27aec7e3de70df3b88d2bb1d81e292bc55229ee656fef480837eedaeae915", @@ -52,8 +37,8 @@ }, { "path": ".github/workflows/verify-and-deploy-public-beta.yml", - "sha256": "2bae1574be2fd81bf97636e77d3810225ac016122ca463971b17511a9f8951fc", - "sizeBytes": 9367 + "sha256": "f0480f0fcf997b1b5e1408c12d7704032a7222aef61d1011599d0ab72a7ecb6c", + "sizeBytes": 10071 }, { "path": ".github/workflows/verify-ontario-sources.yml", @@ -197,8 +182,8 @@ }, { "path": "config/final-completion.v1.json", - "sha256": "2095404de0cde5b8de7beeeb20948309b50e713394fae8a1ccad46465db01f4a", - "sizeBytes": 3367 + "sha256": "f5f6c34845d1ca3a8e57d185bde3207107d1d9b5e6bf475fb4d0afa05ccec8e9", + "sizeBytes": 3379 }, { "path": "config/launch-readiness.v1.json", @@ -487,8 +472,8 @@ }, { "path": "reports/final-completion-dossier.md", - "sha256": "fb40876504aeaf170438dd4a0fe5830f3e103a6c58778ab4864971a140179a21", - "sizeBytes": 2084 + "sha256": "0a7d0d3285e4f0d95e32311b75b0427f08728523573f4ccc1bd245d7eebbe553", + "sizeBytes": 2096 }, { "path": "reports/ontario-evaluation-v1.json", @@ -602,8 +587,8 @@ }, { "path": "tests/baseline/ross-ci-toolchain.test.mjs", - "sha256": "fee3ccf92ab0895cd40fef7776f3a3c1763ee82c160ef5c98c5ba6f64dacb6fe", - "sizeBytes": 3714 + "sha256": "859d19d5982305e78ade9936c260ff69c4df49b3141908660b21dff323220a08", + "sizeBytes": 3602 }, { "path": "tests/baseline/ross-delivery-d.test.mjs", @@ -612,8 +597,8 @@ }, { "path": "tests/baseline/ross-production-readiness.test.mjs", - "sha256": "def2f4e5b2e51fb9048821a05f8109b1cbb5415c520453853a612c2780f07ddd", - "sizeBytes": 3757 + "sha256": "4053d258002b166f92677675aeba9b637f4ecfa60c20d9c1f9b252144513193f", + "sizeBytes": 3874 }, { "path": "tests/baseline/ross-release-train.test.mjs", @@ -642,8 +627,8 @@ }, { "path": "website/app/page-content.ts", - "sha256": "e40f811d708f899da2e025934edadc876f932fc089431a91df4c574eaa370425", - "sizeBytes": 31845 + "sha256": "70bc298aa61d186216a3288d1a242f830551b71f1c15ed44ff9a6abf1b0735b7", + "sizeBytes": 31822 }, { "path": "website/app/generated-brand-config.ts", diff --git a/tests/baseline/dispatch-unverified-agent-heads.test.mjs b/tests/baseline/dispatch-unverified-agent-heads.test.mjs index 9481b85807..58fbf2613c 100644 --- a/tests/baseline/dispatch-unverified-agent-heads.test.mjs +++ b/tests/baseline/dispatch-unverified-agent-heads.test.mjs @@ -6,11 +6,14 @@ import { fileURLToPath } from "node:url"; const root = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const workflow = readFileSync( - resolve(root, ".github/workflows/dispatch-unverified-agent-heads.yml"), + resolve(root, ".github/workflows/agent-pr-reconciler.yml"), "utf8", ); -test("approval-blocked pull-request runs do not suppress exact-head dispatch", () => { +test("the hourly agent reconciler preserves exact-head dispatch and merge gates", () => { + assert.match(workflow, /cron: "0 \* \* \* \*"/); + assert.match(workflow, /workflow_dispatch:/); + assert.match(workflow, /branches: \[main\]/); assert.match(workflow, /run\.head_sha === pr\.head\.sha/); assert.match(workflow, /run\.status === "completed"/); assert.match(workflow, /run\.conclusion === "action_required"/); @@ -19,4 +22,7 @@ test("approval-blocked pull-request runs do not suppress exact-head dispatch", ( /!\(\s*run\.status === "completed" &&\s*run\.conclusion === "action_required"\s*\)/, ); assert.match(workflow, /github\.rest\.actions\.createWorkflowDispatch/); + assert.match(workflow, /github\.rest\.pulls\.merge/); + assert.match(workflow, /reviewDecision === "CHANGES_REQUESTED"/); + assert.match(workflow, /node\.mergeable !== "MERGEABLE"/); }); diff --git a/tests/baseline/private-deployment.test.mjs b/tests/baseline/private-deployment.test.mjs index d9c61a6329..c0b8dddd05 100644 --- a/tests/baseline/private-deployment.test.mjs +++ b/tests/baseline/private-deployment.test.mjs @@ -55,25 +55,24 @@ test("private deployment observes required Ontario sources without storing sourc }); test("public deployment keeps Ontario source verification strict", () => { - const workflow = read(".github/workflows/deploy-public-beta-ross.yml"); - const sourceStep = workflow.match( - /- name: Verify required Ontario legal sources[\s\S]*?(?=\n\s+- name:|$)/, - )?.[0] ?? ""; + const workflow = read(".github/workflows/verify-and-deploy-public-beta.yml"); + const releaseTrain = read("scripts/fly-release-train.mjs"); - assert.match(sourceStep, /observe-legal-sources\.mjs/); - assert.doesNotMatch(sourceStep, /continue-on-error/); + assert.match(workflow, /release-train-legal-source-health/); + assert.match(releaseTrain, /scripts\/observe-legal-sources\.mjs/); + assert.doesNotMatch(releaseTrain, /continue-on-error/); }); test("deployment workflows run the live API and web smoke contract", () => { for (const path of [ ".github/workflows/deploy-private-ross.yml", - ".github/workflows/deploy-public-beta-ross.yml", ]) { const workflow = read(path); assert.match(workflow, /ROSS_E2E_API_URL/); assert.match(workflow, /ROSS_E2E_APP_URL/); assert.match(workflow, /npm run test:e2e/); } + assert.match(read("scripts/fly-release-train.mjs"), /function smoke\(/); }); test("private deployment credentials are supplied only through GitHub secrets", () => { diff --git a/tests/baseline/ross-automation-consolidation.test.mjs b/tests/baseline/ross-automation-consolidation.test.mjs new file mode 100644 index 0000000000..1c3f7585e4 --- /dev/null +++ b/tests/baseline/ross-automation-consolidation.test.mjs @@ -0,0 +1,63 @@ +import assert from "node:assert/strict"; +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const workflowDir = resolve(root, ".github/workflows"); +const read = (path) => readFileSync(resolve(root, path), "utf8"); + +test("the workflow topology keeps one owner for each automation boundary", () => { + const workflows = readdirSync(workflowDir).filter((name) => name.endsWith(".yml")).sort(); + assert.deepEqual(workflows, [ + "agent-pr-reconciler.yml", + "baseline.yml", + "coordinate-upstream-mike.yml", + "deploy-private-ross.yml", + "handle-baseline-result.yml", + "refresh-release-manifest.yml", + "staging-debug-release-train.yml", + "sync-upstream-mike-escalated.yml", + "sync-upstream-mike.yml", + "verify-and-deploy-public-beta.yml", + "verify-ontario-sources.yml", + ]); + + for (const obsolete of [ + "apply-upstream-sync-batch-size.yml", + "deploy-public-beta-ross.yml", + "dispatch-escalated-mike-on-request.yml", + "dispatch-unverified-agent-heads.yml", + "final-controlled-beta-evidence.yml", + "merge-verified-agent-pr.yml", + "reconcile-verified-agent-merges.yml", + "release-candidate.yml", + "repair-failed-baseline.yml", + "run-all-upstream-mike-synchronizers.yml", + ]) { + assert.equal(existsSync(resolve(workflowDir, obsolete)), false, obsolete); + } +}); + +test("consolidated handlers preserve their bounded permissions and triggers", () => { + const baseline = read(".github/workflows/baseline.yml"); + const handler = read(".github/workflows/handle-baseline-result.yml"); + const agent = read(".github/workflows/agent-pr-reconciler.yml"); + const mike = read(".github/workflows/coordinate-upstream-mike.yml"); + const lowRisk = read(".github/workflows/sync-upstream-mike.yml"); + + assert.match(baseline, /paths-ignore:[\s\S]*reports\/release-manifest-v1\.json/); + assert.match(handler, /^ merge:[\s\S]*contents: write/m); + assert.match(handler, /allow-bots: true/); + assert.match(handler, /allow-bot-users: github-actions/); + assert.match(agent, /cron: "0 \* \* \* \*"/); + assert.doesNotMatch(agent, /cron: "\*\/5 \* \* \* \*"/); + assert.match(agent, /actions: write/); + assert.match(agent, /github\.rest\.actions\.createWorkflowDispatch/); + assert.match(agent, /github\.rest\.pulls\.merge/); + assert.match(mike, /docs\/upstream-sync-request\.json/); + assert.match(mike, /sync-upstream-mike\.yml/); + assert.match(mike, /sync-upstream-mike-escalated\.yml/); + assert.doesNotMatch(lowRisk, /docs\/upstream-sync-request\.json/); +}); diff --git a/tests/baseline/ross-ci-toolchain.test.mjs b/tests/baseline/ross-ci-toolchain.test.mjs index 675e60a7fe..ee594ec32f 100644 --- a/tests/baseline/ross-ci-toolchain.test.mjs +++ b/tests/baseline/ross-ci-toolchain.test.mjs @@ -24,13 +24,11 @@ test("CI pins an npm version that uses the supported bulk advisory endpoint", () }); test("every complete engineering workflow uses the shared pinned toolchain", () => { - for (const path of [ - ".github/workflows/baseline.yml", - ".github/workflows/final-controlled-beta-evidence.yml", - ".github/workflows/refresh-release-manifest.yml", - ".github/workflows/release-candidate.yml", - ".github/workflows/verify-and-deploy-public-beta.yml", - ]) { + for (const path of [ + ".github/workflows/baseline.yml", + ".github/workflows/refresh-release-manifest.yml", + ".github/workflows/verify-and-deploy-public-beta.yml", + ]) { const workflow = read(path); assert.match(workflow, /uses: \.\/\.github\/actions\/setup-ross-node/); assert.doesNotMatch(workflow, /uses: actions\/setup-node/); diff --git a/tests/baseline/ross-deliverable-f.test.mjs b/tests/baseline/ross-deliverable-f.test.mjs index fa93d27e9a..c22760c479 100644 --- a/tests/baseline/ross-deliverable-f.test.mjs +++ b/tests/baseline/ross-deliverable-f.test.mjs @@ -34,12 +34,14 @@ test("Deliverable F preserves source boundaries and exposes coverage gaps", () = assert.equal(plan.providerStrategy.canliiWebsiteAutomationAllowed, false); }); -test("final evidence workflow is manual, immutable, and never deploys", () => { - const workflow = read(".github/workflows/final-controlled-beta-evidence.yml"); +test("release train carries final evidence and keeps promotion explicit", () => { + const workflow = read(".github/workflows/verify-and-deploy-public-beta.yml"); assert.match(workflow, /workflow_dispatch/); assert.match(workflow, /final:check/); - assert.match(workflow, /verify-final-release-id/); - assert.doesNotMatch(workflow, /flyctl deploy|wrangler deploy|kubectl|helm upgrade/i); + assert.match(workflow, /retention-days: 90/); + assert.match(workflow, /reports\/final-completion-dossier\.md/); + assert.match(workflow, /config\/release-approvals\.v1\.json/); + assert.match(workflow, /if: inputs\.promote_public/); }); test("owner action sheet ends with the fail-closed gate and limited launch", () => { diff --git a/tests/baseline/ross-deliverable-g.test.mjs b/tests/baseline/ross-deliverable-g.test.mjs index 97fd0d8eef..5be5457765 100644 --- a/tests/baseline/ross-deliverable-g.test.mjs +++ b/tests/baseline/ross-deliverable-g.test.mjs @@ -21,18 +21,21 @@ test("Deliverable G records verified public registration without expanding the d }); test("public deployment is manual, gated, verified-email only, and separately reversible", () => { - const publicWorkflow = read(".github/workflows/deploy-public-beta-ross.yml"); + const publicWorkflow = read(".github/workflows/verify-and-deploy-public-beta.yml"); + const releaseTrain = read("scripts/fly-release-train.mjs"); + const imageBuild = read("scripts/build-release-train-images.sh"); const privateWorkflow = read(".github/workflows/deploy-private-ross.yml"); assert.match(publicWorkflow, /workflow_dispatch:/); assert.doesNotMatch(publicWorkflow, /^\s*push:/m); assert.match(publicWorkflow, /environment: public-beta/); - assert.match(publicWorkflow, /verify-final-release-id\.mjs/); - assert.match(publicWorkflow, /npm run final:check/); - assert.match(publicWorkflow, /ROSS_REQUIRE_VERIFIED_EMAIL=true/); - assert.match(publicWorkflow, /NEXT_PUBLIC_ROSS_SIGNUPS_ENABLED=true/); - assert.match(publicWorkflow, /RATE_LIMIT_CHAT_MAX=20/); - assert.match(publicWorkflow, /--no-cache/); + assert.match(publicWorkflow, /validate-release-id\.mjs/); + assert.match(publicWorkflow, /if: inputs\.promote_public/); + assert.match(releaseTrain, /ROSS_REQUIRE_VERIFIED_EMAIL: "true"/); + assert.match(releaseTrain, /RATE_LIMIT_CHAT_MAX: "20"/); + assert.match(releaseTrain, /--flycast/); + assert.match(imageBuild, /--no-cache/); + assert.match(imageBuild, /NEXT_PUBLIC_ROSS_SIGNUPS_ENABLED=\$\{RELEASE_SIGNUPS_ENABLED:-true\}/); assert.match(privateWorkflow, /NEXT_PUBLIC_ROSS_SIGNUPS_ENABLED=false/); }); diff --git a/tests/baseline/ross-full-catalogue-public-update.test.mjs b/tests/baseline/ross-full-catalogue-public-update.test.mjs index c28a9fe006..c9d1053743 100644 --- a/tests/baseline/ross-full-catalogue-public-update.test.mjs +++ b/tests/baseline/ross-full-catalogue-public-update.test.mjs @@ -76,20 +76,11 @@ test("the release train rehearses by default and generates release IDs automatic assert.match(workflow, /release-train-ledger\.json/); }); -test("the older public deployment workflow is clearly legacy and hard blocked", () => { - const workflow = read(".github/workflows/deploy-public-beta-ross.yml"); - assert.match(workflow, /name: "Legacy: deploy previously governed public beta"/); - assert.match(workflow, /Legacy public deployment is disabled/); - assert.match(workflow, /Run ROSS release train instead/); - assert.match(workflow, /preflight:/); - assert.match(workflow, /preflight:[\s\S]*?if: \$\{\{ false \}\}/); - assert.match(workflow, /needs: preflight/); -}); test("the combined public update expressly excludes the private-only defect set", () => { const instructions = read("docs/deployment/public-beta-combined-update.md"); assert.match(instructions, /private-ROSS OpenAI-key/); assert.match(instructions, /not part of this update/); assert.match(instructions, /No database migration or new secret is required/); - assert.match(instructions, /Do not run the older standalone hotfix/); + assert.match(instructions, /former standalone public\s+deployment path has been removed/); }); diff --git a/tests/baseline/ross-hosted-runtime.test.mjs b/tests/baseline/ross-hosted-runtime.test.mjs index 2d75939f96..c593b98bcf 100644 --- a/tests/baseline/ross-hosted-runtime.test.mjs +++ b/tests/baseline/ross-hosted-runtime.test.mjs @@ -124,7 +124,7 @@ test("release checks reject high dependency advisories and deployments retry tra assert.match(retry, /FLY_DEPLOY_ATTEMPTS:-3/); for (const path of [ ".github/workflows/deploy-private-ross.yml", - ".github/workflows/deploy-public-beta-ross.yml", + "scripts/fly-release-train.mjs", ]) { assert.match(read(path), /scripts\/fly-deploy-with-retry\.sh/); } diff --git a/tests/baseline/ross-production-readiness.test.mjs b/tests/baseline/ross-production-readiness.test.mjs index f19adf51aa..1c20299965 100644 --- a/tests/baseline/ross-production-readiness.test.mjs +++ b/tests/baseline/ross-production-readiness.test.mjs @@ -72,13 +72,15 @@ test("release manifest governs code, schema, evaluation, sources, workflows, and assert.equal(manifest.artifactCount, manifest.artifacts.length); }); -test("release candidate workflow creates evidence but never deploys", () => { - const workflow = read(".github/workflows/release-candidate.yml"); +test("release train creates evidence and keeps public promotion explicit", () => { + const workflow = read(".github/workflows/verify-and-deploy-public-beta.yml"); assert.match(workflow, /workflow_dispatch/); assert.match(workflow, /npm run check/); + assert.match(workflow, /if: inputs\.promote_public/); assert.match(workflow, /upload-artifact@v7/); - assert.doesNotMatch(workflow, /\bdeploy(?:ment)?\s*:/i); - assert.doesNotMatch(workflow, /wrangler deploy|kubectl|helm upgrade/i); + assert.match(workflow, /retention-days: 90/); + assert.match(workflow, /reports\/final-completion-dossier\.md/); + assert.match(workflow, /config\/launch-readiness\.v1\.json/); }); test("production operations are documented without expanding the beta data boundary", () => { diff --git a/tests/baseline/supabase-upload-scan-pipeline.test.mjs b/tests/baseline/supabase-upload-scan-pipeline.test.mjs index 7785e9fe7c..1c53792b4d 100644 --- a/tests/baseline/supabase-upload-scan-pipeline.test.mjs +++ b/tests/baseline/supabase-upload-scan-pipeline.test.mjs @@ -47,15 +47,18 @@ test("private worker scans before structure validation, promotion, or conversion test("deployments reach the private scale-to-zero worker through the Flycast service port", () => { const privateWorkflow = read(".github/workflows/deploy-private-ross.yml"); - const publicWorkflow = read(".github/workflows/deploy-public-beta-ross.yml"); + const publicWorkflow = read("scripts/fly-release-train.mjs"); const fly = read("deploy/fly/file-worker.toml"); - for (const workflow of [privateWorkflow, publicWorkflow]) { - assert.match(workflow, /--flycast/); - assert.match(workflow, /WORKER_URL="http:\/\/\$\{WORKER_APP\}\.flycast"/); + assert.match(privateWorkflow, /--flycast/); + assert.match(privateWorkflow, /WORKER_URL="http:\/\/\$\{WORKER_APP\}\.flycast"/); + assert.match(privateWorkflow, /ROSS_UPLOAD_SCAN_REQUIRED=true/); + assert.match(privateWorkflow, /ROSS_SECURITY_ALERT_WEBHOOK_URL/); + assert.match(publicWorkflow, /--flycast/); + assert.match(publicWorkflow, /FILE_WORKER_URL: `http:\/\/\$\{apps\.stageWorker\}\.flycast`/); + assert.match(publicWorkflow, /ROSS_UPLOAD_SCAN_REQUIRED:\s+"true"/); + assert.match(publicWorkflow, /ROSS_SECURITY_ALERT_WEBHOOK_URL/); + for (const workflow of [privateWorkflow, publicWorkflow]) assert.doesNotMatch(workflow, /\.flycast:3002/); - assert.match(workflow, /ROSS_UPLOAD_SCAN_REQUIRED=true/); - assert.match(workflow, /ROSS_SECURITY_ALERT_WEBHOOK_URL/); - } assert.match(fly, /primary_region = "yyz"/); assert.match(fly, /auto_stop_machines = "stop"/); assert.match(fly, /min_machines_running = 0/); diff --git a/website/app/page-content.ts b/website/app/page-content.ts index 6b756f9e22..f64c635801 100644 --- a/website/app/page-content.ts +++ b/website/app/page-content.ts @@ -403,7 +403,7 @@ export const publicPages: Record = { summary: "ROSS has executable engineering gates, recorded independent reviews, named ownership, approved public domains and contacts, completed live-environment exercises, verified launch vendors, and effective notices.", status: - "Ready for immutable-candidate generation and final controlled-beta evidence. Public deployment has not yet been performed, and the synthetic/non-confidential boundary remains binding.", + "Ready for immutable-candidate generation and release-train evidence. Public deployment has not yet been performed, and the synthetic/non-confidential boundary remains binding.", governance: ownerApproved("Release owner — AR; product owner — Abhi Ranade"), sections: [ { @@ -412,7 +412,7 @@ export const publicPages: Record = { }, { title: "What remains", - body: "The reviews, operational exercises, vendor disclosure, effective notices, source observation, and owner decisions are recorded. The remaining release sequence is to commit the evidence closure, run the complete candidate gate, create immutable evidence, run the final controlled-beta evidence workflow, and deploy the approved tag.", + body: "The reviews, operational exercises, vendor disclosure, effective notices, source observation, and owner decisions are recorded. The remaining release sequence is to commit the evidence closure, run the complete release-train gate, retain its 90-day evidence, and deploy only after explicit public promotion approval.", }, { title: "Current safe boundary",