172 lines
7.7 KiB
YAML
172 lines
7.7 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 self-hosted Linux
|
|
# docker runners 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).
|
|
|
|
concurrency:
|
|
# Auto-cancel a superseded run: a newer commit/event supersedes the old
|
|
# one instead of leaving it queued forever (root fix for waiting-run pileup).
|
|
# PR events group by PR number; push events group by ref.
|
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: true
|
|
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:
|
|
# bp-exempt: informational lint enforcing docker-host/publish pin convention (internal#512), not a merge gate
|
|
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 self-hosted Linux")
|
|
print(" docker 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
|