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).
288 lines
7.4 KiB
Bash
Executable File
288 lines
7.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# tools/branch-protection/test_check_name_parity.sh — unit tests for
|
|
# check_name_parity.sh.
|
|
#
|
|
# Builds synthetic apply.sh + workflow files in a tmpdir for each case,
|
|
# invokes the script with REPO_ROOT pointing at the tmpdir, and asserts
|
|
# on exit code + stderr. Per feedback_assert_exact_not_substring we
|
|
# pin the EXACT exit code AND a substring of the stderr that names the
|
|
# offending workflow + name combo — so a "false-pass that prints the
|
|
# wrong message" still fails the test.
|
|
#
|
|
# Run locally: bash tools/branch-protection/test_check_name_parity.sh
|
|
# Run in CI: same — added to ci.yml's shellcheck job's "E2E bash unit
|
|
# tests" step alongside test_model_slug.sh.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SCRIPT_UNDER_TEST="$SCRIPT_DIR/check_name_parity.sh"
|
|
|
|
if [[ ! -x "$SCRIPT_UNDER_TEST" ]]; then
|
|
echo "test_check_name_parity: script under test missing or not executable: $SCRIPT_UNDER_TEST" >&2
|
|
exit 2
|
|
fi
|
|
|
|
PASSED=0
|
|
FAILED=0
|
|
|
|
# Tracks the active tmpdir for the running case so the trap can clean
|
|
# up even when assertions abort the case mid-flight.
|
|
TMPDIR_FOR_CASE=""
|
|
trap '[[ -n "$TMPDIR_FOR_CASE" && -d "$TMPDIR_FOR_CASE" ]] && rm -rf "$TMPDIR_FOR_CASE"' EXIT
|
|
|
|
# Build a synthetic repo at $1 with apply.sh listing $2 (one name per
|
|
# line) as the staging required set + zero main required, then write
|
|
# whatever .gitea/workflows/* files the test case adds. (Pre-SSOT-4
|
|
# this was .github/workflows; molecule-core switched to Gitea-SSOT in
|
|
# task #331 and the script now reads from .gitea/workflows/.)
|
|
make_fake_repo() {
|
|
local root="$1"
|
|
local checks="$2"
|
|
mkdir -p "$root/tools/branch-protection"
|
|
mkdir -p "$root/.gitea/workflows"
|
|
cat > "$root/tools/branch-protection/apply.sh" <<EOF
|
|
#!/usr/bin/env bash
|
|
# Stub apply.sh — only the heredoc-shaped check lists matter for the
|
|
# parity script. Other functions intentionally absent.
|
|
|
|
read -r -d '' STAGING_CHECKS <<'EOF2' || true
|
|
$checks
|
|
EOF2
|
|
|
|
read -r -d '' MAIN_CHECKS <<'EOF2' || true
|
|
$checks
|
|
EOF2
|
|
EOF
|
|
chmod +x "$root/tools/branch-protection/apply.sh"
|
|
# Place the script-under-test alongside its sibling apply.sh so the
|
|
# script's REPO_ROOT walk finds the synthetic .gitea/workflows/.
|
|
cp "$SCRIPT_UNDER_TEST" "$root/tools/branch-protection/check_name_parity.sh"
|
|
}
|
|
|
|
run_case() {
|
|
local desc="$1"
|
|
local checks="$2"
|
|
local workflow_yaml="$3" # contents to write
|
|
local workflow_filename="$4"
|
|
local expected_exit="$5"
|
|
local expected_stderr_substring="$6"
|
|
TMPDIR_FOR_CASE=$(mktemp -d)
|
|
make_fake_repo "$TMPDIR_FOR_CASE" "$checks"
|
|
printf '%s' "$workflow_yaml" > "$TMPDIR_FOR_CASE/.gitea/workflows/$workflow_filename"
|
|
local stderr_file
|
|
stderr_file=$(mktemp)
|
|
local actual_exit=0
|
|
bash "$TMPDIR_FOR_CASE/tools/branch-protection/check_name_parity.sh" 2>"$stderr_file" >/dev/null || actual_exit=$?
|
|
local stderr_content
|
|
stderr_content=$(cat "$stderr_file")
|
|
rm "$stderr_file"
|
|
if [[ "$actual_exit" -ne "$expected_exit" ]]; then
|
|
echo "FAIL: $desc"
|
|
echo " expected exit: $expected_exit, got: $actual_exit"
|
|
echo " stderr: $stderr_content"
|
|
FAILED=$((FAILED+1))
|
|
rm -rf "$TMPDIR_FOR_CASE"; TMPDIR_FOR_CASE=""
|
|
return
|
|
fi
|
|
# Empty expected substring → no assertion on stderr (used for the
|
|
# passing case where stderr should be empty / not interesting).
|
|
if [[ -n "$expected_stderr_substring" ]]; then
|
|
if ! grep -qF "$expected_stderr_substring" <<< "$stderr_content"; then
|
|
echo "FAIL: $desc"
|
|
echo " expected stderr to contain: '$expected_stderr_substring'"
|
|
echo " actual stderr: $stderr_content"
|
|
FAILED=$((FAILED+1))
|
|
rm -rf "$TMPDIR_FOR_CASE"; TMPDIR_FOR_CASE=""
|
|
return
|
|
fi
|
|
fi
|
|
echo "PASS: $desc"
|
|
PASSED=$((PASSED+1))
|
|
rm -rf "$TMPDIR_FOR_CASE"; TMPDIR_FOR_CASE=""
|
|
}
|
|
|
|
# Case 1: safe workflow — no top-level paths: filter, single job
|
|
# emitting the required name. Should exit 0.
|
|
run_case "safe: no paths filter, job emits required name" \
|
|
"Foo Build" \
|
|
"$(cat <<'EOF'
|
|
name: Foo
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
|
|
jobs:
|
|
foo:
|
|
name: Foo Build
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo ok
|
|
EOF
|
|
)" \
|
|
"foo.yml" \
|
|
0 \
|
|
""
|
|
|
|
# Case 2: unsafe — top-level paths: filter AND no per-step if-gates.
|
|
# This is the silent-block shape from the saved memory.
|
|
run_case "unsafe: top-level paths: filter without per-step if-gates" \
|
|
"Bar Build" \
|
|
"$(cat <<'EOF'
|
|
name: Bar
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- 'bar/**'
|
|
pull_request:
|
|
paths:
|
|
- 'bar/**'
|
|
|
|
jobs:
|
|
bar:
|
|
name: Bar Build
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo ok
|
|
EOF
|
|
)" \
|
|
"bar.yml" \
|
|
1 \
|
|
"UNSAFE-PATH-FILTER"
|
|
|
|
# Case 3: required name has no emitter at all.
|
|
run_case "missing: required name not in any workflow" \
|
|
"Nonexistent Job" \
|
|
"$(cat <<'EOF'
|
|
name: Other
|
|
|
|
on:
|
|
pull_request:
|
|
|
|
jobs:
|
|
other:
|
|
name: Other Job
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo ok
|
|
EOF
|
|
)" \
|
|
"other.yml" \
|
|
1 \
|
|
"MISSING: required check name 'Nonexistent Job'"
|
|
|
|
# Case 4: safe — top-level paths: filter is absent BUT per-step if-
|
|
# gates are present (single-job-with-per-step-if pattern, what
|
|
# ci.yml + e2e-api.yml use). Should exit 0.
|
|
run_case "safe: per-step if-gates without top-level paths" \
|
|
"Baz Build" \
|
|
"$(cat <<'EOF'
|
|
name: Baz
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
|
|
jobs:
|
|
changes:
|
|
name: Detect changes
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
baz: ${{ steps.check.outputs.baz }}
|
|
steps:
|
|
- id: check
|
|
run: echo "baz=true" >> "$GITHUB_OUTPUT"
|
|
|
|
baz:
|
|
needs: changes
|
|
name: Baz Build
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- if: needs.changes.outputs.baz != 'true'
|
|
run: echo no-op
|
|
- if: needs.changes.outputs.baz == 'true'
|
|
run: echo real work
|
|
EOF
|
|
)" \
|
|
"baz.yml" \
|
|
0 \
|
|
""
|
|
|
|
# Case 5: unsafe-mix — top-level paths: AND per-step if-gates. The
|
|
# script flags this distinctly because the workflow may STILL skip
|
|
# entirely when paths exclude the commit (the per-step gates only
|
|
# matter if the workflow actually fires).
|
|
run_case "unsafe-mix: top-level paths: AND per-step if-gates" \
|
|
"Qux Build" \
|
|
"$(cat <<'EOF'
|
|
name: Qux
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- 'qux/**'
|
|
pull_request:
|
|
paths:
|
|
- 'qux/**'
|
|
|
|
jobs:
|
|
changes:
|
|
name: Detect changes
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
qux: ${{ steps.check.outputs.qux }}
|
|
steps:
|
|
- id: check
|
|
run: echo "qux=true" >> "$GITHUB_OUTPUT"
|
|
|
|
qux:
|
|
needs: changes
|
|
name: Qux Build
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- if: needs.changes.outputs.qux == 'true'
|
|
run: echo build
|
|
EOF
|
|
)" \
|
|
"qux.yml" \
|
|
1 \
|
|
"UNSAFE-MIX"
|
|
|
|
# Case 6: codeql.yml matrix — required names like "Analyze (go)" are
|
|
# generated by `Analyze (${{ matrix.language }})`. Script must
|
|
# special-case match this pattern.
|
|
run_case "matrix: codeql Analyze (go) is recognised via matrix expansion" \
|
|
"$(printf 'Analyze (go)\nAnalyze (javascript-typescript)\nAnalyze (python)')" \
|
|
"$(cat <<'EOF'
|
|
name: CodeQL
|
|
|
|
on:
|
|
pull_request:
|
|
|
|
jobs:
|
|
analyze:
|
|
name: Analyze (${{ matrix.language }})
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
matrix:
|
|
language: [go, javascript-typescript, python]
|
|
steps:
|
|
- run: echo analyse
|
|
EOF
|
|
)" \
|
|
"codeql.yml" \
|
|
0 \
|
|
""
|
|
|
|
echo ""
|
|
echo "================================================"
|
|
echo "test_check_name_parity: $PASSED passed, $FAILED failed"
|
|
echo "================================================"
|
|
exit "$FAILED"
|