From 699100142e40ef36c4eaa9fdc88d747835e3e279 Mon Sep 17 00:00:00 2001 From: Johnathan Falk Date: Tue, 28 Apr 2026 20:06:39 -0400 Subject: [PATCH] fix(scaffold): default docker base to ubuntu:22.04 + drop --break-system-packages Two lessons from running the scaffold against burndown-runner-image: * catthehacker's full-22.04 image is tuned for nektos/act and runs as non-root, which makes apt-get fail with EACCES under buildx multi-arch. Default base is now plain ubuntu:22.04 (works as a Docker base). * --break-system-packages is Python 3.12+ (PEP 668). Older bases (incl. ubuntu:22.04 with Python 3.10) reject the flag, breaking the pip layer. Drop the flag; on a CI/build image, system pip without the flag is fine. Co-Authored-By: Claude Sonnet 4.6 --- scripts/template_repo/push_with_gh.py | 109 +++++++++++++++++- .../template_repo/scaffold_template_repo.py | 43 ++++--- 2 files changed, 132 insertions(+), 20 deletions(-) diff --git a/scripts/template_repo/push_with_gh.py b/scripts/template_repo/push_with_gh.py index e314f33e..03fa4f4b 100755 --- a/scripts/template_repo/push_with_gh.py +++ b/scripts/template_repo/push_with_gh.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/template_repo/push_with_gh.py -# version: 1.0.0 +# version: 1.1.0 # guid: a1b2c3d4-e5f6-7890-abcd-ef0123456789 """Optionally initialize a local git repository and publish it to GitHub using the @@ -13,6 +13,13 @@ Safety: - Does not create submodules or nested repos. Operates only in the target dir. - No modification of parent repos; uses 'git init' in the target directory only. + +Repo settings applied after creation (toggleable with --no-settings): +- Disable merge commits + squash merges (rebase-only) +- Disable Projects + Wiki +- Enable auto-merge, allow_update_branch, delete-branch-on-merge +- Workflow permissions: write + can-approve PRs (lets GH Actions merge) +- Actions permissions: sha_pinning_required = true """ from __future__ import annotations @@ -29,6 +36,84 @@ def run(cmd: list[str], cwd: Path) -> None: subprocess.run(cmd, cwd=str(cwd), check=True) +def apply_repo_settings(owner: str, repo: str) -> None: + """Apply the standard repo-settings preset via the gh CLI. + + Each call is independent — if one fails (e.g. an org-level policy + overrides the request), the rest still run. We log failures but don't + abort: settings can always be re-applied with `--settings-only`. + """ + full = f"{owner}/{repo}" + + # Repo-level toggles (gh repo edit covers most of these directly). + edit_cmd = [ + "gh", + "repo", + "edit", + full, + "--enable-merge-commit=false", + "--enable-squash-merge=false", + "--enable-rebase-merge=true", + "--enable-auto-merge=true", + "--delete-branch-on-merge=true", + "--enable-projects=false", + "--enable-wiki=false", + ] + if subprocess.run(edit_cmd, check=False).returncode != 0: + print(f"warning: gh repo edit failed for {full}", file=sys.stderr) + + # allow_update_branch is not exposed on `gh repo edit`; PATCH the repo. + patch_cmd = [ + "gh", + "api", + "-X", + "PATCH", + f"/repos/{full}", + "-F", + "allow_update_branch=true", + "--silent", + ] + if subprocess.run(patch_cmd, check=False).returncode != 0: + print(f"warning: PATCH allow_update_branch failed for {full}", file=sys.stderr) + + # Workflow permissions: read+write, can approve PRs (so Actions can merge). + wf_cmd = [ + "gh", + "api", + "-X", + "PUT", + f"/repos/{full}/actions/permissions/workflow", + "-F", + "default_workflow_permissions=write", + "-F", + "can_approve_pull_request_reviews=true", + "--silent", + ] + if subprocess.run(wf_cmd, check=False).returncode != 0: + print(f"warning: PUT actions/permissions/workflow failed for {full}", file=sys.stderr) + + # Require pinned-SHA action refs. This rejects @vN tag refs at run time; + # the scaffold's generated workflows already use SHAs to satisfy it. + perms_cmd = [ + "gh", + "api", + "-X", + "PUT", + f"/repos/{full}/actions/permissions", + "-F", + "enabled=true", + "-f", + "allowed_actions=all", + "-F", + "sha_pinning_required=true", + "--silent", + ] + if subprocess.run(perms_cmd, check=False).returncode != 0: + print(f"warning: PUT actions/permissions failed for {full}", file=sys.stderr) + + print(f"Settings applied to https://github.com/{full}") + + def main(argv: list[str]) -> int: parser = argparse.ArgumentParser( description="Initialize git and push to GitHub using gh CLI (no secrets)." @@ -49,8 +134,26 @@ def main(argv: list[str]) -> int: action="store_true", help="Create the repository as private", ) + parser.add_argument( + "--no-settings", + action="store_true", + help="Skip applying the standard repo-settings preset after create", + ) + parser.add_argument( + "--settings-only", + action="store_true", + help=( + "Skip git init / repo create / push; only apply the settings preset " + "to an existing repo. Useful for back-filling settings on repos " + "that were created before this tool was extended." + ), + ) args = parser.parse_args(argv) + if args.settings_only: + apply_repo_settings(args.owner, args.repo) + return 0 + target = Path(os.path.expanduser(args.target)).resolve() if not target.exists() or not target.is_dir(): print( @@ -139,6 +242,10 @@ def main(argv: list[str]) -> int: ) print(f"Repository created and pushed: https://github.com/{args.owner}/{args.repo}") + + if not args.no_settings: + apply_repo_settings(args.owner, args.repo) + return 0 diff --git a/scripts/template_repo/scaffold_template_repo.py b/scripts/template_repo/scaffold_template_repo.py index cd49f55e..4fe4e92b 100755 --- a/scripts/template_repo/scaffold_template_repo.py +++ b/scripts/template_repo/scaffold_template_repo.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/template_repo/scaffold_template_repo.py -# version: 1.5.0 +# version: 1.7.0 # guid: 7f2d3a2e-4b5c-8d9e-0f1a-2b3c4d5e6f70 """Scaffold a minimal, public-safe template repository to a target directory. @@ -1086,14 +1086,17 @@ def plan_docker_overlay(opts: Options) -> list[tuple[Path, str]]: """ # Optional pip layer. + # Note: --break-system-packages is Python 3.12+ (PEP 668). Older bases + # (Ubuntu 22.04 ships 3.10) error on the flag. Omit it; on a CI/build + # image, system-pip without the flag is fine because there's no other + # package manager fighting it. pip_layer = "" if opts.docker_pip_packages: - # Render as one pip install line; quote each spec to allow git+ URLs. spec = " ".join(f'"{p}"' for p in opts.docker_pip_packages) pip_layer = f""" # --- Extra Python packages (system pip; image is a build/CI runner, not a multi-tenant host) --- -RUN python3 -m pip install --no-cache-dir --break-system-packages --upgrade pip \\ - && python3 -m pip install --no-cache-dir --break-system-packages {spec} +RUN python3 -m pip install --no-cache-dir --upgrade pip \\ + && python3 -m pip install --no-cache-dir {spec} """ dockerfile = f"""# file: Dockerfile @@ -1211,22 +1214,22 @@ def render_docker_build_workflow(opts: Options) -> str: runs-on: ubuntu-latest timeout-minutes: 45 steps: - # NOTE: action refs are major-version tags here. The repo's - # pin-actions-to-hashes step (or dependabot) will rewrite each `@vN` - # to a digest-pinned `@ # vN.M.K` on first run. Don't hand-pin - # SHAs at scaffold time — there's no source of truth for "the latest - # known-good SHA" without a network round-trip. + # SHAs below were the latest tagged release at scaffold-version-bump + # time. push_with_gh.py applies sha_pinning_required=true on the new + # repo's Actions permissions, so tag refs would be rejected at run + # time. Dependabot (--with-dependabot) ships PRs to bump these as + # upstream tags ship. - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -1234,7 +1237,7 @@ def render_docker_build_workflow(opts: Options) -> str: - name: Extract metadata (tags, labels) id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | @@ -1247,7 +1250,7 @@ def render_docker_build_workflow(opts: Options) -> str: - name: Build and push id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . platforms: linux/amd64,linux/arm64 @@ -1260,7 +1263,7 @@ def render_docker_build_workflow(opts: Options) -> str: sbom: true - name: Attest provenance - uses: actions/attest-build-provenance@v1 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.build.outputs.digest }} @@ -1343,11 +1346,13 @@ def parse_args(argv: list[str]) -> Options: ) parser.add_argument( "--docker-base-image", - default="ghcr.io/catthehacker/ubuntu:full-22.04", + default="ubuntu:22.04", help=( "Base image for the Dockerfile when --with-docker is set. " - "Default mirrors a GitHub Actions Ubuntu runner; catthehacker's " - "'full-22.04' is the known-good tag — verify before bumping to 24.04." + "Default is vanilla ubuntu:22.04 (works as a Docker base, runs " + "as root, plain apt-get). Pass ghcr.io/catthehacker/ubuntu:full-22.04 " + "ONLY if you also handle non-root + apt permission quirks — that " + "image is tuned for `act`, not buildx multi-arch builds." ), ) parser.add_argument(