Repo settings as code: rulesets, settings JSON and setup-repo.py #6
Workflow file for this run
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: Template CI | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: template-ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| render: | |
| name: Render and validate both variants | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 | |
| with: | |
| python-version: "3.12" | |
| - name: Render both variants and check them | |
| # --vcs-ref=HEAD on every copier call is load-bearing. Given a local git | |
| # repo, copier renders the latest *tag* by default, not the checkout -- | |
| # so without this the whole job silently validates the released template | |
| # instead of the branch under review, and every assertion below passes | |
| # while proving nothing about the change. It only works today because | |
| # actions/checkout fetches no tags, which is an accident rather than a | |
| # decision, and it makes local runs disagree with CI on a machine that | |
| # does have the tags. | |
| run: | | |
| set -euo pipefail | |
| for kind in app library; do | |
| uvx copier copy --trust --defaults --vcs-ref=HEAD \ | |
| --data project_name="sample-$kind" \ | |
| --data project_description="A sample $kind" \ | |
| --data project_kind="$kind" \ | |
| . "/tmp/out-$kind" | |
| python3 -c "import tomllib; tomllib.load(open('/tmp/out-$kind/pyproject.toml','rb'))" | |
| test -f "/tmp/out-$kind/.pre-commit-config.yaml" | |
| test -f "/tmp/out-$kind/.github/workflows/ci.yml" | |
| test -f "/tmp/out-$kind/.claude/hooks/session-start.sh" | |
| # The hook is templated, so it can render into invalid shell, and it | |
| # is useless if copier drops the executable bit. | |
| test -x "/tmp/out-$kind/.claude/hooks/session-start.sh" | |
| bash -n "/tmp/out-$kind/.claude/hooks/session-start.sh" | |
| # shellcheck-py builds from source and its build can't verify the | |
| # MITM CA some sandboxes present once Python 3.13 turns on strict | |
| # certificate checking, which breaks `git commit` outright there. The | |
| # pin below the default is what avoids it, so losing the pin while | |
| # raising default_language_version is the regression to catch. | |
| python3 -c "import sys,yaml; cfg=yaml.safe_load(open(sys.argv[1])); hs=[h for r in cfg['repos'] if 'shellcheck-py' in r['repo'] for h in r['hooks'] if h['id']=='shellcheck']; assert len(hs)==1, hs; v=hs[0].get('language_version'); assert v=='python3.12', f'shellcheck language_version is {v!r}'" "/tmp/out-$kind/.pre-commit-config.yaml" | |
| done | |
| # Repo settings as code is opt-in: the defaults carry none of it, so a | |
| # `copier update --defaults` cannot push it into downstream repos. | |
| for kind in app library; do | |
| test ! -e "/tmp/out-$kind/scripts" | |
| test ! -e "/tmp/out-$kind/.github/rulesets" | |
| test ! -e "/tmp/out-$kind/.github/repo-settings.json" | |
| done | |
| # The applier is tested against a stubbed gh, here in the template repo | |
| # (the file's path has copier syntax in it, so the test loads it by path). | |
| uvx --with pytest --from pytest pytest -q tests/test_setup_repo.py | |
| uvx copier copy --trust --defaults --vcs-ref=HEAD \ | |
| --data project_name="sample-repo-settings" \ | |
| --data project_description="A sample managing its repo settings" \ | |
| --data use_repo_settings=true \ | |
| . /tmp/out-repo-settings | |
| # Apply script plus JSON payloads render intact, and the script keeps | |
| # its executable bit. | |
| test -x /tmp/out-repo-settings/scripts/setup_repo.py | |
| python3 -m py_compile /tmp/out-repo-settings/scripts/setup_repo.py | |
| /tmp/out-repo-settings/scripts/setup_repo.py --help > /dev/null | |
| python3 -c "import json,sys; json.load(open(sys.argv[1]))" /tmp/out-repo-settings/.github/rulesets/main.json | |
| python3 -c "import json,sys; json.load(open(sys.argv[1]))" /tmp/out-repo-settings/.github/repo-settings.json | |
| grep -q 'ci / Lint, type-check, and test' /tmp/out-repo-settings/.github/rulesets/main.json | |
| ! grep -q 'frontend /' /tmp/out-repo-settings/.github/rulesets/main.json | |
| echo "repo settings opt-in honoured." | |
| # library-only files present for library, absent for app | |
| test -f /tmp/out-library/RELEASING.md | |
| test -f /tmp/out-library/.github/workflows/publish.yml | |
| test ! -e /tmp/out-app/RELEASING.md | |
| test ! -e /tmp/out-app/.github/workflows/publish.yml | |
| echo "Both variants rendered and validated." | |
| # use_typos defaults on, and answering no drops the hook | |
| uvx copier copy --trust --defaults --vcs-ref=HEAD \ | |
| --data project_name="sample-no-typos" \ | |
| --data project_description="A sample without the typos hook" \ | |
| --data use_typos=false \ | |
| . /tmp/out-no-typos | |
| grep -q "crate-ci/typos" /tmp/out-app/.pre-commit-config.yaml | |
| ! grep -q "crate-ci/typos" /tmp/out-no-typos/.pre-commit-config.yaml | |
| python3 -c "import yaml; yaml.safe_load(open('/tmp/out-no-typos/.pre-commit-config.yaml'))" | |
| echo "typos opt-out honoured." | |
| # use_frontend adds biome.json, the Biome hook, and the node-ci job | |
| uvx copier copy --trust --defaults --vcs-ref=HEAD \ | |
| --data project_name="sample-frontend" \ | |
| --data project_description="A sample with a frontend" \ | |
| --data use_frontend=true \ | |
| --data use_repo_settings=true \ | |
| . /tmp/out-frontend | |
| test -f /tmp/out-frontend/biome.json | |
| grep -q "biomejs/pre-commit" /tmp/out-frontend/.pre-commit-config.yaml | |
| grep -q "node-ci.yml" /tmp/out-frontend/.github/workflows/ci.yml | |
| python3 -c "import json; json.load(open('/tmp/out-frontend/biome.json'))" | |
| python3 -c "import yaml; yaml.safe_load(open('/tmp/out-frontend/.github/workflows/ci.yml'))" | |
| # and its ruleset requires the frontend job too | |
| python3 -c "import json,sys; json.load(open(sys.argv[1]))" /tmp/out-frontend/.github/rulesets/main.json | |
| grep -q 'frontend / Type-check and build' /tmp/out-frontend/.github/rulesets/main.json | |
| # `recommended` is deprecated in Biome 2.5.5; `preset` is the spelling | |
| # that doesn't emit a notice. | |
| grep -q '"preset": "recommended"' /tmp/out-frontend/biome.json | |
| ! grep -q '"recommended": true' /tmp/out-frontend/biome.json | |
| # check-json can't parse the comments tsconfig permits, so a frontend | |
| # scaffold excludes it -- and only a frontend scaffold has one. | |
| grep -q 'exclude: \^frontend/tsconfig' /tmp/out-frontend/.pre-commit-config.yaml | |
| ! grep -q 'tsconfig' /tmp/out-app/.pre-commit-config.yaml | |
| # The session hook points corepack at the npm registry, because | |
| # repo.yarnpkg.com is blocked by some sandbox egress proxies. Only | |
| # useful to a frontend scaffold, so only a frontend scaffold gets it. | |
| test -x /tmp/out-frontend/.claude/hooks/session-start.sh | |
| bash -n /tmp/out-frontend/.claude/hooks/session-start.sh | |
| grep -q 'COREPACK_NPM_REGISTRY' /tmp/out-frontend/.claude/hooks/session-start.sh | |
| ! grep -q 'COREPACK_NPM_REGISTRY' /tmp/out-app/.claude/hooks/session-start.sh | |
| # and adds none of it by default | |
| test ! -e /tmp/out-app/biome.json | |
| ! grep -q "biomejs/pre-commit" /tmp/out-app/.pre-commit-config.yaml | |
| ! grep -q "node-ci.yml" /tmp/out-app/.github/workflows/ci.yml | |
| echo "frontend variant rendered and validated." | |
| # the template-update workflow defaults on, with its baseline | |
| test -f /tmp/out-app/.github/workflows/template-update.yml | |
| test -f /tmp/out-app/.copier-answers.yml | |
| python3 -c "import yaml; yaml.safe_load(open('/tmp/out-app/.github/workflows/template-update.yml'))" | |
| # answering no drops the workflow but KEEPS the answers file, so | |
| # `copier update` by hand still works | |
| uvx copier copy --trust --defaults --vcs-ref=HEAD \ | |
| --data project_name="sample-no-auto-update" \ | |
| --data project_description="A sample without the update workflow" \ | |
| --data use_template_update=false \ | |
| . /tmp/out-no-auto-update | |
| test ! -e /tmp/out-no-auto-update/.github/workflows/template-update.yml | |
| test -f /tmp/out-no-auto-update/.copier-answers.yml | |
| echo "template-update workflow present by default, opt-out honoured." | |
| - name: Run each rendered project's own hooks | |
| # The actual dogfooding, and the only check here that exercises the | |
| # generated config rather than just asserting files exist. Rendering and | |
| # grepping proves biome.json is present and parses; it cannot tell you | |
| # that Biome accepts what it says. | |
| # | |
| # This was added after a fresh frontend scaffold turned out to fail its | |
| # own stack on the first run: the shipped `includes` used a glob Biome | |
| # normalises, so biome-check rewrote biome.json and every new project | |
| # started red with a diff in a file nobody had touched. | |
| # | |
| # pre-commit enumerates through `git ls-files`, so the render has to be | |
| # a git repo with everything staged or every hook silently skips. | |
| run: | | |
| set -euo pipefail | |
| for out in /tmp/out-app /tmp/out-frontend; do | |
| echo "::group::pre-commit in $out" | |
| git -C "$out" init -q | |
| git -C "$out" add -A | |
| ( cd "$out" && uvx pre-commit run --all-files --show-diff-on-failure ) | |
| echo "::endgroup::" | |
| done | |
| # The reusable workflows are `workflow_call`-only, so nothing in this repo | |
| # ever executed them: a change to either shipped straight to every consumer | |
| # on the next `v1` move, untested. Both bugs that surfaced while adopting the | |
| # template in a real repo were of exactly that kind -- `corepack enable` | |
| # ordered after setup-node, and a `run:` line that wasn't valid YAML. | |
| # | |
| # These call the workflows for real against fixtures under tests/smoke/. The | |
| # fixtures are deliberately minimal and are NOT a mirror of the scaffold -- | |
| # template-ci already checks the scaffold's content. Their job is to prove the | |
| # workflows themselves run end to end. | |
| smoke-python-ci: | |
| name: Smoke-test python-ci.yml | |
| uses: ./.github/workflows/python-ci.yml | |
| with: | |
| working-directory: tests/smoke/python | |
| typecheck-paths: src | |
| smoke-node-ci: | |
| name: Smoke-test node-ci.yml | |
| uses: ./.github/workflows/node-ci.yml | |
| with: | |
| working-directory: tests/smoke/node |