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
209 lines
12 KiB
YAML
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
|