From 427d5b04ed29803d3c9c34230099e64467651ba4 Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Fri, 8 May 2026 17:55:10 -0700 Subject: [PATCH 1/3] ci(sop-tier-check): deploy workflow to molecule-core (soft-launch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase-1 fan-out of §SOP-6 enforcement to molecule-core. No branch protection change in this PR — workflow runs and reports a status, doesn't block any merge yet. Branch protection update is the follow-up PR after the workflow demonstrates a green run on its own PR, per the Phase 2 plan. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitea/workflows/sop-tier-check.yml | 120 ++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 .gitea/workflows/sop-tier-check.yml diff --git a/.gitea/workflows/sop-tier-check.yml b/.gitea/workflows/sop-tier-check.yml new file mode 100644 index 00000000..be46d29d --- /dev/null +++ b/.gitea/workflows/sop-tier-check.yml @@ -0,0 +1,120 @@ +# sop-tier-check — canonical Gitea Actions workflow for §SOP-6 enforcement. +# +# Copy this file to `.gitea/workflows/sop-tier-check.yml` in any repo that +# wants the §SOP-6 PR gate enforced. Pair with branch protection on the +# protected branch: +# required_status_checks: ["sop-tier-check"] +# required_approving_reviews: 1 +# approving_review_teams: ["ceo", "managers", "engineers"] +# +# What it does: +# 1. Reads the PR's `tier:*` label (low | medium | high). Fails if absent +# or ambiguous. +# 2. Reads every approving review on the PR. +# 3. For each approver, queries Gitea team membership. +# 4. Marks the check success only if at least one approver is in a team +# whose tier-tag covers the PR's tier label, AND the approver is not +# the author. +# +# Tier → eligible-team mapping (mirror of dev-sop §SOP-6): +# tier:low → engineers, managers, ceo +# tier:medium → managers, ceo +# tier:high → ceo +# +# Author identity is excluded automatically; Gitea's review system already +# rejects self-reviews, but this workflow re-checks defensively in case the +# native rule is bypassed (admin override, branch-protection edit, etc.). +# +# Force-merge: Owners-team override remains available out-of-band via the +# Gitea merge API; force-merge writes `incident.force_merge` to +# structure_events per §Persistent structured logging gate (Phase 3). + +name: sop-tier-check + +on: + pull_request: + types: [opened, edited, synchronize, reopened, labeled, unlabeled] + pull_request_review: + types: [submitted, dismissed, edited] + +jobs: + tier-check: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + steps: + - name: Verify tier label + reviewer team membership + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_HOST: ${{ vars.GITEA_HOST || 'git.moleculesai.app' }} + REPO: ${{ gitea.repository }} + PR_NUMBER: ${{ gitea.event.pull_request.number }} + PR_AUTHOR: ${{ gitea.event.pull_request.user.login }} + run: | + set -euo pipefail + OWNER="${REPO%%/*}" + NAME="${REPO##*/}" + API="https://${GITEA_HOST}/api/v1" + AUTH="Authorization: token ${GITEA_TOKEN}" + + # 1. Read tier label + LABELS=$(curl -sS -H "$AUTH" "${API}/repos/${OWNER}/${NAME}/issues/${PR_NUMBER}/labels" | jq -r '.[].name') + TIER="" + for L in $LABELS; do + case "$L" in + tier:low|tier:medium|tier:high) + if [ -n "$TIER" ]; then + echo "::error::Multiple tier labels: $TIER + $L. Apply exactly one." + exit 1 + fi + TIER="$L" + ;; + esac + done + if [ -z "$TIER" ]; then + echo "::error::PR has no tier:low|tier:medium|tier:high label. Apply one before merge." + exit 1 + fi + echo "tier=$TIER" + + # 2. Tier → eligible teams + case "$TIER" in + tier:low) ELIGIBLE="engineers managers ceo" ;; + tier:medium) ELIGIBLE="managers ceo" ;; + tier:high) ELIGIBLE="ceo" ;; + esac + echo "eligible_teams=$ELIGIBLE" + + # 3. Read approving reviewers + REVIEWS=$(curl -sS -H "$AUTH" "${API}/repos/${OWNER}/${NAME}/pulls/${PR_NUMBER}/reviews") + APPROVERS=$(echo "$REVIEWS" | jq -r '[.[] | select(.state=="APPROVED") | .user.login] | unique | .[]') + if [ -z "$APPROVERS" ]; then + echo "::error::No approving reviews. Tier $TIER requires approval from {$ELIGIBLE} (non-author)." + exit 1 + fi + + # 4. For each approver: check non-author + team membership + OK="" + for U in $APPROVERS; do + if [ "$U" = "$PR_AUTHOR" ]; then + echo "skip self-review by $U" + continue + fi + for T in $ELIGIBLE; do + CODE=$(curl -sS -o /dev/null -w '%{http_code}' -H "$AUTH" \ + "${API}/orgs/${OWNER}/teams/${T}/members/${U}") + if [ "$CODE" = "200" ] || [ "$CODE" = "204" ]; then + echo "approver $U is in team $T (eligible for $TIER)" + OK="yes" + break + fi + done + [ -n "$OK" ] && break + done + + if [ -z "$OK" ]; then + echo "::error::Tier $TIER requires approval from a non-author member of {$ELIGIBLE}. Got approvers: $APPROVERS" + exit 1 + fi + echo "::notice::sop-tier-check passed: $TIER, approver in {$ELIGIBLE}" From 4534e922c87a43ab3f8c10abdf34b8dff19bbf76 Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Fri, 8 May 2026 17:56:14 -0700 Subject: [PATCH 2/3] trigger: re-run after dev-lead approval From a526dabf046e33bf777f1ec3a408f57a8102055c Mon Sep 17 00:00:00 2001 From: claude-ceo-assistant Date: Fri, 8 May 2026 17:59:43 -0700 Subject: [PATCH 3/3] ci(sop-tier-check): update to latest canonical (team-id resolution + scope-aware probe) --- .gitea/workflows/sop-tier-check.yml | 68 +++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/sop-tier-check.yml b/.gitea/workflows/sop-tier-check.yml index be46d29d..db720f2f 100644 --- a/.gitea/workflows/sop-tier-check.yml +++ b/.gitea/workflows/sop-tier-check.yml @@ -46,17 +46,38 @@ jobs: steps: - name: Verify tier label + reviewer team membership env: - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} - GITEA_HOST: ${{ vars.GITEA_HOST || 'git.moleculesai.app' }} - REPO: ${{ gitea.repository }} - PR_NUMBER: ${{ gitea.event.pull_request.number }} - PR_AUTHOR: ${{ gitea.event.pull_request.user.login }} + # SOP_TIER_CHECK_TOKEN is the read-only `sop-tier-bot` PAT, + # provisioned with read:org scope and added to ceo/managers/ + # engineers teams (a Gitea team-membership probe requires the + # caller to be a member of the team being probed). The auto- + # injected GITHUB_TOKEN's scope is repo-level only and cannot + # query org team membership, hence the dedicated secret. + # Falls back to GITHUB_TOKEN so the workflow at least starts and + # surfaces a clear error when the secret is missing. + GITEA_TOKEN: ${{ secrets.SOP_TIER_CHECK_TOKEN || secrets.GITHUB_TOKEN }} + GITEA_HOST: git.moleculesai.app + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} run: | set -euo pipefail + if [ -z "${GITEA_TOKEN:-}" ]; then + echo "::error::Neither GITEA_TOKEN nor GITHUB_TOKEN is available. Add a GITEA_TOKEN secret with org-membership read scope to enable team-based approval gating." + exit 1 + fi OWNER="${REPO%%/*}" NAME="${REPO##*/}" API="https://${GITEA_HOST}/api/v1" AUTH="Authorization: token ${GITEA_TOKEN}" + echo "::notice::tier-check start: repo=$OWNER/$NAME pr=$PR_NUMBER author=$PR_AUTHOR" + # Sanity-check the token resolves a user; surfaces token-scope problems + # early instead of failing on a downstream call with no context. + WHOAMI=$(curl -sS -H "$AUTH" "${API}/user" | jq -r '.login // ""') + if [ -z "$WHOAMI" ]; then + echo "::error::GITEA_TOKEN cannot resolve a user via /api/v1/user — check the token scope and that the secret is wired correctly." + exit 1 + fi + echo "::notice::token resolves to user: $WHOAMI" # 1. Read tier label LABELS=$(curl -sS -H "$AUTH" "${API}/repos/${OWNER}/${NAME}/issues/${PR_NUMBER}/labels" | jq -r '.[].name') @@ -86,6 +107,32 @@ jobs: esac echo "eligible_teams=$ELIGIBLE" + # Resolve team-name → team-id once. The /orgs/{org}/teams/{slug}/... + # endpoints don't exist on Gitea 1.22; we have to use /teams/{id}. + # Fail loud on missing team rather than treating it as "user not in + # team" — that'd mask a misconfigured deployment. + ORG_TEAMS_FILE=$(mktemp) + HTTP_CODE=$(curl -sS -o "$ORG_TEAMS_FILE" -w '%{http_code}' -H "$AUTH" \ + "${API}/orgs/${OWNER}/teams") + echo "teams-list HTTP=$HTTP_CODE size=$(wc -c <"$ORG_TEAMS_FILE")" + echo "teams-list body (first 300 chars):" + head -c 300 "$ORG_TEAMS_FILE"; echo + if [ "$HTTP_CODE" != "200" ]; then + echo "::error::GET /orgs/${OWNER}/teams returned HTTP $HTTP_CODE — token likely lacks read:org scope. Add a SOP_TIER_CHECK_TOKEN secret with read:organization scope." + exit 1 + fi + declare -A TEAM_ID + for T in $ELIGIBLE; do + ID=$(jq -r --arg t "$T" '.[] | select(.name==$t) | .id' <"$ORG_TEAMS_FILE" | head -1) + if [ -z "$ID" ] || [ "$ID" = "null" ]; then + VISIBLE=$(jq -r '.[]?.name? // empty' <"$ORG_TEAMS_FILE" 2>/dev/null | tr '\n' ' ') + echo "::error::Team \"$T\" not found in org $OWNER. Teams visible: $VISIBLE" + exit 1 + fi + TEAM_ID[$T]="$ID" + echo "team-id: $T → $ID" + done + # 3. Read approving reviewers REVIEWS=$(curl -sS -H "$AUTH" "${API}/repos/${OWNER}/${NAME}/pulls/${PR_NUMBER}/reviews") APPROVERS=$(echo "$REVIEWS" | jq -r '[.[] | select(.state=="APPROVED") | .user.login] | unique | .[]') @@ -93,8 +140,9 @@ jobs: echo "::error::No approving reviews. Tier $TIER requires approval from {$ELIGIBLE} (non-author)." exit 1 fi + echo "approvers: $(echo $APPROVERS | tr '\n' ' ')" - # 4. For each approver: check non-author + team membership + # 4. For each approver: check non-author + team membership (by id) OK="" for U in $APPROVERS; do if [ "$U" = "$PR_AUTHOR" ]; then @@ -102,10 +150,12 @@ jobs: continue fi for T in $ELIGIBLE; do + ID="${TEAM_ID[$T]}" CODE=$(curl -sS -o /dev/null -w '%{http_code}' -H "$AUTH" \ - "${API}/orgs/${OWNER}/teams/${T}/members/${U}") + "${API}/teams/${ID}/members/${U}") + echo " probe: $U in team $T (id=$ID) → HTTP $CODE" if [ "$CODE" = "200" ] || [ "$CODE" = "204" ]; then - echo "approver $U is in team $T (eligible for $TIER)" + echo "::notice::approver $U is in team $T (eligible for $TIER)" OK="yes" break fi @@ -114,7 +164,7 @@ jobs: done if [ -z "$OK" ]; then - echo "::error::Tier $TIER requires approval from a non-author member of {$ELIGIBLE}. Got approvers: $APPROVERS" + echo "::error::Tier $TIER requires approval from a non-author member of {$ELIGIBLE}. Got approvers: $APPROVERS — none of them satisfied team membership (probe HTTP codes above)." exit 1 fi echo "::notice::sop-tier-check passed: $TIER, approver in {$ELIGIBLE}"