release(v0.16.0): culture/release → main (wow layer + SH content + UK intake) #1127
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: validate | |
| on: | |
| pull_request: | |
| branches: [main, culture/release] | |
| jobs: | |
| khai-encoding: | |
| name: khai - Encoding rules | |
| needs: [setup, cultures-stamp-check] | |
| # Fork guard: every job that installs the private khai-tests wheel via | |
| # secrets.KAIHACKS runs only for same-repo PRs. Forked PRs get no | |
| # secrets, so these jobs would fail regardless -- skip them cleanly | |
| # instead. Each secrets.KAIHACKS job below carries the same condition. | |
| if: needs.setup.outputs.any == 'true' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Run encoding tests | |
| run: python -m pytest --pyargs khai_tests.test_khai_encoding --khai-files="${{ needs.setup.outputs.files }}" -v | |
| khai-process: | |
| name: khai - Process rules | |
| needs: [setup, khai-encoding] | |
| if: needs.setup.outputs.any == 'true' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Run process tests | |
| run: python -m pytest --pyargs khai_tests.components.test_khai_process --khai-files="${{ needs.setup.outputs.files }}" -v | |
| khai-position: | |
| name: khai - Position rules | |
| needs: [setup, khai-encoding] | |
| if: needs.setup.outputs.any == 'true' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Run position tests | |
| run: python -m pytest --pyargs khai_tests.components.test_khai_position --khai-files="${{ needs.setup.outputs.files }}" -v | |
| khai-piece: | |
| name: khai - Piece rules | |
| needs: [setup, khai-encoding] | |
| if: needs.setup.outputs.any == 'true' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Run piece tests | |
| run: python -m pytest --pyargs khai_tests.components.test_khai_piece --khai-files="${{ needs.setup.outputs.files }}" -v | |
| khai-place: | |
| name: khai - Place rules | |
| needs: [setup, khai-encoding] | |
| if: needs.setup.outputs.any == 'true' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Run place tests | |
| run: python -m pytest --pyargs khai_tests.components.test_khai_place --khai-files="${{ needs.setup.outputs.files }}" -v | |
| khai-persona: | |
| name: khai - Persona rules | |
| needs: [setup, khai-encoding] | |
| if: needs.setup.outputs.any == 'true' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Run persona tests | |
| run: python -m pytest --pyargs khai_tests.components.test_khai_persona --khai-files="${{ needs.setup.outputs.files }}" -v | |
| cultures-sections: | |
| name: cultures - Section structure | |
| needs: [setup, khai-process, khai-position, khai-piece, khai-place, khai-persona] | |
| if: needs.setup.outputs.any == 'true' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Run section tests | |
| run: python -m pytest tests/test_sections.py --khai-files="${{ needs.setup.outputs.files }}" -v | |
| cultures-branch-scope-tests: | |
| name: cultures - Branch scope tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Python deps | |
| # PyYAML required for E2E test (hook invokes validate_country_bag.py). | |
| run: pip install -r tests/requirements.txt pytest | |
| - name: Run branch_scope unit + E2E tests | |
| run: python -m pytest tests/test_branch_scope.py tests/test_hook_scope_e2e.py -v | |
| cultures-extract-culture-slugs-tests: | |
| name: cultures - Extract-culture-slugs tests | |
| # The release-gating gate (release-gating-gate.yml) and the | |
| # auto-PR opener (release-gating.yml) both extract culture slugs | |
| # from a diff via scripts/extract_culture_slugs.py. Locks the | |
| # depth-3 sovereign / depth-4 sub-national contract so the | |
| # Schleswig-Holstein bypass class cannot return silently. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run extract-culture-slugs tests | |
| run: python -m pytest tests/test_extract_culture_slugs.py -v | |
| cultures-release-gating-tests: | |
| name: cultures - Release-gating tests | |
| # The release-gating engine: registry generator, gating check, and | |
| # manifest projection. All three define the KAIHACKS download | |
| # contract. Pre-#535 these test files lived on the test_validate_wiring | |
| # exempt list (pre-existing backlog) -- promoted to live gates here | |
| # so contract changes can't be made silently. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Python deps | |
| run: pip install pyyaml pytest | |
| - name: Run release-gating + manifest tests | |
| run: | | |
| python -m pytest \ | |
| tests/test_generate_country_entry.py \ | |
| tests/test_release_gating.py \ | |
| tests/test_generate_available_json.py \ | |
| -v | |
| cultures-branch-scope-diff-check: | |
| name: cultures - Branch scope diff check | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Check PR diff against branch scope rules | |
| env: | |
| BRANCH: ${{ github.head_ref }} | |
| BASE_REF: ${{ github.base_ref }} | |
| run: | | |
| set -e | |
| python3 - <<'PY' | |
| import os, subprocess, sys | |
| sys.path.insert(0, "tests") | |
| from branch_scope import ( | |
| check_scope, | |
| classify_branch, | |
| diagnose_scope_failure, | |
| render_scope_failure, | |
| ) | |
| branch = os.environ["BRANCH"] | |
| base_ref = os.environ["BASE_REF"] | |
| base = f"origin/{base_ref}" | |
| def git(*args): | |
| return subprocess.run( | |
| ["git", *args], capture_output=True, text=True, check=True, | |
| ).stdout | |
| kind = classify_branch(branch) | |
| if kind == "main": | |
| print("ERROR: PR head branch is 'main'; impossible state.") | |
| sys.exit(1) | |
| # --diff-filter=ACMRT skips deletions; three-dot range matches the | |
| # merge-base semantics the scope check has always used. | |
| files = [ | |
| line.strip() | |
| for line in git( | |
| "diff", "--name-only", "--diff-filter=ACMRT", | |
| f"{base}...HEAD", | |
| ).splitlines() | |
| if line.strip() | |
| ] | |
| if not files: | |
| print("No files changed; skipping scope check.") | |
| sys.exit(0) | |
| ok, unsafe = check_scope(kind, files, branch) | |
| if ok: | |
| print( | |
| f"OK: branch '{branch}' (classified '{kind}') passes " | |
| f"scope check ({len(files)} file(s))." | |
| ) | |
| sys.exit(0) | |
| # Scope failed. Attribute each offending file to its introducing | |
| # commit(s) so the report can distinguish a mis-based branch | |
| # (rebase) from a genuine out-of-scope edit (revert/move). | |
| _on_main: dict[str, bool] = {} | |
| def is_on_main(sha): | |
| if sha not in _on_main: | |
| _on_main[sha] = subprocess.run( | |
| ["git", "merge-base", "--is-ancestor", sha, | |
| "origin/main"], | |
| ).returncode == 0 | |
| return _on_main[sha] | |
| def commits_for_file(path): | |
| out = git("log", "--format=%H", f"{base}..HEAD", "--", path) | |
| return [line.strip() for line in out.splitlines() if line.strip()] | |
| inherited, genuine = diagnose_scope_failure( | |
| unsafe, | |
| commits_for_file=commits_for_file, | |
| is_on_main=is_on_main, | |
| ) | |
| try: | |
| base_behind = int( | |
| git("rev-list", "--count", f"{base}..origin/main").strip() | |
| ) | |
| except (subprocess.CalledProcessError, ValueError): | |
| base_behind = 0 | |
| print(render_scope_failure( | |
| branch, base_ref, inherited, genuine, base_behind, | |
| )) | |
| sys.exit(1) | |
| PY | |
| cultures-culture-branch-base: | |
| name: cultures - Culture branch base | |
| # A culture/<country|region> branch must be cut from culture/release, | |
| # the integration branch -- never from main. A branch cut from main | |
| # carries main's lead, which pollutes the PR diff and (see PR #213) | |
| # sends contributors -- LLMs especially -- chasing phantom scope | |
| # violations. The branch-scope check above only catches this | |
| # incidentally, when main's drift includes out-of-scope files; this | |
| # job catches it directly, on commit ancestry, and fails fast. | |
| # culture/release itself is exempt: it integrates upward into main. | |
| if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'culture/') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Check culture branch was cut from culture/release | |
| env: | |
| BRANCH: ${{ github.head_ref }} | |
| run: | | |
| set -e | |
| python3 - <<'PY' | |
| import os, subprocess, sys | |
| sys.path.insert(0, "tests") | |
| from branch_scope import ( | |
| WORLD_SLUGS, | |
| classify_branch, | |
| misbased_commits, | |
| render_misbased_branch, | |
| ) | |
| branch = os.environ["BRANCH"] | |
| if classify_branch(branch) != "culture": | |
| print(f"SKIP: '{branch}' is not a culture branch.") | |
| sys.exit(0) | |
| if branch[len("culture/"):] in WORLD_SLUGS: | |
| print(f"SKIP: '{branch}' is the integration branch.") | |
| sys.exit(0) | |
| integration = "origin/culture/release" | |
| def git(*args): | |
| return subprocess.run( | |
| ["git", *args], capture_output=True, text=True, check=True, | |
| ).stdout | |
| # Commits on the branch but not on the integration branch. A | |
| # tab separates the abbreviated SHA from the subject so subjects | |
| # containing spaces survive the split. | |
| commits = [] | |
| for line in git( | |
| "log", "--format=%h%x09%s", f"{integration}..HEAD", | |
| ).splitlines(): | |
| if not line.strip(): | |
| continue | |
| sha, _, subject = line.partition("\t") | |
| commits.append((sha.strip(), subject.strip())) | |
| _on_main: dict[str, bool] = {} | |
| def is_on_main(sha): | |
| if sha not in _on_main: | |
| _on_main[sha] = subprocess.run( | |
| ["git", "merge-base", "--is-ancestor", sha, | |
| "origin/main"], | |
| ).returncode == 0 | |
| return _on_main[sha] | |
| offending = misbased_commits(commits, is_on_main=is_on_main) | |
| if offending: | |
| print(render_misbased_branch(branch, offending)) | |
| sys.exit(1) | |
| print( | |
| f"OK: '{branch}' was cut from culture/release " | |
| f"({len(commits)} branch commit(s), none already on main)." | |
| ) | |
| PY | |
| cultures-pr-base-tests: | |
| name: cultures - PR base tests | |
| # Unit-test gate for tests/validate_pr_base.py (the PR base routing | |
| # contract). Independent, always runs, no culture-file dependency. | |
| # Catches routing regressions before pr-gate.yml enforces them in | |
| # production. CLI file exercises the script pr-gate.yml actually calls. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run PR base routing tests | |
| run: python -m pytest tests/test_validate_pr_base.py tests/test_validate_pr_base_cli.py -v | |
| cultures-brownfield-tests: | |
| name: cultures - Brownfield migration validator tests | |
| # Unit-test gate for tests/validate_pr_brownfield.py (the brownfield | |
| # "if you touch, you migrate" rule). Independent, always runs, no | |
| # culture-file dependency. Catches regressions in the gate's rule | |
| # logic, exempt list, suffix regex, and frontmatter parsing before | |
| # pr-gate.yml enforces them on every PR. Mirrors cultures-pr-base-tests. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest pyyaml | |
| - name: Run brownfield validator tests | |
| run: python -m pytest tests/test_validate_pr_brownfield.py -v | |
| cultures-audit-readme-bands-tests: | |
| name: cultures - Audit README bands tests | |
| # Unit-test gate for scripts/audit_readme_bands.py (the canonical Hofstede | |
| # band contract: 0-39 Low, 40-69 Moderate, 70-100 High). Mirrors the | |
| # cultures-branch-scope-tests pattern: independent, always runs, exercises the | |
| # contract on every PR. Closes the gap noted during the v2 stage 2 | |
| # sequence where this test had no CI runner. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run audit_readme_bands tests | |
| run: python -m pytest tests/test_audit_readme_bands.py -v | |
| cultures-culture-traceability: | |
| name: cultures - Culture-set traceability | |
| # Gate for REFERENCES.md / README.md / persona-link traceability against | |
| # the real culture file set (ChBrain/Cultures#301 rules R4-R6). Independent, | |
| # always runs, no deps. The checks are xfail-soft during the corpus | |
| # rollout, so this job stays green while per-country drift is corrected. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run culture traceability tests | |
| run: python -m pytest tests/test_culture_traceability.py -v | |
| cultures-hofstede-readme-updater-tests: | |
| name: cultures - Hofstede README updater tests | |
| # Unit-test gate for scripts/update_hofstede_readme.py (deterministic | |
| # rewriter for the Hofstede Cultural Dimensions + Alignment Status tables | |
| # in country READMEs). Pins the EXCELLENT/PASS/WARN/FAIL thresholds, the | |
| # canonical band-label rendering, and the table-replacement contract. | |
| # Mirrors the cultures-audit-readme-bands-tests pattern: independent, always runs. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Python deps | |
| # Mirrors cultures-marker-derived / cultures-hofstede-bag-loader-tests: tests/requirements.txt | |
| # has lingua-language-detector, PyYAML, and pytest. Required because | |
| # update_hofstede_readme imports data.hofstede_keywords transitively | |
| # via hofstede_bag_loader, and data.hofstede_keywords imports lingua | |
| # at module level (for detect_language). The unit tests don't call | |
| # detect_language but the import has to succeed for the module to | |
| # load. | |
| run: pip install -r tests/requirements.txt | |
| - name: Run update_hofstede_readme tests | |
| run: python -m pytest tests/test_update_hofstede_readme.py -v | |
| cultures-history-arc: | |
| name: cultures - History arc | |
| # Always-run gate for the history-as-defining-moments-arc methodology | |
| # (khai-cultures-create skill v0.1.1+). Validates every | |
| # culture_*_history_*.md file in regions/** has: | |
| # - *khai: piece* declaration footer | |
| # - Yearbook section with >= 12 dated entries | |
| # - Date range spanning >= 5 centuries (broad arc, not single event) | |
| # Catches the class of bug PR #105 surfaced: a history file shipping | |
| # as a narrow-event essay (the original Beeldenstorm-only Netherlands | |
| # history) rather than a broad-arc Yearbook. Mirrors the | |
| # cultures-audit-readme-bands-tests pattern: independent, always runs. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest pyyaml | |
| - name: Run history-arc tests | |
| run: python -m pytest tests/test_history_arc.py -v | |
| cultures-language-process-ladder: | |
| name: cultures - Language-process ladder | |
| # Always-run gate for the engine language-as-process ladder (issue #281). | |
| # The 21 ladder files under engine/ are universal solution-grade culture | |
| # content, but khai does not see engine files (--khai-files is regions/- | |
| # scoped), so the ladder's structural contract -- completeness, the IDLE | |
| # section set, Scope: Universal, the link graph -- is enforced here. | |
| # Skips cleanly until the ladder lands (PR #283). | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest pyyaml | |
| - name: Run language-process ladder tests | |
| run: python -m pytest tests/test_language_process_ladder.py -v | |
| cultures-persona-projection-ladder: | |
| name: cultures - Persona projection ladder | |
| # Always-run gate for the persona ## Projection ladder rule. | |
| # Canonical persona files (<adj>_persona_<gender>_<name>.md) must | |
| # link engine/process_<channel>_*.md inside ## Projection for all | |
| # four channels: speaking, hearing, reading, writing. Cultures- | |
| # specific gate; the khai chapter-list contract is enforced | |
| # separately in khai_tests. Skips legacy and deprecated forms | |
| # cleanly so the gate fires only on the canonical v0.2.0 file shape. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run persona projection ladder tests | |
| run: python -m pytest tests/test_persona_projection_ladder.py -v | |
| cultures-persona-english-link: | |
| name: cultures - Persona English link (L4h) | |
| # Gate: every canonical persona's ## Projection must link a named English | |
| # position file (anchor text OR target path contains 'english', | |
| # case-insensitive). Known gaps listed in | |
| # tests/persona_english_link_exceptions.txt are xfail while #425 is | |
| # being worked down country-by-country. Hard block from day one -- | |
| # the exceptions file is the safety valve. | |
| runs-on: ubuntu-latest | |
| needs: [cultures-persona-projection-ladder] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run persona English link tests | |
| run: python -m pytest tests/test_persona_english_link.py -v | |
| cultures-zip-build: | |
| name: cultures - Zip build | |
| # Runs the real release zip build (scripts/build_zips.py) and validates | |
| # every produced zip. build-zips.yml only fires on release/dispatch, so | |
| # this is what proves the build is sound on a PR. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest pyyaml | |
| - name: Run zip-build tests | |
| run: python -m pytest tests/test_build_zips.py -v | |
| cultures-pdf-build: | |
| name: cultures - PDF build (smoke) | |
| # Smoke-tests scripts/build_pdfs.py with pandoc mocked. The real | |
| # release PDF build (build-pdfs.yml) only fires on release/dispatch | |
| # and needs the apt-installed pandoc/weasyprint; this gates the | |
| # enumeration + markdown-assembly + completeness-filter logic on | |
| # every PR. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest pyyaml | |
| - name: Run pdf-build smoke tests | |
| run: python -m pytest tests/test_build_pdfs.py -v | |
| cultures-completeness-alignment: | |
| name: cultures - Completeness alignment (registry vs shipped) | |
| # Cross-check that data/countries.json (the website-map registry) | |
| # and the complete-cultures set (zips + PDFs) stay in lockstep. | |
| # Catches the drift class where a culture lands in culture/release | |
| # but isn't registered (the Mexico gap, #257), or where a registry | |
| # entry references a country whose folder is not yet complete. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run completeness-alignment tests | |
| run: python -m pytest tests/test_culture_completeness_alignment.py -v | |
| cultures-completeness-module-tests: | |
| name: cultures - Completeness module tests | |
| # Unit tests for scripts/culture_completeness.py itself -- | |
| # complete_countries (sovereign-only) and complete_cultures (both | |
| # depths). Locks the depth contracts the auto-release bump diff | |
| # and the alignment cross-check rely on; pre-#536 a sub-national | |
| # could silently slip past both. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run culture-completeness module tests | |
| run: python -m pytest tests/test_culture_completeness.py -v | |
| cultures-hofstede-score-home: | |
| name: cultures - Hofstede score home | |
| # Single-home gate for Hofstede scores: data/hofstede_scores.json is | |
| # complete and authoritative, and no bag has drifted from it. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest pyyaml | |
| - name: Run Hofstede score-home tests | |
| run: python -m pytest tests/test_hofstede_score_home.py -v | |
| cultures-validator-wiring: | |
| name: cultures - Validator wiring | |
| # Meta-gate: every tests/test_*.py is invoked by a validate.yml job. | |
| # A validator is dead weight until a job runs it; this fails when a | |
| # test file is neither wired nor explicitly exempted. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run validator-wiring tests | |
| run: python -m pytest tests/test_validate_wiring.py -v | |
| cultures-metadata-format: | |
| name: cultures - Metadata format | |
| # Always-run guard for the footer -> YAML-frontmatter migration. Per | |
| # country: no culture_*.md carries both a frontmatter block and a | |
| # trailing footer, and frontmatter is all-or-nothing within a country | |
| # (a country migrates as a unit, never half-and-half). Mirrors the | |
| # cultures-history-arc pattern: independent, always runs. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run metadata-format guard | |
| run: python -m pytest tests/test_metadata_format.py -v | |
| cultures-language-preflight: | |
| name: cultures - Language pre-flight | |
| # Always-run pre-flight: every country README's **Language(s):** slug | |
| # must appear in data/language_policy.yaml, and every country's | |
| # data/countries.json language must be unlocked via iso_map. Mirrors the | |
| # cultures-audit-readme-bands-tests / cultures-history-arc pattern: independent, | |
| # always runs, no setup-job dependency. Fast (no lingua, no whl | |
| # install) -- under 30 seconds. | |
| # | |
| # Why a separate job: the same assertion runs late in the chain via | |
| # cultures-language, which depends on khai-language, which depends | |
| # on the components chain (encoding -> process/position/piece/place/ | |
| # persona -> sections). When the registry is missing an entry, that | |
| # failure surfaces 5-10 minutes into CI after every other validator | |
| # has run. This job surfaces it in ~30 seconds on every PR so a | |
| # missing-language case fails before the rest of the chain spins up. | |
| # | |
| # Incident: PR #166 (culture/spain) cleared every other validator | |
| # then failed on this assertion because `spanish` was not in | |
| # data/language_policy.yaml. Emergency PRs #167 (governance) and | |
| # #168 (sync) unblocked the release at the cost of two extra round | |
| # trips. The check itself was correct; it just ran too late to be | |
| # useful for prevention. | |
| # | |
| # LANGUAGES.md section "Pre-flight: check language enablement before | |
| # starting a culture" documents the human-side procedure that this | |
| # gate enforces. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest + pyyaml | |
| run: pip install pytest pyyaml | |
| - name: Run registry + unlock pre-flight | |
| run: python -m pytest tests/test_language.py -k "registry or unlocked or iso_map" -v | |
| setup: | |
| name: Setup - Compute changed files | |
| runs-on: ubuntu-latest | |
| outputs: | |
| any: ${{ steps.changed.outputs.any }} | |
| files: ${{ steps.changed.outputs.files }} | |
| # `data_changed`: true when the PR diff touches the Hofstede reference | |
| # dataset (data/hofstede_scores.json). Drives cultures-hofstede-reference into audit mode (run | |
| # across all countries) so a reference update doesn't silently widen | |
| # divergence for cultures whose declared scores are now off-band. | |
| data_changed: ${{ steps.changed.outputs.data_changed }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Get changed regions/*.md files | |
| id: changed | |
| env: | |
| HEAD_REF: ${{ github.head_ref }} | |
| run: | | |
| # Sync branches (sync/<name>) are pointer branches at main's HEAD. | |
| # Content was already validated when it landed on main. The PR diff | |
| # against culture/release would re-surface every file that has | |
| # advanced on main since the last sync, which is pure waste -- | |
| # short-circuit here so all downstream content jobs gated on | |
| # `any == 'true'` skip cleanly. | |
| if [[ "$HEAD_REF" == sync/* ]]; then | |
| echo "Sync branch detected ($HEAD_REF); skipping content-validator file detection." | |
| echo "any=false" >> "$GITHUB_OUTPUT" | |
| echo "files=" >> "$GITHUB_OUTPUT" | |
| echo "data_changed=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Validate fires only on pull_request, so the diff range is always | |
| # the merge base. (The previous push trigger required handling | |
| # `github.event.before` and HEAD~1 fallback; dropped along with | |
| # the push trigger -- post-merge re-validation added no value.) | |
| RANGE="origin/${{ github.base_ref }}...HEAD" | |
| # --diff-filter=ACMRT skips deletions; the validator must not be handed paths that no longer exist. | |
| ALL_CHANGED=$(git diff --name-only --diff-filter=ACMRT "$RANGE") | |
| # khai is path-independent: it classifies a file from its `khai:` | |
| # declaration, not its location. engine/*.md is solution-grade | |
| # culture content, so it is handed to the same validators (khai, | |
| # link integrity) as regions/*.md content. | |
| FILES=$(echo "$ALL_CHANGED" | { grep -E '^(regions|engine)/.*\.md$' || true; } | tr '\n' ' ' | sed 's/ *$//') | |
| ANY=$(if [ -z "$FILES" ]; then echo "false"; else echo "true"; fi) | |
| DATA_CHANGED=$(if echo "$ALL_CHANGED" | grep -qx 'data/hofstede_scores.json'; then echo "true"; else echo "false"; fi) | |
| echo "any=$ANY" >> "$GITHUB_OUTPUT" | |
| echo "files=$FILES" >> "$GITHUB_OUTPUT" | |
| echo "data_changed=$DATA_CHANGED" >> "$GITHUB_OUTPUT" | |
| cultures-stamp-check: | |
| name: cultures - Validation stamp gate | |
| needs: [setup] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check validation stamp exists | |
| run: | | |
| if [ ! -f ".validation-stamp" ]; then | |
| echo "❌ VALIDATION FAILED: .validation-stamp missing" | |
| echo "" | |
| echo "This file records that local validators passed before commit." | |
| echo "Run locally to generate it:" | |
| echo " git config core.hooksPath .githooks" | |
| echo " scripts/setup-hooks.sh # or setup-hooks.bat on Windows" | |
| echo " git commit --amend -m 'your message'" | |
| exit 1 | |
| fi | |
| echo "✔️ Validation stamp present" | |
| - name: Check validation stamp shape | |
| # Gate against malformed stamps (CRLF, NUL padding, BOM, wrong length, | |
| # non-hex content). .githooks/pre-commit writes the stamp as exactly | |
| # 40 hex chars + LF in UTF-8 (see the closing lines: | |
| # tree_hash = run_git("write-tree") | |
| # Path(".validation-stamp").write_text(tree_hash + "\n", encoding="utf-8") | |
| # ). Any deviation means the stamp was written through a non-hook | |
| # path -- typically an LLM agent's file write API (Claude Code in | |
| # VS Code, Copilot, MCP create_or_update_file) on a Windows host | |
| # that normalised line endings or added UTF-16 padding. The stamp | |
| # value itself isn't checked against tree state (the hook owns | |
| # that); this step only enforces shape so byte-exact three-way | |
| # merges don't trip on encoding artifacts. | |
| # | |
| # Incident: PR #105 (culture/netherlands -> culture/release) was | |
| # unmergeable for hours because culture/release's stamp had been | |
| # written by Claude Code in VS Code with `\r\n\r\x00\n\x00` after | |
| # the hash (46 bytes total, binary on disk). Both Copilot and a | |
| # second Claude Code session tried to "just pick one" via clean | |
| # 41-byte UTF-8 writes; git's three-way blob comparison rejected | |
| # every attempt because the bytes never matched. Resolution | |
| # required reverting the stamp to the merge-base blob -- a | |
| # non-obvious path that no LLM-driven agent found on its own. | |
| run: | | |
| python3 - <<'PY' | |
| import re | |
| import sys | |
| from pathlib import Path | |
| raw = Path(".validation-stamp").read_bytes() | |
| if len(raw) == 41 and re.fullmatch(rb"[0-9a-f]{40}\n", raw): | |
| print("✔️ Validation stamp shape OK (40 hex chars + LF, 41 bytes)") | |
| sys.exit(0) | |
| print("❌ VALIDATION FAILED: .validation-stamp is malformed") | |
| print(f" Expected: 41 bytes, [0-9a-f]{{40}} + LF (UTF-8, no BOM)") | |
| print(f" Actual: {len(raw)} bytes") | |
| print(f" Hex dump: {raw.hex()[:120]}" + ("..." if len(raw) > 60 else "")) | |
| print() | |
| print(" Most common cause: an LLM-driven file write (Claude Code in") | |
| print(" VS Code, Copilot suggesting a fix, MCP create_or_update_file)") | |
| print(" produced CRLF, UTF-16 padding, or BOM bytes instead of the") | |
| print(" clean UTF-8 LF the pre-commit hook writes. See PR #105.") | |
| print() | |
| print(" Fix: re-run the pre-commit hook locally to rewrite cleanly.") | |
| print(" touch a staged file (or git add --renormalize .)") | |
| print(" git commit --amend --no-edit") | |
| print(" git push --force-with-lease") | |
| sys.exit(1) | |
| PY | |
| khai-language: | |
| name: khai - Language rules | |
| needs: [setup, cultures-sections] | |
| if: needs.setup.outputs.any == 'true' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Install lingua + pyyaml | |
| run: pip install lingua-language-detector pyyaml | |
| - name: Run language detection tests | |
| run: python -m pytest --pyargs khai_tests.test_khai_language --khai-files="${{ needs.setup.outputs.files }}" -v | |
| cultures-language: | |
| name: cultures - Language policy | |
| needs: [setup, khai-language] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest + pyyaml | |
| run: pip install pytest pyyaml | |
| - name: Run static language checks | |
| run: python -m pytest tests/test_language.py -v | |
| cultures-language-dispatch: | |
| name: cultures - Language dispatch | |
| # Always-run gate for the per-file frontmatter `language:` dispatcher | |
| # (Nigeria mother-tongue arc -- Stage 2c). Pins the four routing | |
| # outcomes -- absent/lingua-known/NLP-only/unknown -- so a regression | |
| # in tests/validate_language.py's dispatch_route surfaces immediately. | |
| # Independent, no culture-file dependency. Mirrors the | |
| # cultures-history-arc pattern: always runs, fast (no lingua install). | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest + pyyaml | |
| run: pip install pytest pyyaml | |
| - name: Run language-dispatch tests | |
| run: python -m pytest tests/test_validate_language_dispatch.py -v | |
| khai-links: | |
| name: khai - Link rules | |
| needs: [setup, cultures-language] | |
| if: needs.setup.outputs.any == 'true' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Run link tests | |
| run: python -m pytest --pyargs khai_tests.test_khai_links --khai-files="${{ needs.setup.outputs.files }}" -v | |
| cultures-links: | |
| name: cultures - Link integrity | |
| needs: [setup, khai-links] | |
| if: needs.setup.outputs.any == 'true' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Run link tests | |
| run: python -m pytest tests/test_links.py --khai-roots=engine --khai-files="${{ needs.setup.outputs.files }}" -v | |
| cultures-completeness: | |
| name: cultures - Culture completeness | |
| needs: [setup, cultures-links] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest pyyaml | |
| - name: Validate culture completeness | |
| run: python -m pytest tests/test_completeness.py -v | |
| cultures-audit-readme: | |
| name: cultures - Audit README status tables | |
| needs: [setup, cultures-completeness] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Validate audit README | |
| run: python -m pytest tests/test_audit_readme.py -v | |
| cultures-audit-consistency: | |
| name: cultures - Audit table consistency | |
| needs: [setup, cultures-audit-readme] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Validate audit consistency | |
| run: python -m pytest tests/test_audit_consistency.py -v | |
| khai-plagiarism: | |
| name: khai - Phrase denylist rules | |
| needs: [setup, cultures-audit-consistency] | |
| if: needs.setup.outputs.any == 'true' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install khai-tests | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Run phrase denylist tests | |
| run: python -m pytest --pyargs khai_tests.test_khai_plagiarism --khai-files="${{ needs.setup.outputs.files }}" -v | |
| cultures-plagiarism: | |
| name: cultures - Plagiarism structure | |
| needs: [setup, khai-plagiarism] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run plagiarism structural checks | |
| run: python -m pytest tests/test_plagiarism.py -v | |
| cultures-hofstede-structure: | |
| name: cultures - Hofstede structure | |
| needs: [setup, cultures-plagiarism] | |
| if: needs.setup.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest pyyaml | |
| - name: Validate Hofstede structure | |
| run: python -m pytest tests/test_hofstede_structure.py -v | |
| cultures-marker-derived: | |
| name: cultures - Marker derived vs declared | |
| needs: [setup, cultures-hofstede-structure] | |
| # Fork guard: install-khai-tests below uses secrets.KAIHACKS, which | |
| # is not available on forked PRs. Skip cleanly rather than fail. | |
| if: needs.setup.outputs.any == 'true' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| env: | |
| PR_CHANGED_FILES: ${{ needs.setup.outputs.files }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Python deps | |
| run: pip install -r tests/requirements.txt | |
| - name: Install khai-tests | |
| # tests/test_marker_derived.py imports khai_tests.marker_score | |
| # (the shared scoring primitive); without this step the module | |
| # import fails with ModuleNotFoundError before any test collects. | |
| uses: ./.github/actions/install-khai-tests | |
| with: | |
| token: ${{ secrets.KAIHACKS }} | |
| - name: Validate Hofstede derived scores | |
| run: python -m pytest tests/test_marker_derived.py -v | |
| cultures-hofstede-reference: | |
| name: cultures - Culture docs Hofstede-free | |
| needs: [setup, cultures-marker-derived] | |
| if: needs.setup.outputs.any == 'true' || needs.setup.outputs.data_changed == 'true' | |
| runs-on: ubuntu-latest | |
| env: | |
| PR_CHANGED_FILES: ${{ needs.setup.outputs.files }} | |
| PR_DATA_CHANGED: ${{ needs.setup.outputs.data_changed }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install pytest | |
| run: pip install pytest | |
| - name: Run Hofstede reference tests | |
| run: python -m pytest tests/test_hofstede_reference.py -v | |
| cultures-hofstede-bag-loader-tests: | |
| name: cultures - Hofstede bag loader tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Python deps | |
| run: pip install -r tests/requirements.txt | |
| - name: Run loader tests | |
| run: python -m pytest tests/test_hofstede_bag_loader.py -v | |
| cultures-hofstede-bag-integrity: | |
| name: cultures - Hofstede bag integrity | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Python deps | |
| run: pip install -r tests/requirements.txt | |
| - name: Run bag integrity tests | |
| run: | | |
| python3 -m pytest tests/test_hofstede_bag_shape.py \ | |
| tests/test_hofstede_bag_quality.py \ | |
| tests/test_hofstede_bag_locked.py \ | |
| tests/test_hofstede_bag_completeness.py \ | |
| tests/test_hofstede_bag_fork.py \ | |
| -v | |
| no-changes: | |
| name: No regions/*.md files changed | |
| needs: [setup] | |
| if: needs.setup.outputs.any == 'false' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - run: echo "No regions/*.md files changed - skipping validation" | |
| # Aggregator gate. Succeeds iff no validate.yml job failed (skipped jobs | |
| # are fine -- they were not applicable to this PR). This is the single | |
| # check to add to branch protection on main and culture/release, instead | |
| # of every job above; new jobs are covered by adding them to `needs`. | |
| validate-gate: | |
| name: cultures - all checks passed | |
| needs: | |
| - khai-encoding | |
| - khai-process | |
| - khai-position | |
| - khai-piece | |
| - khai-place | |
| - khai-persona | |
| - cultures-sections | |
| - cultures-branch-scope-tests | |
| - cultures-branch-scope-diff-check | |
| - cultures-extract-culture-slugs-tests | |
| - cultures-release-gating-tests | |
| - cultures-culture-branch-base | |
| - cultures-pr-base-tests | |
| - cultures-brownfield-tests | |
| - cultures-audit-readme-bands-tests | |
| - cultures-culture-traceability | |
| - cultures-hofstede-readme-updater-tests | |
| - cultures-history-arc | |
| - cultures-language-process-ladder | |
| - cultures-persona-projection-ladder | |
| - cultures-zip-build | |
| - cultures-pdf-build | |
| - cultures-completeness-alignment | |
| - cultures-completeness-module-tests | |
| - cultures-hofstede-score-home | |
| - cultures-validator-wiring | |
| - cultures-metadata-format | |
| - cultures-language-preflight | |
| - setup | |
| - cultures-stamp-check | |
| - khai-language | |
| - cultures-language | |
| - cultures-language-dispatch | |
| - khai-links | |
| - cultures-links | |
| - cultures-completeness | |
| - cultures-audit-readme | |
| - cultures-audit-consistency | |
| - khai-plagiarism | |
| - cultures-plagiarism | |
| - cultures-hofstede-structure | |
| - cultures-marker-derived | |
| - cultures-hofstede-reference | |
| - cultures-hofstede-bag-loader-tests | |
| - cultures-hofstede-bag-integrity | |
| - no-changes | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Fail if any validate.yml job failed | |
| if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} | |
| run: | | |
| echo "::error::One or more validate.yml checks failed - see the jobs above." | |
| exit 1 | |
| - name: Fail if a khai check skipped on a same-repo content PR | |
| # khai jobs legitimately skip on fork PRs (a fork gets no KAIHACKS | |
| # secret) and when no regions/*.md changed. But on a same-repo PR | |
| # that DID change culture content, every khai job must actually | |
| # run -- a skip there means the khai validation silently did not | |
| # happen, and the step above (failure/cancelled only) would let it | |
| # pass green. This converts that silent skip to red. Fork PRs are | |
| # exempt by the head.repo condition, so they still skip cleanly. | |
| if: >- | |
| github.event.pull_request.head.repo.full_name == github.repository | |
| && needs.setup.outputs.any == 'true' | |
| && (needs.khai-encoding.result == 'skipped' | |
| || needs.khai-process.result == 'skipped' | |
| || needs.khai-position.result == 'skipped' | |
| || needs.khai-piece.result == 'skipped' | |
| || needs.khai-place.result == 'skipped' | |
| || needs.khai-persona.result == 'skipped' | |
| || needs.khai-language.result == 'skipped' | |
| || needs.khai-links.result == 'skipped' | |
| || needs.khai-plagiarism.result == 'skipped') | |
| run: | | |
| echo "::error::A khai validation job was skipped on a same-repo content PR -- khai must run on culture content. See the skipped khai-* jobs above." | |
| exit 1 | |
| - name: All clear | |
| run: echo "All validate.yml checks passed or were not applicable." |