Skip to content

Commit 9ad7654

Browse files
authored
Merge pull request #2606 from ContextualWisdomLab/ci/pg-erd-central-quality-20261008
ci(pg-erd): centralize quality on isolated self-hosted runners
2 parents 4e32b99 + 7ad86ae commit 9ad7654

3 files changed

Lines changed: 392 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
name: pg-erd-cloud quality
2+
3+
# Fixed consumer only. Keep concurrency in the calling workflow, not here.
4+
# Requires disposable isolated capacity and consumer repository access.
5+
on:
6+
workflow_call:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
backend:
13+
runs-on:
14+
group: CWL CI isolated
15+
labels: [self-hosted, linux, x64, cwlab-ci-isolated]
16+
timeout-minutes: 30
17+
steps:
18+
- name: Admit fixed caller before checkout
19+
env:
20+
CALLER_REPOSITORY: ${{ github.repository }}
21+
CALLER_EVENT: ${{ github.event_name }}
22+
PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
23+
PR_BASE_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name }}
24+
CALLER_REF: ${{ github.ref }}
25+
shell: bash --noprofile --norc -e -o pipefail {0}
26+
run: |
27+
if [ "$CALLER_REPOSITORY" != "ContextualWisdomLab/pg-erd-cloud" ]; then
28+
echo "Unsupported caller repository" >&2
29+
exit 1
30+
fi
31+
case "$CALLER_EVENT" in
32+
pull_request)
33+
if [ "$PR_HEAD_REPOSITORY" != "$CALLER_REPOSITORY" ] || [ "$PR_BASE_REPOSITORY" != "$CALLER_REPOSITORY" ]; then
34+
echo "Unsupported pull request source" >&2
35+
exit 1
36+
fi
37+
;;
38+
push|workflow_dispatch)
39+
if [ "$CALLER_REF" != "refs/heads/main" ]; then
40+
echo "Unsupported caller ref" >&2
41+
exit 1
42+
fi
43+
;;
44+
*) echo "Unsupported caller event" >&2; exit 1 ;;
45+
esac
46+
- name: Harden runner
47+
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
48+
with:
49+
egress-policy: audit
50+
51+
- name: Checkout
52+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
53+
with:
54+
persist-credentials: false
55+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
56+
57+
- name: Setup Python
58+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
59+
with:
60+
python-version: "3.10"
61+
62+
- name: Verify backend in a job-private environment
63+
shell: bash --noprofile --norc -e -o pipefail {0}
64+
run: |
65+
job_root="$(mktemp -d "${RUNNER_TEMP:?}/pg-erd-backend.XXXXXXXX")"
66+
trap 'rm -rf -- "$job_root"' EXIT
67+
unset PYTHONHOME PYTHONPATH
68+
export PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1
69+
export PIP_CONFIG_FILE=/dev/null
70+
python -m venv "$job_root/venv"
71+
export PATH="$job_root/venv/bin:$PATH"
72+
cd backend
73+
python -m pip install --require-hashes -r requirements-dev.lock
74+
python -m pip install --require-hashes -r ../.github/requirements.lock
75+
cd ..
76+
python -m unittest discover -s .github/tests -v
77+
python .github/scripts/validate_codeql_backfill.py
78+
actionlint .github/workflows/*.yml
79+
shellcheck .github/scripts/*.sh
80+
cd backend
81+
export PYTHONPATH=.
82+
mypy app
83+
pytest -q
84+
85+
frontend:
86+
# Persistent self-hosted hosts may inherit NODE_ENV=production.
87+
env:
88+
NODE_ENV: test
89+
runs-on:
90+
group: CWL CI isolated
91+
labels: [self-hosted, linux, x64, cwlab-ci-isolated]
92+
timeout-minutes: 30
93+
steps:
94+
- name: Admit fixed caller before checkout
95+
env:
96+
CALLER_REPOSITORY: ${{ github.repository }}
97+
CALLER_EVENT: ${{ github.event_name }}
98+
PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
99+
PR_BASE_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name }}
100+
CALLER_REF: ${{ github.ref }}
101+
shell: bash --noprofile --norc -e -o pipefail {0}
102+
run: |
103+
if [ "$CALLER_REPOSITORY" != "ContextualWisdomLab/pg-erd-cloud" ]; then
104+
echo "Unsupported caller repository" >&2
105+
exit 1
106+
fi
107+
case "$CALLER_EVENT" in
108+
pull_request)
109+
if [ "$PR_HEAD_REPOSITORY" != "$CALLER_REPOSITORY" ] || [ "$PR_BASE_REPOSITORY" != "$CALLER_REPOSITORY" ]; then
110+
echo "Unsupported pull request source" >&2
111+
exit 1
112+
fi
113+
;;
114+
push|workflow_dispatch)
115+
if [ "$CALLER_REF" != "refs/heads/main" ]; then
116+
echo "Unsupported caller ref" >&2
117+
exit 1
118+
fi
119+
;;
120+
*) echo "Unsupported caller event" >&2; exit 1 ;;
121+
esac
122+
- name: Harden runner
123+
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
124+
with:
125+
egress-policy: audit
126+
127+
- name: Checkout
128+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
129+
with:
130+
persist-credentials: false
131+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
132+
133+
- name: Setup Node
134+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
135+
with:
136+
node-version: "26"
137+
cache: "npm"
138+
cache-dependency-path: frontend/package-lock.json
139+
140+
- name: Install
141+
working-directory: frontend
142+
run: npm ci --include=dev
143+
144+
- name: Typecheck
145+
working-directory: frontend
146+
run: npm run typecheck
147+
148+
- name: Test
149+
working-directory: frontend
150+
run: npm run test
151+
152+
- name: Build
153+
working-directory: frontend
154+
env:
155+
NODE_ENV: production
156+
run: npm run build
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# pg-erd-cloud central quality workflow — candidate
2+
3+
## Owner requirement
4+
5+
The October 8, 2026 instruction requires central ContextualWisdomLab/.github
6+
ownership, self-hosted execution with no GitHub-hosted fallback, and genuine
7+
Noema/OpenCode exact-head review and independent bot approval. Model selection
8+
is `auto`. This change owns only the missing product validation reusable workflow;
9+
it does not duplicate the all-workflow routing candidate in #2565, the review
10+
reliability lane in #2574, or their secrets and publication handlers.
11+
12+
## Implementation
13+
14+
`.github/workflows/pg-erd-cloud-quality.yml` has fixed backend and frontend jobs.
15+
It declares only `workflow_call`, no caller-provided executable strings, no
16+
inherited secrets, and `contents: read`. Both jobs target `CWL CI isolated` and
17+
`[self-hosted, linux, x64, cwlab-ci-isolated]`. The first inline step rejects
18+
foreign repositories, forks, pull_request_target, unsupported events and
19+
non-main push/dispatch before any action or checkout. Same-repository PRs remain
20+
admitted, including stacked PRs. Actions use immutable SHA pins; checkout selects
21+
the caller head and disables persisted credentials.
22+
23+
Backend keeps Python 3.10, both product hash locks, automation contracts, mypy
24+
and pytest. A unique RUNNER_TEMP directory holds its venv, is cleaned on EXIT,
25+
and avoids installing candidate dependencies into shared runner Python. Frontend
26+
keeps Node 26, npm ci including dev dependencies, typecheck, tests, production
27+
build, and explicit NODE_ENV test/production. All phases remain failure-gating.
28+
29+
Keep concurrency in the caller only. The eventual thin caller has:
30+
31+
- name `ci`;
32+
- push on main and same-repository PR triggers;
33+
- contents read;
34+
- `ci-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }}`
35+
concurrency with PR-only cancellation;
36+
- one reusable job calling
37+
`ContextualWisdomLab/.github/.github/workflows/pg-erd-cloud-quality.yml@`
38+
followed by a real reviewed/published 40-character central commit SHA;
39+
- no `secrets: inherit` and no arbitrary inputs.
40+
41+
Do not activate a mutable branch caller or invent its not-yet-published SHA.
42+
Reusable invocation can change displayed required check names. Reconcile those
43+
through the protection owner without removing checks merely to unblock merge.
44+
45+
## Executed acceptance
46+
47+
Tests first failed for the missing workflow. The next test failed because the
48+
initial port installed Python dependencies globally. Job-private venv repair
49+
followed. Final local focused result: 24/24 new contracts plus 72/72 existing
50+
queue contracts, 96 passed. Actual admission shell was executed for 18 accepted
51+
and rejected tuples. The actual backend shell was exercised with synthetic tool
52+
executables for success, mypy failure and pytest failure: exit propagation,
53+
owned temporary cleanup and unrelated-directory preservation passed. This is
54+
shell/contract execution, not backend dependency installation or runner evidence.
55+
56+
Actionlint passed with the exact custom-label vocabulary supplied by #2565's
57+
`.github/actionlint.yaml` in a separate scratch config; that PR-owned file was
58+
not duplicated or edited. Default actionlint without custom-label config failed
59+
on the isolation label, and that failure must not be hidden as schema success.
60+
61+
## Fresh capacity observation — October 8, 2026
62+
63+
Group 13, `CWL CI isolated`, contains three runners: orgmetra-ci-01,
64+
keyverse-ci-01 and calendarweave-ci-01, all offline at the observed GET. Its five
65+
selected repositories are .github, CalendarWeave, fast-mlsirm, keyverse and
66+
Orgmetra. pg-erd-cloud is not selected. Other online runners do not establish
67+
this group's capacity and must not be relabeled or silently borrowed.
68+
69+
The existing operator lane is ContextualWisdomLab/linux-cluster-ops#326. Its
70+
isolation, custodian acceptance, runner registration, tool availability and
71+
cleanup canary remain required. This source change neither provisions a runner
72+
nor modifies ACLs. No remote job execution, model-backed approval, merge or
73+
hosted-only retirement is established by these local tests.
74+
75+
## Review/gateway boundary
76+
77+
Protected central workflows currently route Noema/OpenCode through the vendored
78+
contextual-orchestrator sidecar and `orchestrator/free`. The sidecar explicitly
79+
rejects auto in its current admission. Workflow model-string substitution alone
80+
is therefore incorrect. External LiteLLM routing needs a separately reviewed
81+
configuration/provider contract, least-privilege key, budget and private-target
82+
ZDR protection while preserving the existing independent bot publisher and
83+
exact-head verdict checks. The LiteLLM UI requested login; the secure credential
84+
save was declined, so no key was issued or read and no inference claim is made.
85+
86+
The implementing account must not self-approve. Only a real passing independent
87+
model verdict plus current-head publication authority permits formal approval.
88+
89+
## Primary technical reference
90+
91+
GitHub Docs, *Reuse workflows* and *Using self-hosted runners in a workflow*.
92+
Check the live documentation before activation. Group restriction and label
93+
matching are routing/access constraints, not proof of isolation. Local proposed
94+
configuration, central publication, consumer adoption, actual runner execution,
95+
review approval and ordinary protected merge are distinct acceptance states.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""Executable admission and lane contracts for the fixed pg-erd consumer."""
2+
3+
import re
4+
import subprocess
5+
from pathlib import Path
6+
7+
import pytest
8+
import yaml
9+
10+
ROOT = Path(__file__).resolve().parents[1]
11+
PATH = ROOT / '.github/workflows/pg-erd-cloud-quality.yml'
12+
13+
14+
def workflow():
15+
"""Load with BaseLoader so GitHub's on key is not parsed as YAML 1.1 bool."""
16+
return yaml.load(PATH.read_text(), Loader=yaml.BaseLoader)
17+
18+
19+
def test_fixed_reusable_scope():
20+
"""No arbitrary executable inputs, secrets, or cancellation of its caller."""
21+
data = workflow()
22+
assert data['on'] == {'workflow_call': ''}
23+
assert data['permissions'] == {'contents': 'read'}
24+
assert 'concurrency' not in data
25+
assert set(data['jobs']) == {'backend', 'frontend'}
26+
for job in data['jobs'].values():
27+
assert job['runs-on'] == {'group': 'CWL CI isolated', 'labels': ['self-hosted', 'linux', 'x64', 'cwlab-ci-isolated']}
28+
assert job['timeout-minutes'] == '30'
29+
assert 'if' not in job
30+
assert job['steps'][0]['name'] == 'Admit fixed caller before checkout'
31+
assert 'uses' not in job['steps'][0]
32+
assert job['steps'][1]['uses'].startswith('step-security/harden-runner@')
33+
checkout = job['steps'][2]
34+
assert checkout['with']['persist-credentials'] == 'false'
35+
assert checkout['with']['ref'] == '${{ github.event.pull_request.head.sha || github.sha }}'
36+
for step in job['steps']:
37+
assert not step.get('continue-on-error')
38+
if 'uses' in step:
39+
assert re.search(r'@[0-9a-f]{40}$', step['uses'])
40+
41+
42+
@pytest.mark.parametrize('job_name', ['backend', 'frontend'])
43+
@pytest.mark.parametrize('event,repository,head,base,ref,expected', [
44+
('pull_request', 'ContextualWisdomLab/pg-erd-cloud', 'ContextualWisdomLab/pg-erd-cloud', 'ContextualWisdomLab/pg-erd-cloud', 'refs/pull/1/merge', 0),
45+
('pull_request', 'ContextualWisdomLab/pg-erd-cloud', 'attacker/fork', 'ContextualWisdomLab/pg-erd-cloud', 'refs/pull/1/merge', 1),
46+
('push', 'ContextualWisdomLab/pg-erd-cloud', '', '', 'refs/heads/main', 0),
47+
('push', 'ContextualWisdomLab/pg-erd-cloud', '', '', 'refs/heads/feature', 1),
48+
('workflow_dispatch', 'ContextualWisdomLab/pg-erd-cloud', '', '', 'refs/heads/main', 0),
49+
('workflow_dispatch', 'ContextualWisdomLab/pg-erd-cloud', '', '', 'refs/heads/feature', 1),
50+
('pull_request_target', 'ContextualWisdomLab/pg-erd-cloud', 'ContextualWisdomLab/pg-erd-cloud', 'ContextualWisdomLab/pg-erd-cloud', 'refs/heads/main', 1),
51+
('push', 'ContextualWisdomLab/.github', '', '', 'refs/heads/main', 1),
52+
('pull_request', 'ContextualWisdomLab/pg-erd-cloud', 'ContextualWisdomLab/pg-erd-cloud', 'attacker/base', 'refs/pull/1/merge', 1),
53+
])
54+
def test_actual_admission_shell(job_name, event, repository, head, base, ref, expected):
55+
"""Run the actual first step with allowed and rejected caller tuples."""
56+
step = workflow()['jobs'][job_name]['steps'][0]
57+
assert step['env'] == {
58+
'CALLER_REPOSITORY': '${{ github.repository }}', 'CALLER_EVENT': '${{ github.event_name }}',
59+
'PR_HEAD_REPOSITORY': '${{ github.event.pull_request.head.repo.full_name }}',
60+
'PR_BASE_REPOSITORY': '${{ github.event.pull_request.base.repo.full_name }}',
61+
'CALLER_REF': '${{ github.ref }}',
62+
}
63+
env = {'PATH': '/usr/bin:/bin', 'CALLER_REPOSITORY': repository, 'CALLER_EVENT': event,
64+
'PR_HEAD_REPOSITORY': head, 'PR_BASE_REPOSITORY': base, 'CALLER_REF': ref}
65+
result = subprocess.run(['/bin/bash', '--noprofile', '--norc', '-e', '-o', 'pipefail', '-c', step['run']], env=env, capture_output=True, text=True, timeout=5)
66+
assert result.returncode == expected
67+
assert 'attacker' not in result.stdout + result.stderr
68+
69+
70+
def test_backend_environment_is_job_private():
71+
"""Candidate dependencies cannot be installed into a shared runner Python."""
72+
steps = workflow()['jobs']['backend']['steps']
73+
install = next(s for s in steps if 'pip install' in s.get('run', ''))
74+
script = install['run']
75+
assert 'mktemp -d' in script
76+
assert 'python -m venv' in script
77+
assert 'trap ' in script and ' EXIT' in script
78+
assert 'PIP_NO_CACHE_DIR' in script
79+
assert install['shell'] == 'bash --noprofile --norc -e -o pipefail {0}'
80+
81+
82+
@pytest.mark.parametrize('failed_tool, expected', [('', 0), ('mypy', 17), ('pytest', 17)])
83+
def test_backend_shell_cleanup_and_failure_propagation(tmp_path, failed_tool, expected):
84+
"""Execute the actual lane with synthetic tools; never install dependencies."""
85+
checkout = tmp_path / 'checkout'; checkout.mkdir()
86+
(checkout / 'backend').mkdir()
87+
runner_temp = tmp_path / 'runner-temp'; runner_temp.mkdir()
88+
sentinel = runner_temp / 'unrelated'; sentinel.write_text('keep')
89+
tools = tmp_path / 'tools'; tools.mkdir()
90+
trace = tmp_path / 'trace'
91+
python_stub = tools / 'python'
92+
python_stub.write_text('''#!/bin/bash
93+
set -eu
94+
printf 'python %s\\n' "$*" >> "$TRACE"
95+
if [ "${1-}" = '-m' ] && [ "${2-}" = 'venv' ]; then
96+
mkdir -p "$3/bin"
97+
ln -s "$STUB_PYTHON" "$3/bin/python"
98+
fi
99+
''')
100+
python_stub.chmod(0o755)
101+
for tool in ['actionlint', 'shellcheck', 'mypy', 'pytest']:
102+
path = tools / tool
103+
path.write_text(f'''#!/bin/bash
104+
printf '{tool}\\n' >> "$TRACE"
105+
if [ "$FAILED_TOOL" = '{tool}' ]; then exit 17; fi
106+
''')
107+
path.chmod(0o755)
108+
step = next(s for s in workflow()['jobs']['backend']['steps'] if 'pip install' in s.get('run', ''))
109+
env = {'PATH': f'{tools}:/usr/bin:/bin', 'RUNNER_TEMP': str(runner_temp),
110+
'TRACE': str(trace), 'STUB_PYTHON': str(python_stub), 'FAILED_TOOL': failed_tool}
111+
result = subprocess.run(['/bin/bash', '--noprofile', '--norc', '-e', '-o', 'pipefail', '-c', step['run']], cwd=checkout, env=env, capture_output=True, text=True, timeout=5)
112+
assert result.returncode == expected, result.stderr
113+
assert sorted(p.name for p in runner_temp.iterdir()) == ['unrelated']
114+
assert sentinel.read_text() == 'keep'
115+
lines = trace.read_text().splitlines()
116+
assert sum('pip install --require-hashes' in line for line in lines) == 2
117+
assert 'python -m unittest discover -s .github/tests -v' in lines
118+
if failed_tool == 'mypy':
119+
assert 'pytest' not in lines
120+
else:
121+
assert 'pytest' in lines
122+
123+
124+
def test_preserved_product_commands():
125+
"""Centralization keeps every existing backend/frontend validation command."""
126+
data = workflow()
127+
backend = data['jobs']['backend']; frontend = data['jobs']['frontend']
128+
commands = '\n'.join(s.get('run', '') for s in backend['steps'])
129+
for command in ['python -m pip install --require-hashes -r requirements-dev.lock',
130+
'python -m pip install --require-hashes -r ../.github/requirements.lock',
131+
'python -m unittest discover -s .github/tests -v',
132+
'python .github/scripts/validate_codeql_backfill.py',
133+
'actionlint .github/workflows/*.yml', 'shellcheck .github/scripts/*.sh',
134+
'mypy app', 'pytest -q']:
135+
assert command in commands
136+
frontend_commands = '\n'.join(s.get('run', '') for s in frontend['steps'])
137+
for command in ['npm ci --include=dev', 'npm run typecheck', 'npm run test', 'npm run build']:
138+
assert command in frontend_commands
139+
assert frontend['env']['NODE_ENV'] == 'test'
140+
assert next(s for s in frontend['steps'] if s['name'] == 'Build')['env']['NODE_ENV'] == 'production'
141+
assert all('runner.' not in str(v) for j in data['jobs'].values() for v in j.get('env', {}).values())

0 commit comments

Comments
 (0)