7932bc4c48
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 5s
CI / Detect changes (pull_request) Successful in 9s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 31s
E2E API Smoke Test / detect-changes (pull_request) Successful in 9s
E2E Chat / detect-changes (pull_request) Successful in 8s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 4s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 8s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 8s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 10s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 6s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m19s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 1m19s
CI / Platform (Go) (pull_request) Successful in 5m12s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 3s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m28s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 12s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 4s
gate-check-v3 / gate-check (pull_request) Failing after 4s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m23s
qa-review / approved (pull_request) Failing after 5s
sop-checklist / na-declarations (pull_request) N/A: (none)
security-review / approved (pull_request) Failing after 5s
sop-checklist / review-refire (pull_request) Has been skipped
sop-tier-check / tier-check (pull_request) Successful in 4s
sop-checklist / all-items-acked (pull_request) Successful in 4s
CI / Canvas (Next.js) (pull_request) Successful in 6m8s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m22s
CI / Python Lint & Test (pull_request) Successful in 6m54s
CI / all-required (pull_request) Successful in 6m27s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2s
E2E Chat / E2E Chat (pull_request) Successful in 3s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 10s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 10s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
audit-force-merge / audit (pull_request) Successful in 4s
Per CTO directive 2026-05-20 and task #347 (disabled GitHub-mirror push fleet-wide), .github/workflows/ on molecule-core is dead — Gitea Actions reads .gitea/workflows/ exclusively (memory: reference_molecule_core_actions_gitea_only), and GitHub Actions has had no real push activity since 2026-05-06 (the only post-2026-05-06 runs are dynamic CodeQL re-runs on frozen pre-suspension PRs). Empirical validation: - 24 files total in .github/workflows/. - 23 have same-name siblings in .gitea/workflows/ (port carries "Ported from .github/workflows/X on 2026-05-11 per RFC internal#219" header on most files). - 1 .github-only file: canary-staging.yml — already ported to .gitea/workflows/staging-smoke.yml on 2026-05-11 per the same RFC, Hongming directive renamed canary→smoke. Verified via header comment in staging-smoke.yml. - Last GitHub-side push event: 2026-05-06T07:06:12Z (pre-suspension). - All 24 .github/workflows/* files removed. Tooling updates needed (load-bearing): - tools/branch-protection/check_name_parity.sh: hard-coded $REPO_ROOT/.github/workflows path → switched to .gitea/workflows. Pre-existing parity findings (3x Analyze CodeQL names absent from any workflow file) are unchanged — that drift exists pre-PR and is out-of-scope (file as follow-up). - tools/branch-protection/test_check_name_parity.sh: synthetic test fixtures now create .gitea/workflows/ instead of .github/workflows/. All 6 unit tests pass after change. - .gitea/workflows/lint-required-workflows-docker-host-pinned.yml: dropped '.github/workflows/**' from path-filter triggers + dropped '.github/workflows' from the python directory-walk loop (the isdir-check would have made this a no-op cleanly, but pruning reflects current truth). Out-of-scope (NOT touched in this PR): - .github/CODEOWNERS, .github/dependabot.yml, .github/scripts/ remain (task is scoped to .github/workflows/). - COVERAGE_FLOOR.md, workspace/smoke_mode.py, workspace/main.py contain comment references to .github/workflows/* — stale docs string-references only, not behavioral. Separate follow-up. - Provenance comments inside .gitea/workflows/* of the form "Ported from .github/workflows/X on 2026-05-11" are intentionally preserved — useful history. Refs: task #331 (SSOT-Instance-4), task #347 (mirror push disabled), memory reference_molecule_core_actions_gitea_only, memory reference_per_repo_gitea_vs_github_actions_dir, RFC internal#219 §1 (the original 2026-05-11 port sweep).
165 lines
7.2 KiB
YAML
165 lines
7.2 KiB
YAML
name: lint-required-workflows-docker-host-pinned
|
|
|
|
# Fail-closed lint that catches workflows touching docker.sock without
|
|
# pinning `runs-on:` to a Linux-only label.
|
|
#
|
|
# Class defect (internal#512 + mc#1529 + today's oc#81/82/83 + autogen#8):
|
|
# the `ubuntu-latest` label is advertised by BOTH the Linux operator-host
|
|
# runners (molecule-runner-*) AND the Windows act_runner v1.0.3 on
|
|
# hongming-pc-runner-*. Job placement is non-deterministic. When a docker-
|
|
# bound job lands on a Windows runner, `docker run`/`docker login`/
|
|
# `docker compose` fail with platform-specific errors ("protocol not
|
|
# available", "cannot exec", etc.) — placement-dependent, not transient.
|
|
#
|
|
# This lint enforces the convention: any workflow whose YAML body
|
|
# contains a docker exec (`docker run|build|buildx|compose|pull|push|
|
|
# exec|tag|login|cp|inspect|ps` OR `docker/build-push-action|docker/
|
|
# login-action|docker/setup-buildx`) MUST pin every job's `runs-on:` to
|
|
# one of:
|
|
# - docker-host (general docker.sock work — molecule-runner-*)
|
|
# - publish (image build/push — molecule-runner-publish-*)
|
|
#
|
|
# Comments and heredoc/markdown bodies that merely MENTION docker are
|
|
# excluded by the detection rule (see scan.py below).
|
|
#
|
|
# Per `feedback_never_skip_ci`: this is fail-closed (exit 1 on miss).
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- '.gitea/workflows/**'
|
|
push:
|
|
branches: [main, staging]
|
|
paths:
|
|
- '.gitea/workflows/**'
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
GITHUB_SERVER_URL: https://git.moleculesai.app
|
|
|
|
jobs:
|
|
lint-docker-host-pin:
|
|
name: Lint docker-host pin on docker-touching workflows
|
|
runs-on: docker-host
|
|
timeout-minutes: 5
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Scan workflows for docker-bound jobs missing docker-host/publish pin
|
|
run: |
|
|
set -euo pipefail
|
|
python3 - <<'PY'
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
# Docker-step detection: real exec, not just word-mention in comments.
|
|
# We strip comment-only lines, then look for the docker subcommand
|
|
# tokens at word-boundary, OR uses: docker/* actions.
|
|
DOCKER_EXEC = re.compile(
|
|
r'(?<!\w)docker\s+(run|build|buildx|compose|pull|push|exec|tag|login|cp|inspect|ps)\b'
|
|
)
|
|
DOCKER_ACTION = re.compile(
|
|
r'uses:\s*docker/(build-push-action|login-action|setup-buildx-action|setup-qemu-action)'
|
|
)
|
|
# Detect a job header line like ` myjob:` (2-space indent) AND its runs-on.
|
|
JOB_HEADER = re.compile(r'^( {2})([a-zA-Z0-9_-]+):\s*$')
|
|
RUNS_ON = re.compile(r'^( {4})runs-on:\s*(.+?)\s*$')
|
|
|
|
ALLOWED_LABELS = {'docker-host', 'publish'}
|
|
|
|
fails = []
|
|
warnings = []
|
|
|
|
# Gitea is SSOT for molecule-core CI per task #347 / memory
|
|
# reference_molecule_core_actions_gitea_only. The legacy
|
|
# .github/workflows/ tree was deleted in SSOT-Instance-4 (#331).
|
|
roots = []
|
|
for root in ('.gitea/workflows',):
|
|
if os.path.isdir(root):
|
|
roots.append(root)
|
|
|
|
for root in roots:
|
|
for fn in sorted(os.listdir(root)):
|
|
if not (fn.endswith('.yml') or fn.endswith('.yaml')):
|
|
continue
|
|
path = os.path.join(root, fn)
|
|
with open(path) as f:
|
|
raw_lines = f.readlines()
|
|
|
|
# Parse job headers + their runs-on. Simple line scan; relies on
|
|
# 2-space job indent + 4-space runs-on indent under `jobs:`.
|
|
jobs = []
|
|
current = None
|
|
in_jobs = False
|
|
for i, line in enumerate(raw_lines, 1):
|
|
if re.match(r'^jobs:\s*$', line):
|
|
in_jobs = True
|
|
continue
|
|
if not in_jobs:
|
|
continue
|
|
mh = JOB_HEADER.match(line)
|
|
if mh:
|
|
if current:
|
|
current['end'] = i - 1
|
|
jobs.append(current)
|
|
current = {'name': mh.group(2), 'line': i, 'end': len(raw_lines), 'runs_on': None}
|
|
continue
|
|
mr = RUNS_ON.match(line)
|
|
if mr and current and current['runs_on'] is None:
|
|
current['runs_on'] = mr.group(2).strip()
|
|
if current:
|
|
jobs.append(current)
|
|
|
|
for j in jobs:
|
|
# Strip pure-comment lines for docker-exec detection so
|
|
# documentation comments don't trigger the lint. Scan the
|
|
# current job body only: a workflow may contain one
|
|
# docker-bound job and several harmless metadata jobs.
|
|
job_lines = raw_lines[j['line'] - 1:j['end']]
|
|
scan_text = ''.join(
|
|
l for l in job_lines
|
|
if not re.match(r'^\s*#', l)
|
|
)
|
|
has_docker = bool(DOCKER_EXEC.search(scan_text)) or bool(DOCKER_ACTION.search(scan_text))
|
|
if not has_docker:
|
|
continue
|
|
ro = j['runs_on']
|
|
if ro is None:
|
|
# Reusable workflow caller (`uses:` instead of `runs-on:`) —
|
|
# skip; rule enforced in the called workflow.
|
|
continue
|
|
# Strip surrounding [ ] and quotes.
|
|
ro_norm = ro.strip('[]').strip().strip('"\'')
|
|
# Multi-label "[a, b]" — split.
|
|
labels = [t.strip().strip('"\'') for t in ro_norm.split(',') if t.strip()]
|
|
if any(lbl in ALLOWED_LABELS for lbl in labels):
|
|
continue
|
|
# Allow caller-supplied label expressions; spell the
|
|
# marker indirectly so Gitea's expression parser does
|
|
# not try to parse this Python heredoc.
|
|
expression_marker = '$' + '{{'
|
|
if any(expression_marker in lbl for lbl in labels):
|
|
continue
|
|
fails.append(
|
|
f"{path}:{j['line']}: job `{j['name']}` uses docker but runs-on={ro!r} "
|
|
f"(must be one of {sorted(ALLOWED_LABELS)})"
|
|
)
|
|
|
|
if fails:
|
|
print("FAIL: docker-bound jobs missing docker-host/publish pin:")
|
|
for f in fails:
|
|
print(f" - {f}")
|
|
print()
|
|
print("Why this rule exists (internal#512 + mc#1529):")
|
|
print(" Bare `ubuntu-latest` is advertised by BOTH Linux operator-host")
|
|
print(" runners AND Windows hongming-pc-runner-* (act_runner v1.0.3).")
|
|
print(" Docker-bound jobs that land on Windows fail non-deterministically.")
|
|
print(" Pin to `docker-host` (general) or `publish` (image build/push).")
|
|
sys.exit(1)
|
|
|
|
print("OK: all docker-bound jobs are pinned to docker-host or publish.")
|
|
PY
|