Files
molecule-core/.gitea/workflows/gitea-merge-queue.yml
T
release-manager 4469e34033
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 18s
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (pull_request) Successful in 17s
CI / Python Lint & Test (pull_request) Successful in 17s
CI / Detect changes (pull_request) Successful in 25s
E2E API Smoke Test / detect-changes (pull_request) Successful in 24s
Concierge Creates Workspace Hermetic / Concierge Creates Workspace Hermetic (pull_request) Successful in 26s
ci-required-drift / drift (pull_request) Successful in 31s
E2E Ephemeral CP Happy Path / detect (pull_request) Successful in 23s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Successful in 15s
E2E Staging SaaS (full lifecycle) / E2E Staging Platform Boot (pull_request) Successful in 16s
E2E Peer Visibility (literal MCP list_peers) / detect-changes (pull_request) Successful in 22s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 28s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 28s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge user_tasks (pull_request) Successful in 29s
E2E Staging SaaS (full lifecycle) / E2E Staging Workspace Requests (core#2606) (pull_request) Successful in 31s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge (compile+skip) (pull_request) Successful in 30s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 24s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Creates Workspace (pull_request) Successful in 34s
E2E Staging SaaS (full lifecycle) / E2E Staging Plugin Install Lifecycle (pull_request) Successful in 31s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 19s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 16s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Platform Agent (pull_request) Successful in 38s
Lint forbidden hand-written mcp__ tool-id literals / Scan for hand-written mcp__ tool-id literals (pull_request) Successful in 17s
lint-bp-context-emit-match / lint-bp-context-emit-match (pull_request) Successful in 34s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 21s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 34s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 18s
lint-phantom-gate / lint-phantom-gate (pull_request) Successful in 30s
lint-no-coe-on-required / lint-no-coe-on-required (pull_request) Successful in 34s
Lint publish-runner timeout-minutes / Lint publish-runner timeout-minutes (pull_request) Successful in 29s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 31s
lint-staging-tenant-cd-gate-chain / lint-staging-tenant-cd-gate-chain (pull_request) Successful in 24s
Lint schedule budget / Zero-cron ratchet (pull_request) Successful in 36s
lint-env-coupling-dismissal / lint-env-coupling-dismissal (pull_request) Successful in 52s
lint-setup-go-cache / lint-setup-go-cache (pull_request) Successful in 34s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 47s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 46s
Lint workflow YAML (repository compatibility policy) / Lint workflow YAML for repository compatibility policy (pull_request) Successful in 29s
sdk-route-milestone-contract-drift / SDK route + milestone derive-gates (pull_request) Successful in 25s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 17s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 13s
E2E Staging SaaS (full lifecycle) / Prune stale e2e DNS records (pull_request) Successful in 16s
template-delivery-e2e / detect-changes (pull_request) Successful in 26s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 16s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 14s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 58s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 35s
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (pull_request) Successful in 14s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 59s
E2E Chat / E2E Chat (pull_request) Successful in 2m28s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 40s
CI / Platform (Go) (pull_request) Successful in 3m48s
CI / Canvas (Next.js) (pull_request) Successful in 4m43s
CI / Canvas Deploy Status (pull_request) Successful in 12s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6m43s
CI / all-required (pull_request) Successful in 15s
E2E Ephemeral CP Happy Path / E2E Ephemeral CP Happy Path (pull_request) Has been cancelled
refactor(governance): share reviewer policy SSOT
2026-07-18 12:28:48 -07:00

209 lines
12 KiB
YAML

name: gitea-merge-queue
# External serialized merge queue for this repository's exact-head approval,
# status, and stale-review policy.
#
# Gitea's `pull_auto_merge` does not implement this repository's exact-head,
# one-at-a-time validation policy. This workflow runs the user-space queue bot,
# one PR per tick, using the non-bypass merge actor.
#
# Queue contract:
# - auto-discovery (default): any open same-repo PR is considered — no
# `merge-queue` label required (the label is optional metadata now)
# - bot updates stale PR heads with current main, then waits for CI
# - bot merges only when current main is green, genuine approvals are present
# on the current head, required PR contexts pass, and the PR is mergeable
# - under wildcard branch protection, the Actions job's own in-flight
# `gitea-merge-queue / queue` status is a gate-runner, not a product gate;
# after the final live checks pass, the bot reasserts only that exact
# context as success and requires Gitea to confirm it before the ordinary
# protected merge request. Any status-write failure aborts the merge
# - add `merge-queue-hold`, `do-not-auto-merge`, or `wip` to keep a PR OUT of
# autonomous merging; draft PRs are also skipped
on:
# Schedule moved to operator-config:
# /etc/cron.d/molecule-core-merge-queue ->
# /usr/local/bin/molecule-core-cron-bot.sh merge-queue
#
# The queue bot still processes one PR per tick, but no longer occupies
# one of the shared Actions runners just to poll.
workflow_dispatch:
# Deliberately no `inputs:` block under the repository's flat-dispatch
# policy. The dry-run knob is derived from the step-level
# `DRY_RUN` env var below (default "true" = safe evaluate-only) rather than
# a dispatch input. See .gitea/scripts/lint-workflow-yaml.py Rule 1 and
# memory feedback_gitea_workflow_dispatch_inputs_unsupported.
# EVENT-DRIVEN automerge — completes the dispatch-only -> event-driven
# transition begun by the cron strip (pairs #3344). Fire the queue the MOMENT
# a PR could become newly mergeable, with NO cron and NO polling:
# - pull_request_review [submitted]: a new approval just landed. Re-run the
# queue so an approved+green PR merges immediately instead of waiting for
# the conductor's in-process pass. On this event the run step below takes
# the LIVE-merge branch (event_name != workflow_dispatch), while a manual
# dispatch still defaults to the safe `--enumerate --dry-run` path.
#
# The full merge bar is UNCHANGED and still enforced by gitea-merge-queue.py
# (genuine non-author approval on the CURRENT head, BP-required CI green, no
# open REQUEST_CHANGES, reserved-path author!=merger). This changes only WHEN
# the queue runs, never WHETHER a PR merges. The exact runner-status
# reconciliation above exists only because Gitea also applies the wildcard
# rule to the currently running gate job; it is not a force/admin merge and
# cannot rewrite any product or test context (core#4420).
#
# FORK SAFETY: on a pull_request_review event the job still checks out the
# default branch for the queue script (never PR-head), so no attacker code
# runs even though the merge-actor token is fetched — the same BASE-ref trust
# boundary qa-review.yml / security-review.yml rely on for this exact event.
#
# NOTE: `status:` / `check_suite:` are deliberately NOT added — Gitea (1.26.4)
# does not recognise them as Actions trigger events (verified against
# go-gitea modules/actions/github.go: no GithubEvent constant for "status",
# "check_suite", or "check_run"), so they would register but fire for ZERO
# events — the lint-workflow-yaml Rule 2 silent-no-op failure mode. The
# "CI just turned green" re-trigger stays on the conductor in-process pass /
# a CI->dispatch bridge; it cannot be a native Gitea event.
pull_request_review:
types: [submitted]
permissions:
contents: read
concurrency:
group: gitea-merge-queue-${{ github.repository }}
cancel-in-progress: false
jobs:
queue:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check out queue script from main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.repository.default_branch }}
- name: Fetch AUTO_SYNC_TOKEN from Infisical SSOT
# Secret-source consolidation (Infisical SSOT). The scoped non-bypass
# merge credential is pulled from the Infisical
# SSOT at /shared/dev-utils::AUTO_SYNC_TOKEN (environment=prod) instead
# of the duplicated `secrets.AUTO_SYNC_TOKEN` Gitea Actions secret.
# Exported to $GITHUB_ENV as AUTO_SYNC_TOKEN and consumed as env.* by
# "Process one queued PR" below. The old Gitea secret stays in place
# until a green run proves this fetch (validate-before-delete).
#
# FORK SAFETY: this workflow now also runs on pull_request_review
# (event-driven automerge), but the queue script is still checked out
# at the default branch (never a PR head), so the merge-actor token is
# only ever consumed by trusted base-ref code — even on a fork review
# event. The conditional below is the belt-and-suspenders guard for any
# pull_request-family trigger.
id: auto_sync_token
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
env:
INFISICAL_CI_CLIENT_ID: ${{ secrets.INFISICAL_CI_CLIENT_ID }}
INFISICAL_CI_CLIENT_SECRET: ${{ secrets.INFISICAL_CI_CLIENT_SECRET }}
INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_CI_PROJECT_ID }}
run: |
set -uo pipefail
BASE="https://key.moleculesai.app"
TOK=$(curl -fsS -X POST "$BASE/api/v1/auth/universal-auth/login" \
-H 'Content-Type: application/json' \
-d "{\"clientId\":\"$INFISICAL_CI_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CI_CLIENT_SECRET\"}" \
| python3 -c 'import sys,json; d=json.load(sys.stdin); v=d.get("accessToken"); sys.stdout.write(v if isinstance(v,str) and v else "")')
read_secret() {
curl -fsS "$BASE/api/v3/secrets/raw/$1?workspaceId=$INFISICAL_PROJECT_ID&environment=prod&secretPath=$2" \
-H "Authorization: Bearer $TOK" \
| python3 -c 'import sys,json; d=json.load(sys.stdin); v=(d.get("secret") or {}).get("secretValue"); sys.stdout.write(v if isinstance(v,str) and v else "")'
}
AUTO_SYNC_TOKEN=$(read_secret AUTO_SYNC_TOKEN %2Fshared%2Fdev-utils)
echo "::add-mask::$AUTO_SYNC_TOKEN"
if [ -z "$AUTO_SYNC_TOKEN" ]; then
echo "::error::Infisical returned empty for AUTO_SYNC_TOKEN at /shared/dev-utils"
exit 1
fi
echo "AUTO_SYNC_TOKEN=$AUTO_SYNC_TOKEN" >> "$GITHUB_ENV"
echo "AUTO_SYNC_TOKEN loaded from Infisical (len=${#AUTO_SYNC_TOKEN})"
- name: Process one queued PR
env:
# AUTO_SYNC_TOKEN is the scoped non-bypass merge credential allowed
# by branch protection. It is sourced
# from the Infisical SSOT by the fetch step above (exported to
# $GITHUB_ENV as AUTO_SYNC_TOKEN -> hence env.* here).
# It intentionally has no write:issue capability. If a protected
# merge is refused and the optional hold label therefore cannot be
# persisted, the script exits non-success so the queue status itself
# is a durable fail-closed signal; it never reports an unheld refusal
# as green (internal#1082).
GITEA_TOKEN: ${{ env.AUTO_SYNC_TOKEN }}
GITEA_HOST: git.moleculesai.app
REPO: ${{ github.repository }}
WATCH_BRANCH: ${{ github.event.repository.default_branch }}
QUEUE_LABEL: merge-queue
HOLD_LABEL: merge-queue-hold
# Auto-discovery (opt-OUT). When on (default), the queue considers ALL
# open same-repo PRs that meet the merge bar — it does NOT wait for a
# human/agent to add `merge-queue`. Agent Gitea tokens lack
# write:issue (labels are issue-scoped) and could never self-label,
# which stalled the queue; the label is now OPTIONAL metadata. The
# merge bar is UNCHANGED — only candidate selection widens. Set
# AUTO_DISCOVER=0 to restore legacy opt-IN (require the merge-queue
# label to be considered).
AUTO_DISCOVER: "1"
# Opt-OUT labels: any of these on a PR keeps it OUT of autonomous
# merging (the human escape hatch). HOLD_LABEL is always also honoured.
# A human who wants a PR held just adds one of these labels.
OPT_OUT_LABELS: do-not-auto-merge,wip
UPDATE_STYLE: merge
# Recognised reviewer roster and minimum approval floor are loaded
# from `.gitea/scripts/_review_policy.py`, the checked-in SSOT shared
# with the post-merge audit. Do not duplicate either value here.
# CTO 2026-07-14: reviewers + management + owners may all satisfy the
# floor (union of teams code-reviewers/qa/security/managers/Owners).
# The prior {agent-reviewer,agent-researcher,agent-reviewer-cr2} were
# undriven accounts that produced 0 genuine approvals across 29 merges,
# so the queue could never auto-merge. See gitea-merge-queue.py for the
# full rationale + the team-keyed follow-up.
# NOTE: REQUIRED_CONTEXTS is no longer the authoritative PR gate. The
# queue now reads the required status contexts from BRANCH PROTECTION
# (status_check_contexts) so non-required governance reds (qa-review,
# security-review, sop-checklist when not branch-required,
# E2E Chat, Staging SaaS) cannot block a merge.
# If branch protection cannot be enumerated the queue HOLDS
# (fail-closed). REQUIRED_APPROVALS is the genuine-peer floor; BP wins
# when it specifies a valid int >= this value.
# CTO 2026-07-14: floor = 1 (matches .gitea/security-reviewer.md's
# documented required_approvals=1; the prior 2 was drift that, with the
# dead REVIEWER_SET, wedged the queue).
# Push-side required contexts. Checking CI / all-required (push)
# explicitly instead of the combined state avoids false-pause when
# non-blocking jobs (continue-on-error: true) have failed — those
# failures pollute combined state but do not gate merges.
# NOTE: the event-suffixed context name is intentional — branch protection
# MUST require `CI / all-required (pull_request)` (with suffix), NOT the
# bare `CI / all-required`. Gitea treats absent contexts as pending, not
# skipped; requiring the bare name silently blocks all merges (issue #1473).
PUSH_REQUIRED_CONTEXTS: CI / all-required (push)
# Safe-by-default dry-run knob under the flat-dispatch policy.
# DEFAULT "true" => a manual
# dispatch evaluates every candidate and prints a readiness summary
# WITHOUT merging (`--enumerate --dry-run`). Flip to "false" (edit
# here, or wire to a repo/org Actions variable) ONLY to deliberately
# run a real live merge tick. The scheduled/automatic cron path
# (operator-config) does not use this trigger and stays LIVE.
DRY_RUN: "true"
# SAFE manual dispatch: when this run was started from the UI/API and
# DRY_RUN is not "false" (the default), evaluate every candidate and
# print a readiness summary WITHOUT merging (`--enumerate --dry-run`).
# Any other trigger (and DRY_RUN=false) runs the bare script in LIVE
# MERGE mode, so the scheduled/automatic path is byte-equivalent to before.
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${DRY_RUN}" != "false" ]; then
echo "::notice::dry_run=true — evaluate-only (no merges): --enumerate --dry-run"
python3 .gitea/scripts/gitea-merge-queue.py --enumerate --dry-run
else
python3 .gitea/scripts/gitea-merge-queue.py
fi