From 10dc98112ccfdc24fd129381f10c4d471b887546 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-DevOps Date: Thu, 14 May 2026 03:57:27 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix(ci):=20rename=20sop-checklist-gate?= =?UTF-8?q?=E2=86=92sop-checklist=20to=20match=20BP=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mc#948 (BP→emitter drift): `sop-checklist / all-items-acked (pull_request)` was required by branch protection but the workflow was named `sop-checklist-gate`, so it emitted the misnamed context `sop-checklist-gate / gate (pull_request)` instead. Rename to align the workflow's `name:` field with the context that BP requires: sop-checklist-gate.yml → sop-checklist.yml sop-checklist-gate.py → sop-checklist.py test_sop_checklist_gate.py → test_sop_checklist.py New context emitted: `sop-checklist / all-items-acked (pull_request)`. Added `# bp-required: yes` directive to the workflow header per Tier 2g lint convention (mc#774). All 52 script tests pass. Closes #948. Co-Authored-By: Claude Opus 4.7 --- .gitea/scripts/review-check.sh | 2 +- .../{sop-checklist-gate.py => sop-checklist.py} | 4 ++-- ...t_sop_checklist_gate.py => test_sop_checklist.py} | 8 ++++---- .gitea/sop-checklist-config.yaml | 2 +- .gitea/workflows/review-refire-comments.yml | 2 +- .../{sop-checklist-gate.yml => sop-checklist.yml} | 12 +++++++----- 6 files changed, 16 insertions(+), 14 deletions(-) rename .gitea/scripts/{sop-checklist-gate.py => sop-checklist.py} (99%) rename .gitea/scripts/tests/{test_sop_checklist_gate.py => test_sop_checklist.py} (98%) rename .gitea/workflows/{sop-checklist-gate.yml => sop-checklist.yml} (95%) diff --git a/.gitea/scripts/review-check.sh b/.gitea/scripts/review-check.sh index ef238e36..5bc00448 100755 --- a/.gitea/scripts/review-check.sh +++ b/.gitea/scripts/review-check.sh @@ -145,7 +145,7 @@ if [ -z "$PR_AUTHOR" ] || [ -z "$PR_HEAD_SHA" ]; then fi # --- RFC#324 §N/A follow-up: check N/A declarations status --- -# sop-checklist-gate.py posts `sop-checklist / na-declarations (pull_request)` +# sop-checklist.py posts `sop-checklist / na-declarations (pull_request)` # status when a peer posts /sop-n/a . If our gate is declared N/A, # the requirement for a Gitea APPROVE review is waived. NA_STATUSES_TMP=$(mktemp) diff --git a/.gitea/scripts/sop-checklist-gate.py b/.gitea/scripts/sop-checklist.py similarity index 99% rename from .gitea/scripts/sop-checklist-gate.py rename to .gitea/scripts/sop-checklist.py index e53c60b7..323b5126 100755 --- a/.gitea/scripts/sop-checklist-gate.py +++ b/.gitea/scripts/sop-checklist.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 -# sop-checklist-gate — evaluate whether a PR has peer-acked each +# sop-checklist — evaluate whether a PR has peer-acked each # SOP-checklist item. Posts a commit-status that branch protection # can require. # # RFC#351 Step 2 of 6 (implementation MVP). # -# Invoked by .gitea/workflows/sop-checklist-gate.yml on: +# Invoked by .gitea/workflows/sop-checklist.yml on: # - pull_request_target: [opened, edited, synchronize, reopened] # - issue_comment: [created, edited, deleted] # diff --git a/.gitea/scripts/tests/test_sop_checklist_gate.py b/.gitea/scripts/tests/test_sop_checklist.py similarity index 98% rename from .gitea/scripts/tests/test_sop_checklist_gate.py rename to .gitea/scripts/tests/test_sop_checklist.py index 47ae4f23..24fbc54c 100644 --- a/.gitea/scripts/tests/test_sop_checklist_gate.py +++ b/.gitea/scripts/tests/test_sop_checklist.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -# Unit tests for sop-checklist-gate.py +# Unit tests for sop-checklist.py # -# Run: python3 .gitea/scripts/tests/test_sop_checklist_gate.py -# or: pytest .gitea/scripts/tests/test_sop_checklist_gate.py +# Run: python3 .gitea/scripts/tests/test_sop_checklist.py +# or: pytest .gitea/scripts/tests/test_sop_checklist.py # # RFC#351 Step 2 of 6 — implementation MVP. Tests cover: # - slug normalization (the 4 example variants in the script header) @@ -33,7 +33,7 @@ sys.path.insert(0, PARENT) import importlib.util # noqa: E402 _spec = importlib.util.spec_from_file_location( - "sop_checklist_gate", os.path.join(PARENT, "sop-checklist-gate.py") + "sop_checklist", os.path.join(PARENT, "sop-checklist.py") ) sop = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(sop) # type: ignore[union-attr] diff --git a/.gitea/sop-checklist-config.yaml b/.gitea/sop-checklist-config.yaml index 3b61605d..346d231f 100644 --- a/.gitea/sop-checklist-config.yaml +++ b/.gitea/sop-checklist-config.yaml @@ -111,7 +111,7 @@ items: # N/A gate declarations (RFC#324 §N/A follow-up). # PRs where a gate genuinely does not apply (e.g., pure-infra with no # qa surface, or docs-only) can be declared N/A by a non-author peer -# who is in one of the gate's required_teams. The sop-checklist-gate +# who is in one of the gate's required_teams. The sop-checklist # posts a `sop-checklist / na-declarations (pull_request)` status that # review-check.sh reads to skip the Gitea-APPROVE requirement. # diff --git a/.gitea/workflows/review-refire-comments.yml b/.gitea/workflows/review-refire-comments.yml index 97eb1371..c799c442 100644 --- a/.gitea/workflows/review-refire-comments.yml +++ b/.gitea/workflows/review-refire-comments.yml @@ -2,7 +2,7 @@ # # Gitea 1.22 queues one run per workflow subscribed to `issue_comment` before # evaluating job-level `if:`. SOP-heavy PRs therefore created queue storms when -# qa-review, security-review, sop-checklist-gate, and sop-tier-refire all +# qa-review, security-review, sop-checklist, and sop-tier-refire all # listened to comments. This workflow is the single non-SOP comment subscriber: # ordinary comments no-op quickly; slash commands post the required status # contexts to the PR head SHA. diff --git a/.gitea/workflows/sop-checklist-gate.yml b/.gitea/workflows/sop-checklist.yml similarity index 95% rename from .gitea/workflows/sop-checklist-gate.yml rename to .gitea/workflows/sop-checklist.yml index 3fd3ba81..72e33f04 100644 --- a/.gitea/workflows/sop-checklist-gate.yml +++ b/.gitea/workflows/sop-checklist.yml @@ -1,4 +1,4 @@ -# sop-checklist-gate — peer-ack merge gate for SOP-checklist items. +# sop-checklist — peer-ack merge gate for SOP-checklist items. # # RFC#351 Step 2 of 6 (implementation MVP). # @@ -65,7 +65,9 @@ # membership, compute, post status). Re-running on any event is safe — # the new status overwrites the previous one for the same context. -name: sop-checklist-gate +name: sop-checklist + +# bp-required: yes ← emits sop-checklist / all-items-acked (pull_request) on: pull_request_target: @@ -83,7 +85,7 @@ permissions: statuses: write jobs: - gate: + all-items-acked: # Run on pull_request_target events always. On issue_comment events, # only when the comment is on a PR (issue_comment fires for issues # too) and the body contains one of the slash-commands. @@ -106,7 +108,7 @@ jobs: # qa-review.yml so the script source is always trusted. ref: ${{ github.event.repository.default_branch }} - - name: Run sop-checklist-gate + - name: Run sop-checklist env: GITEA_TOKEN: ${{ secrets.SOP_CHECKLIST_GATE_TOKEN || secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} @@ -114,7 +116,7 @@ jobs: REPO_NAME: ${{ github.event.repository.name }} run: | set -euo pipefail - python3 .gitea/scripts/sop-checklist-gate.py \ + python3 .gitea/scripts/sop-checklist.py \ --owner "$OWNER" \ --repo "$REPO_NAME" \ --pr "$PR_NUMBER" \ -- 2.45.2 From 7250ebbed8f7b83610554f587ac8d00ab499b2a5 Mon Sep 17 00:00:00 2001 From: hongming-codex-laptop Date: Wed, 13 May 2026 21:00:52 -0700 Subject: [PATCH 2/3] ci: fix publish docker healthcheck pipefail --- .gitea/scripts/lint-workflow-yaml.py | 50 +++++++++++++++ .../publish-workspace-server-image.yml | 4 +- tests/test_lint_workflow_yaml.py | 64 +++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/.gitea/scripts/lint-workflow-yaml.py b/.gitea/scripts/lint-workflow-yaml.py index 34cfefe6..bd254c90 100755 --- a/.gitea/scripts/lint-workflow-yaml.py +++ b/.gitea/scripts/lint-workflow-yaml.py @@ -36,6 +36,9 @@ Rules (4 fatal + 1 fatal cross-file + 1 heuristic-warn): raw `.error` fields into CI logs/summaries. 9. Production deploy/redeploy workflows must expose an operational control: kill switch for auto deploys or rollback tag for manual deploys. + 10. Docker health checks must not run `docker info | head` under pipefail. + `head` closes the pipe early, `docker info` can exit nonzero from + SIGPIPE, and the step can falsely report Docker daemon failure. Per `feedback_smoke_test_vendor_truth_not_shape_match`: fixtures used to validate this lint must mirror real Gitea 1.22.6 YAML semantics, not @@ -225,6 +228,24 @@ def _iter_uses(doc: Any) -> Iterable[str]: yield step["uses"] +def _iter_run_blocks(doc: Any) -> Iterable[str]: + """Yield every shell `run:` block from job steps in a workflow document.""" + if not isinstance(doc, dict): + return + jobs = doc.get("jobs") + if not isinstance(jobs, dict): + return + for job in jobs.values(): + if not isinstance(job, dict): + continue + steps = job.get("steps") + if not isinstance(steps, list): + continue + for step in steps: + if isinstance(step, dict) and isinstance(step.get("run"), str): + yield step["run"] + + def check_cross_repo_uses(filename: str, doc: Any) -> list[str]: """Return per-violation error lines for cross-repo `uses:` references.""" errors: list[str] = [] @@ -264,6 +285,10 @@ GITHUB_API_REF_RE = re.compile( PROD_CP_URL_RE = re.compile(r"https://api\.moleculesai\.app\b") REDEPLOY_FLEET_RE = re.compile(r"\b/cp/admin/tenants/redeploy-fleet\b") +RUN_SETS_PIPEFAIL_RE = re.compile(r"(?m)^\s*set\s+-[^\n]*o\s+pipefail\b") +DOCKER_INFO_HEAD_PIPE_RE = re.compile( + r"(?m)^\s*docker\s+info\b[^\n|]*\|\s*head\b" +) RAW_CP_RESPONSE_RE = re.compile( r"""(?x) (?:\bjq\s+\.\s+["']?\$HTTP_RESPONSE["']?) @@ -383,6 +408,30 @@ def check_production_operational_control(filename: str, raw: str) -> list[str]: return errors +# --------------------------------------------------------------------------- +# Rule 10 — docker info piped to head under pipefail +# --------------------------------------------------------------------------- + +def check_docker_info_head_pipefail(filename: str, doc: Any) -> list[str]: + errors: list[str] = [] + for run_block in _iter_run_blocks(doc): + if not ( + RUN_SETS_PIPEFAIL_RE.search(run_block) + and DOCKER_INFO_HEAD_PIPE_RE.search(run_block) + ): + continue + errors.append( + f"::error file={filename}::Rule 10 (FATAL): workflow runs " + f"`docker info | head` after enabling `pipefail`. `head` can " + f"close the pipe early, making `docker info` exit nonzero and " + f"falsely fail the Docker daemon health check. Capture " + f"`docker_info=\"$(docker info 2>&1)\"` first, then print a " + f"bounded preview with `printf ... | sed -n '1,5p'`." + ) + break + return errors + + # --------------------------------------------------------------------------- # Driver # --------------------------------------------------------------------------- @@ -436,6 +485,7 @@ def main(argv: list[str] | None = None) -> int: fatal_errors.extend(check_production_concurrency(rel, doc, raw)) fatal_errors.extend(check_production_raw_response_logging(rel, raw)) fatal_errors.extend(check_production_operational_control(rel, raw)) + fatal_errors.extend(check_docker_info_head_pipefail(rel, doc)) warnings.extend(check_github_server_url_missing(rel, doc, raw)) # Cross-file checks diff --git a/.gitea/workflows/publish-workspace-server-image.yml b/.gitea/workflows/publish-workspace-server-image.yml index 25012dcf..02a42962 100644 --- a/.gitea/workflows/publish-workspace-server-image.yml +++ b/.gitea/workflows/publish-workspace-server-image.yml @@ -68,12 +68,14 @@ jobs: set -euo pipefail echo "::group::Docker daemon health check" echo "Runner: ${HOSTNAME:-unknown}" - docker info 2>&1 | head -5 || { + docker_info="$(docker info 2>&1)" || { echo "::error::Docker daemon is not accessible at /var/run/docker.sock" echo "::error::Runner: ${HOSTNAME:-unknown}" + printf '%s\n' "${docker_info}" echo "::error::Check: (1) daemon is running, (2) runner user is in docker group, (3) sock permissions are 660+" exit 1 } + printf '%s\n' "${docker_info}" | sed -n '1,5p' echo "Docker daemon OK" echo "::endgroup::" diff --git a/tests/test_lint_workflow_yaml.py b/tests/test_lint_workflow_yaml.py index 4cd4b151..18b67060 100644 --- a/tests/test_lint_workflow_yaml.py +++ b/tests/test_lint_workflow_yaml.py @@ -545,6 +545,70 @@ def test_rule9_prod_manual_deploy_allows_rollback_control(tmp_path): assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" +# --------------------------------------------------------------------------- +# Rule 10 — docker info piped to head under pipefail +# --------------------------------------------------------------------------- + +DOCKER_INFO_HEAD_BAD = """ + name: docker-info-head-bad + on: [push] + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: | + set -euo pipefail + docker info 2>&1 | head -5 || exit 1 +""" + +DOCKER_INFO_CAPTURE_OK = """ + name: docker-info-capture-ok + on: [push] + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: | + set -euo pipefail + docker_info="$(docker info 2>&1)" || exit 1 + printf '%s\\n' "${docker_info}" | sed -n '1,5p' +""" + +DOCKER_INFO_SEPARATE_STEP_OK = """ + name: docker-info-separate-step-ok + on: [push] + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: | + set -euo pipefail + echo setup + - run: | + docker info 2>&1 | head -5 || true +""" + + +def test_rule10_docker_info_head_under_pipefail_detects_violation(tmp_path): + _write(tmp_path, "bad.yml", DOCKER_INFO_HEAD_BAD) + r = _run_lint(tmp_path) + assert r.returncode == 1 + assert "docker info" in r.stdout.lower() + assert "pipefail" in r.stdout.lower() + + +def test_rule10_docker_info_capture_passes(tmp_path): + _write(tmp_path, "ok.yml", DOCKER_INFO_CAPTURE_OK) + r = _run_lint(tmp_path) + assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" + + +def test_rule10_docker_info_head_in_separate_step_without_pipefail_passes(tmp_path): + _write(tmp_path, "ok.yml", DOCKER_INFO_SEPARATE_STEP_OK) + r = _run_lint(tmp_path) + assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" + + # --------------------------------------------------------------------------- # CI change detector fanout — workflow-only PRs keep required contexts without # running Go/Canvas/Python/shellcheck heavy steps. -- 2.45.2 From 725869834e5e773904937a90b7d2b7baf750e64e Mon Sep 17 00:00:00 2001 From: molecule-operator Date: Thu, 14 May 2026 04:30:57 +0000 Subject: [PATCH 3/3] fix(ci): add concurrency block to sop-checklist workflow (cancel stale runs) --- .gitea/workflows/sop-checklist.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitea/workflows/sop-checklist.yml b/.gitea/workflows/sop-checklist.yml index 72e33f04..fe86219f 100644 --- a/.gitea/workflows/sop-checklist.yml +++ b/.gitea/workflows/sop-checklist.yml @@ -67,6 +67,12 @@ name: sop-checklist +# Cancel any in-progress runs for the same PR to prevent +# stale runs from overwriting newer status contexts. +concurrency: + group: ${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + # bp-required: yes ← emits sop-checklist / all-items-acked (pull_request) on: -- 2.45.2