From 6d3cf556bf115e6bd91e39b87cd506892b6b4d86 Mon Sep 17 00:00:00 2001 From: devops-engineer Date: Wed, 17 Jun 2026 14:50:03 -0700 Subject: [PATCH] =?UTF-8?q?ci(delivery-e2e):=20flip=20to=20merge-blocking?= =?UTF-8?q?=20=E2=80=94=20fail-closed=20gate=20(#37=20/=20mc#2996=20Phase?= =?UTF-8?q?=202b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2a (f6155d68) hardened the asset assertions and banked a green main run; lint-pre-flip-continue-on-error now permits the flip. Make template-delivery-e2e a REQUIRED, fail-closed gate: - Remove `continue-on-error` — a real delivery regression now FAILS the job. - Remove the `on: paths:` filter (a required workflow must not be path-filtered — lint-required-no-paths / feedback_path_filtered_workflow_ cant_be_required would wedge docs-only PRs on a perpetual 'pending'). - Move path-scoping into a detect-changes job (new `template-delivery` profile in detect-changes.py) applied per-step, mirroring the e2e-api / peer-visibility required-gate shape: a non-delivery PR runs only the no-op step and emits SUCCESS cheaply (no provision); a delivery PR runs the full e2e and BLOCKS on failure. One always-running job → exactly one check run (no SKIPPED-check branch-protection trap). - Add the emitted context to .gitea/required-contexts.txt (SSOT). - detect-changes (new emitter) carries bp-exempt; delivery carries bp-required: yes. Branch protection required_status_checks add (the '... (pull_request)' context) is performed out-of-band AFTER this lands on main, so PRs whose branch still carries the path-filtered workflow aren't phantom-blocked. detect-changes regex unit-checked (delivery paths → true; docs/canvas/ a2a_proxy → false); 60 meta-lint unit tests + detect-changes tests green; lint_no_coe_on_required sees 6 required contexts, none with COE. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/required-contexts.txt | 1 + .gitea/scripts/detect-changes.py | 29 ++++ .gitea/workflows/template-delivery-e2e.yml | 146 ++++++++++++--------- 3 files changed, 111 insertions(+), 65 deletions(-) diff --git a/.gitea/required-contexts.txt b/.gitea/required-contexts.txt index c39275be..c9072037 100644 --- a/.gitea/required-contexts.txt +++ b/.gitea/required-contexts.txt @@ -12,3 +12,4 @@ E2E API Smoke Test / E2E API Smoke Test Handlers Postgres Integration / Handlers Postgres Integration E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility Secret scan / Scan diff for credential-shaped strings +template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) diff --git a/.gitea/scripts/detect-changes.py b/.gitea/scripts/detect-changes.py index 63756391..adbbe1fb 100644 --- a/.gitea/scripts/detect-changes.py +++ b/.gitea/scripts/detect-changes.py @@ -74,6 +74,35 @@ PROFILES: dict[str, dict[str, str]] = { r"|^\.gitea/workflows/e2e-peer-visibility\.yml$" ), }, + # mc#2996 / RFC#2843 #37: the template-delivery e2e is being flipped to a + # REQUIRED status check. A required-check workflow may NOT carry an `on: + # paths:` filter (lint-required-no-paths.py / feedback_path_filtered_ + # workflow_cant_be_required would wedge docs-only PRs), so the path-scoping + # that used to live in template-delivery-e2e.yml's `on:` block moves here + # and is applied per-step inside the always-running job (mirrors the + # peer-visibility shape). This set MUST mirror the old `on: paths:` list — + # the delivery surface (provisioning asset channel + post-online plugin + # reconcile) whose regressions this gate exists to catch. + "template-delivery": { + "delivery": ( + r"^workspace-server/internal/provisioner/template_assets\.go$" + r"|^workspace-server/internal/provisioner/gitea_template_assets\.go$" + r"|^workspace-server/internal/provisioner/cp_provisioner\.go$" + r"|^workspace-server/internal/handlers/platform_agent\.go$" + r"|^workspace-server/cmd/server/main\.go$" + r"|^workspace-server/internal/handlers/org_import\.go$" + r"|^workspace-server/internal/handlers/workspace\.go$" + r"|^workspace-server/internal/handlers/template_plugins\.go$" + r"|^workspace-server/internal/handlers/plugins_reconcile\.go$" + r"|^workspace-server/internal/handlers/registry\.go$" + r"|^workspace-server/internal/handlers/plugins_install_pipeline\.go$" + r"|^workspace-server/internal/handlers/plugins_tracking\.go$" + r"|^workspace-server/internal/plugins/source\.go$" + r"|^manifest\.json$" + r"|^tests/e2e/test_template_delivery_e2e\.sh$" + r"|^\.gitea/workflows/template-delivery-e2e\.yml$" + ), + }, } diff --git a/.gitea/workflows/template-delivery-e2e.yml b/.gitea/workflows/template-delivery-e2e.yml index 1b51990c..dd22d406 100644 --- a/.gitea/workflows/template-delivery-e2e.yml +++ b/.gitea/workflows/template-delivery-e2e.yml @@ -13,99 +13,115 @@ name: template-delivery-e2e # /configs/plugins/seo-all/. The e2e asserts the skill arrives via the # PLUGIN channel and NOT the asset channel (negative control). # -# STAGED ROLLOUT (do NOT make required until green): -# Phase 1 (done): advisory — runs on the relevant paths + main + dispatch. -# Asserts the new two-channel contract. -# Phase 2a (THIS change, mc#2996): HARDEN the asset-channel assertions so the -# gate is reliable enough to make required. The C (config.yaml) -# + D (prompts) checks now poll within E2E_ASSET_SETTLE_SECS: a -# freshly-online tenant's /configs inspection endpoint can be -# transiently slow / time out the first read (the 9c2161d red was -# `curl: (28) ... 0 bytes` → config read as size 0, a FALSE stub, -# not a real delivery failure). A genuine stub still fails after -# the budget. continue-on-error STAYS in this PR — the flip is -# gated by lint-pre-flip-continue-on-error on recent green main -# runs, which this hardening produces. -# Phase 2b (follow-up, after 2a is green on main): remove continue-on-error and -# add the emitted context to branch protection -# required_status_checks → a delivery regression is merge-blocking. +# STAGED ROLLOUT (now COMPLETE — this gate is merge-blocking): +# Phase 1 (done): advisory — asserted the new two-channel contract. +# Phase 2a (done, f6155d68): HARDENED the asset-channel assertions (C +# config.yaml, D prompts) to poll within E2E_ASSET_SETTLE_SECS, +# killing the false stub from a transient /configs read +# (`curl: (28) ... 0 bytes`). Banked a green main run, which +# lint-pre-flip-continue-on-error requires before the flip. +# Phase 2b (THIS change, mc#2996): FLIP to merge-blocking — +# • continue-on-error removed → a real delivery regression fails; +# • `on: paths:` removed (required workflows must not be +# path-filtered); path-scoping moved to the detect-changes job +# (profile `template-delivery`) and applied per-step; +# • the emitted context is added to .gitea/required-contexts.txt +# and to branch-protection required_status_checks (as +# "... (pull_request)") so a delivery PR cannot merge unless a +# fresh seo-agent provisions and BOTH channels verify. # # Cost: provisions ONE throwaway tenant + ONE seo-agent (real EC2), teardown -# trap deletes the org even on failure. Path-filtered so it only runs when the -# delivery code actually changes. +# trap deletes the org even on failure. The workflow has NO `on: paths:` filter: +# a REQUIRED-check workflow must not carry one (lint-required-no-paths.py / +# feedback_path_filtered_workflow_cant_be_required — a docs-only PR would never +# emit the context, Gitea reports it `pending`, and the PR wedges forever). +# Instead the detect-changes job applies the same path-scoping at RUNTIME, and +# the delivery job's real steps are gated on its output: a non-delivery PR emits +# SUCCESS cheaply (no provision), while a delivery PR runs the full e2e and +# BLOCKS on failure. Mirrors the e2e-api / peer-visibility required-gate shape. on: workflow_dispatch: {} push: branches: [main] - paths: - - 'workspace-server/internal/provisioner/template_assets.go' - - 'workspace-server/internal/provisioner/gitea_template_assets.go' - - 'workspace-server/internal/provisioner/cp_provisioner.go' - - 'workspace-server/internal/handlers/platform_agent.go' - - 'workspace-server/cmd/server/main.go' - - 'workspace-server/internal/handlers/org_import.go' - - 'workspace-server/internal/handlers/workspace.go' - - 'workspace-server/internal/handlers/template_plugins.go' - - 'workspace-server/internal/handlers/plugins_reconcile.go' - - 'workspace-server/internal/handlers/registry.go' - - 'workspace-server/internal/handlers/plugins_install_pipeline.go' - - 'workspace-server/internal/handlers/plugins_tracking.go' - - 'workspace-server/internal/plugins/source.go' - - 'manifest.json' - - 'tests/e2e/test_template_delivery_e2e.sh' - - '.gitea/workflows/template-delivery-e2e.yml' pull_request: - paths: - - 'workspace-server/internal/provisioner/template_assets.go' - - 'workspace-server/internal/provisioner/gitea_template_assets.go' - - 'workspace-server/internal/provisioner/cp_provisioner.go' - - 'workspace-server/internal/handlers/platform_agent.go' - - 'workspace-server/cmd/server/main.go' - - 'workspace-server/internal/handlers/org_import.go' - - 'workspace-server/internal/handlers/workspace.go' - - 'workspace-server/internal/handlers/template_plugins.go' - - 'workspace-server/internal/handlers/plugins_reconcile.go' - - 'workspace-server/internal/handlers/registry.go' - - 'workspace-server/internal/handlers/plugins_install_pipeline.go' - - 'workspace-server/internal/handlers/plugins_tracking.go' - - 'workspace-server/internal/plugins/source.go' - - 'manifest.json' - - 'tests/e2e/test_template_delivery_e2e.sh' - - '.gitea/workflows/template-delivery-e2e.yml' + branches: [main] concurrency: - group: template-delivery-e2e-${{ github.ref }} - cancel-in-progress: true + group: template-delivery-e2e-${{ github.event.pull_request.head.sha || github.sha }} + cancel-in-progress: false jobs: - # Job renamed for the RFC#2843 #32 two-channel contract (config+prompts via - # the asset channel; seo-all installs via the post-online plugin reconcile, - # not at boot). Renaming the job changes the emitted status context. - # bp-exempt: Phase 2a — still advisory (continue-on-error) while the hardened - # asset assertions bank green main runs; lint-pre-flip-continue-on-error then - # permits the Phase-2b flip + branch-protection add (mc#2996). + # Runtime path-scoping (replaces the removed `on: paths:`). Mirrors the + # e2e-api / peer-visibility detect-changes shape. Outputs `delivery=true` + # when the diff touches the delivery surface; the gate job runs the real e2e + # only then. bp-exempt: helper job; the REQUIRED context is the `delivery` + # job below, not this one. + detect-changes: + runs-on: ubuntu-latest + continue-on-error: false + outputs: + delivery: ${{ steps.decide.outputs.delivery }} + debug: ${{ steps.decide.outputs.debug }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - id: decide + env: + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + PUSH_BEFORE: ${{ github.event.before }} + run: | + python3 .gitea/scripts/detect-changes.py \ + --profile template-delivery \ + --event-name "${{ github.event_name }}" \ + --pr-base-sha "$PR_BASE_SHA" \ + --base-ref "$PR_BASE_REF" \ + --push-before "$PUSH_BEFORE" || { + # Script crash → fail OPEN so the gate runs rather than silently + # no-oping a potentially-breaking delivery PR. + echo "delivery=true" >> "$GITHUB_OUTPUT" + echo "debug=detect-script-error event=${{ github.event_name }}" >> "$GITHUB_OUTPUT" + exit 0 + } + echo "debug=profile=template-delivery event=${{ github.event_name }}" >> "$GITHUB_OUTPUT" + + # ONE job (no job-level `if:`) that ALWAYS runs and reports under the + # required-check name. Real work is gated per-step on + # `needs.detect-changes.outputs.delivery` — a non-delivery PR runs only the + # no-op step and emits SUCCESS (branch-protection-clean; a job-level `if:` + # would emit a SKIPPED check run that fails the required-check eval — see + # e2e-api.yml's PR#2264 note). A delivery PR runs the full e2e and, with no + # continue-on-error, BLOCKS the merge on failure. + # bp-required: yes — mc#2996 / RFC#2843 #37: this context is merge-blocking; + # branch protection lists " (pull_request)". delivery: + needs: detect-changes # No colon in the name — lint-required-context's PyYAML AST parse rejects an # unquoted scalar containing a colon. name: Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) runs-on: ubuntu-latest - # Phase 2a: STILL advisory. The flip to required (remove this line + add to - # branch protection) is Phase 2b, gated by lint-pre-flip-continue-on-error on - # the green main runs this hardening produces. mc#2996. - continue-on-error: true # mc#2996 timeout-minutes: 30 env: MOLECULE_CP_URL: ${{ vars.CP_URL || 'https://staging-api.moleculesai.app' }} MOLECULE_ADMIN_TOKEN: ${{ secrets.CP_ADMIN_API_TOKEN }} E2E_EXPECTED_MODEL: moonshot/kimi-k2.6 steps: - - uses: actions/checkout@v4 + - name: No-op pass (delivery surface unchanged in this diff) + if: needs.detect-changes.outputs.delivery != 'true' + run: | + echo "No delivery-surface changes — gate satisfied without provisioning." + echo "::notice::template-delivery-e2e no-op pass (detect-changes: ${{ needs.detect-changes.outputs.debug }})." + - if: needs.detect-changes.outputs.delivery == 'true' + uses: actions/checkout@v4 - name: Verify required secret present + if: needs.detect-changes.outputs.delivery == 'true' run: | if [ -z "${MOLECULE_ADMIN_TOKEN:-}" ]; then echo "::error::CP_ADMIN_API_TOKEN secret not set — cannot run delivery e2e" exit 2 fi - name: Run template-asset delivery e2e + if: needs.detect-changes.outputs.delivery == 'true' run: bash tests/e2e/test_template_delivery_e2e.sh -- 2.52.0