From ff0011804a1f0d5329187974223b4d0b9dfd7ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roch=C3=A9=20Compaan?= Date: Tue, 3 Mar 2026 13:53:54 +0200 Subject: [PATCH 01/10] refactor(template): emit nested copier template payload --- Makefile | 62 ++++++-- README.md | 9 +- copier.yml | 5 +- template/copier.yml | 145 ++++++++++++++++++ .../.github/workflows/secret-scan.yaml | 0 .../workflows/semantic-pull-request.yaml | 0 .../.github/workflows/semantic-release.yaml | 0 .../workflows/template-correctness.yaml | 0 template/{ => template}/.gitignore | 0 template/{ => template}/.gitlab-ci.yml | 0 .../{ => template}/.pre-commit-config.yaml | 0 template/{ => template}/.releaserc.json | 0 template/{ => template}/CHANGELOG.md | 0 template/{ => template}/Makefile | 0 template/{ => template}/README.md | 0 template/{ => template}/Taskfile.yml | 0 .../{ => template}/dependencies-dev-init.txt | 0 template/{ => template}/dependencies-init.txt | 0 .../docs/semantic-release-github.md | 0 template/{ => template}/docs/upgrading.md | 0 .../{ => template}/docs/using-template.md | 0 template/{ => template}/justfile | 0 template/{ => template}/package.json | 0 template/{ => template}/scripts/init-dev.sh | 0 template/{ => template}/tasks.py | 0 ...pier_conf.answers_file}}{% endraw %}.jinja | 2 + ...}} => {{_copier_conf.answers_file}}.jinja} | 0 27 files changed, 199 insertions(+), 24 deletions(-) create mode 100644 template/copier.yml rename template/{ => template}/.github/workflows/secret-scan.yaml (100%) rename template/{ => template}/.github/workflows/semantic-pull-request.yaml (100%) rename template/{ => template}/.github/workflows/semantic-release.yaml (100%) rename template/{ => template}/.github/workflows/template-correctness.yaml (100%) rename template/{ => template}/.gitignore (100%) rename template/{ => template}/.gitlab-ci.yml (100%) rename template/{ => template}/.pre-commit-config.yaml (100%) rename template/{ => template}/.releaserc.json (100%) rename template/{ => template}/CHANGELOG.md (100%) rename template/{ => template}/Makefile (100%) rename template/{ => template}/README.md (100%) rename template/{ => template}/Taskfile.yml (100%) rename template/{ => template}/dependencies-dev-init.txt (100%) rename template/{ => template}/dependencies-init.txt (100%) rename template/{ => template}/docs/semantic-release-github.md (100%) rename template/{ => template}/docs/upgrading.md (100%) rename template/{ => template}/docs/using-template.md (100%) rename template/{ => template}/justfile (100%) rename template/{ => template}/package.json (100%) rename template/{ => template}/scripts/init-dev.sh (100%) rename template/{ => template}/tasks.py (100%) create mode 100644 template/template/{% raw %}{{_copier_conf.answers_file}}{% endraw %}.jinja rename template/{{{_copier_conf.answers_file}} => {{_copier_conf.answers_file}}.jinja} (100%) diff --git a/Makefile b/Makefile index 7589711..51e7c57 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ SHELL := bash test-template-render: test-template-render-github test-template-render-gitlab -# Validate GitHub-scaffolded output and task-runner pruning. +# Validate generated template repository structure. test-template-render-github: @set -euo pipefail; \ out_dir="$$(mktemp -d /tmp/scaf-template-github-XXXXXX)"; \ @@ -20,18 +20,33 @@ test-template-render-github: -d copier__github_semantic_release_auth="github_token" \ -d copier__enable_secret_scanning=true \ -d copier__task_runner="task"; \ - test -f "$$out_dir/README.md"; \ + test -f "$$out_dir/copier.yml"; \ test -f "$$out_dir/.copier-answers.yml"; \ - test -f "$$out_dir/Taskfile.yml"; \ - test ! -f "$$out_dir/Makefile"; \ - test ! -f "$$out_dir/justfile"; \ - test -f "$$out_dir/.github/workflows/template-correctness.yaml"; \ - test -f "$$out_dir/.github/workflows/secret-scan.yaml"; \ - test ! -f "$$out_dir/.github/workflows/semantic-release.yaml"; \ - test ! -f "$$out_dir/.gitlab-ci.yml"; \ + test -f "$$out_dir/template/README.md"; \ + test -f "$$out_dir/template/tasks.py"; \ + test -f "$$out_dir/template/Makefile"; \ + test -f "$$out_dir/template/Taskfile.yml"; \ + test -f "$$out_dir/template/justfile"; \ + test -f "$$out_dir/template/.github/workflows/template-correctness.yaml"; \ + test -f "$$out_dir/template/.github/workflows/secret-scan.yaml"; \ + test -f "$$out_dir/template/.github/workflows/semantic-release.yaml"; \ + test -f "$$out_dir/template/.gitlab-ci.yml"; \ + test -f "$$out_dir/template/{{_copier_conf.answers_file}}"; \ + rg -Fq '{{ copier__project_name }}' "$$out_dir/template/README.md"; \ + rg -q '^copier__project_name_raw:' "$$out_dir/copier.yml"; \ + render_dir="$$(mktemp -d /tmp/scaf-template-rendered-gh-XXXXXX)"; \ + copier copy "$$out_dir" "$$render_dir" --trust --defaults \ + -d copier__configure_repo=false \ + -d copier__enable_semantic_release=false \ + -d copier__enable_secret_scanning=false \ + -d copier__ci_provider="github" \ + -d copier__task_runner="task"; \ + test -f "$$render_dir/.copier-answers.yml"; \ + test ! -f "$$render_dir/{{_copier_conf.answers_file}}"; \ + rm -rf "$$render_dir"; \ rm -rf "$$out_dir" -# Validate GitLab-scaffolded output and task-runner pruning. +# Validate generated template repository structure. test-template-render-gitlab: @set -euo pipefail; \ out_dir="$$(mktemp -d /tmp/scaf-template-gitlab-XXXXXX)"; \ @@ -46,11 +61,26 @@ test-template-render-gitlab: -d copier__enable_semantic_release=false \ -d copier__enable_secret_scanning=true \ -d copier__task_runner="just"; \ - test -f "$$out_dir/README.md"; \ + test -f "$$out_dir/copier.yml"; \ test -f "$$out_dir/.copier-answers.yml"; \ - test -f "$$out_dir/justfile"; \ - test ! -f "$$out_dir/Makefile"; \ - test ! -f "$$out_dir/Taskfile.yml"; \ - test -f "$$out_dir/.gitlab-ci.yml"; \ - test ! -d "$$out_dir/.github"; \ + test -f "$$out_dir/template/README.md"; \ + test -f "$$out_dir/template/tasks.py"; \ + test -f "$$out_dir/template/Makefile"; \ + test -f "$$out_dir/template/Taskfile.yml"; \ + test -f "$$out_dir/template/justfile"; \ + test -f "$$out_dir/template/.gitlab-ci.yml"; \ + test -d "$$out_dir/template/.github"; \ + test -f "$$out_dir/template/{{_copier_conf.answers_file}}"; \ + rg -Fq '{{ copier__project_name }}' "$$out_dir/template/README.md"; \ + rg -q '^copier__project_name_raw:' "$$out_dir/copier.yml"; \ + render_dir="$$(mktemp -d /tmp/scaf-template-rendered-gl-XXXXXX)"; \ + copier copy "$$out_dir" "$$render_dir" --trust --defaults \ + -d copier__configure_repo=false \ + -d copier__enable_semantic_release=false \ + -d copier__enable_secret_scanning=false \ + -d copier__ci_provider="gitlab" \ + -d copier__task_runner="just"; \ + test -f "$$render_dir/.copier-answers.yml"; \ + test ! -f "$$render_dir/{{_copier_conf.answers_file}}"; \ + rm -rf "$$render_dir"; \ rm -rf "$$out_dir" diff --git a/README.md b/README.md index 44c2101..60b31f4 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Use this starter to generate new Scaf templates with validated Copier prompts, GitHub/GitLab CI templates, optional semantic-release wiring (GitHub token or GitHub App), optional gitleaks scanning, and generated `make`/`task`/`just` -`init`/`check` commands. +`init`/`check` commands in the emitted `template/` payload. ## Features @@ -18,6 +18,7 @@ GitHub App), optional gitleaks scanning, and generated `make`/`task`/`just` - Optional semantic-release setup and config - Optional secret scanning in CI - Template correctness CI (Copier render checks) +- Generated templates are emitted under `template/` in the produced repository - Choice of local task runner (`make`, `task`, or `just`) with `init`/`check` - Generated project docs for usage and upgrade flow - Apache-2.0 license by default (license selection via Copier option planned) @@ -60,9 +61,9 @@ CI runs the same command in `.github/workflows/template-render-tests.yaml`. Each generated project includes: -- `docs/using-template.md` -- `docs/upgrading.md` -- `docs/semantic-release-github.md` (when GitHub + semantic-release is enabled) +- `template/docs/using-template.md` +- `template/docs/upgrading.md` +- `template/docs/semantic-release-github.md` (when GitHub + semantic-release is enabled) These cover day-to-day usage and copier update workflow for downstream projects. diff --git a/copier.yml b/copier.yml index a19a821..4baa8cc 100644 --- a/copier.yml +++ b/copier.yml @@ -1,9 +1,6 @@ -_templates_suffix: "" +_templates_suffix: ".jinja" _subdirectory: template -_tasks: - - python tasks.py - copier__project_name_raw: type: str default: "My Scaf Template" diff --git a/template/copier.yml b/template/copier.yml new file mode 100644 index 0000000..a19a821 --- /dev/null +++ b/template/copier.yml @@ -0,0 +1,145 @@ +_templates_suffix: "" +_subdirectory: template + +_tasks: + - python tasks.py + +copier__project_name_raw: + type: str + default: "My Scaf Template" + help: "The generated project name." + validator: >- + {% if not copier__project_name_raw.strip() %} + "Project name cannot be empty." + {% endif %} + +copier__project_name: + type: str + default: "{{ copier__project_name_raw | replace(\"'\", \"\") | replace('\"', '') }}" + when: false + +copier__project_slug: + type: str + default: "{{ is_project_slug if is_project_slug else copier__project_name.lower().replace(' ', '_').replace('-', '_').replace('.', '_').strip() }}" + help: "Python identifier-style slug." + validator: >- + {% if not copier__project_slug.isidentifier() %} + "'{{ copier__project_slug }}' is not a valid Python identifier." + {% elif copier__project_slug != copier__project_slug.lower() %} + "'{{ copier__project_slug }}' must be lowercase." + {% elif not copier__project_slug.strip() %} + "Project slug cannot be empty." + {% endif %} + +copier__project_dash: + type: str + default: "{{ copier__project_slug.lower().replace(' ', '-').replace('_', '-').replace('.', '-').strip() }}" + when: false + +copier__description: + type: str + default: "Template for new scaf templates" + help: "Short project description." + +copier__author_name: + type: str + default: "Template Author" + help: "Author name." + +copier__email: + type: str + default: "{{ copier__author_name.lower().replace(' ', '.') }}@example.com" + help: "Author email." + +copier__configure_repo: + type: bool + default: true + help: "Configure a git remote for the generated project." + +copier__repo_provider: + type: str + default: github + choices: + - github + - gitlab + help: "Repository provider." + when: "{{ copier__configure_repo }}" + +copier__repo_org: + type: str + default: "getscaf" + help: "Repository organization/group." + when: "{{ copier__configure_repo }}" + +copier__repo_name: + type: str + default: "{{ copier__project_dash }}" + help: "Repository name." + when: "{{ copier__configure_repo }}" + +copier__repo_url: + type: str + default: "{{ ('git@github.com:' + copier__repo_org + '/' + copier__repo_name + '.git') if (copier__configure_repo and copier__repo_provider == 'github') else (('git@gitlab.com:' + copier__repo_org + '/' + copier__repo_name + '.git') if copier__configure_repo else '') }}" + when: false + +copier__create_repo: + type: bool + default: true + help: "Create the repository automatically if it does not exist (gh for GitHub, glab for GitLab)." + when: "{{ copier__configure_repo }}" + +copier__repo_visibility: + type: str + default: public + choices: + - private + - public + help: "Visibility to use when creating the repository." + when: "{{ copier__configure_repo and copier__create_repo }}" + +copier__version: + type: str + default: "0.1.0" + help: "Initial project version." + +copier__ci_provider: + type: str + default: "{{ copier__repo_provider if copier__configure_repo else 'github' }}" + help: "CI provider to scaffold." + choices: + - github + - gitlab + when: "{{ not copier__configure_repo }}" + +copier__enable_semantic_release: + type: bool + default: true + help: "Include semantic-release config and CI release job." + +copier__github_semantic_release_auth: + type: str + default: github_token + help: "GitHub semantic-release authentication method." + choices: + - github_token + - github_app + when: "{{ copier__ci_provider == 'github' and copier__enable_semantic_release }}" + +copier__enable_secret_scanning: + type: bool + default: true + help: "Include secret scanning in CI." + +copier__task_runner: + type: str + default: task + help: "Primary local task runner." + choices: + - make + - task + - just + +copier__template_sample_name: + type: str + default: "sample" + help: "Sample project name used by template correctness CI." diff --git a/template/.github/workflows/secret-scan.yaml b/template/template/.github/workflows/secret-scan.yaml similarity index 100% rename from template/.github/workflows/secret-scan.yaml rename to template/template/.github/workflows/secret-scan.yaml diff --git a/template/.github/workflows/semantic-pull-request.yaml b/template/template/.github/workflows/semantic-pull-request.yaml similarity index 100% rename from template/.github/workflows/semantic-pull-request.yaml rename to template/template/.github/workflows/semantic-pull-request.yaml diff --git a/template/.github/workflows/semantic-release.yaml b/template/template/.github/workflows/semantic-release.yaml similarity index 100% rename from template/.github/workflows/semantic-release.yaml rename to template/template/.github/workflows/semantic-release.yaml diff --git a/template/.github/workflows/template-correctness.yaml b/template/template/.github/workflows/template-correctness.yaml similarity index 100% rename from template/.github/workflows/template-correctness.yaml rename to template/template/.github/workflows/template-correctness.yaml diff --git a/template/.gitignore b/template/template/.gitignore similarity index 100% rename from template/.gitignore rename to template/template/.gitignore diff --git a/template/.gitlab-ci.yml b/template/template/.gitlab-ci.yml similarity index 100% rename from template/.gitlab-ci.yml rename to template/template/.gitlab-ci.yml diff --git a/template/.pre-commit-config.yaml b/template/template/.pre-commit-config.yaml similarity index 100% rename from template/.pre-commit-config.yaml rename to template/template/.pre-commit-config.yaml diff --git a/template/.releaserc.json b/template/template/.releaserc.json similarity index 100% rename from template/.releaserc.json rename to template/template/.releaserc.json diff --git a/template/CHANGELOG.md b/template/template/CHANGELOG.md similarity index 100% rename from template/CHANGELOG.md rename to template/template/CHANGELOG.md diff --git a/template/Makefile b/template/template/Makefile similarity index 100% rename from template/Makefile rename to template/template/Makefile diff --git a/template/README.md b/template/template/README.md similarity index 100% rename from template/README.md rename to template/template/README.md diff --git a/template/Taskfile.yml b/template/template/Taskfile.yml similarity index 100% rename from template/Taskfile.yml rename to template/template/Taskfile.yml diff --git a/template/dependencies-dev-init.txt b/template/template/dependencies-dev-init.txt similarity index 100% rename from template/dependencies-dev-init.txt rename to template/template/dependencies-dev-init.txt diff --git a/template/dependencies-init.txt b/template/template/dependencies-init.txt similarity index 100% rename from template/dependencies-init.txt rename to template/template/dependencies-init.txt diff --git a/template/docs/semantic-release-github.md b/template/template/docs/semantic-release-github.md similarity index 100% rename from template/docs/semantic-release-github.md rename to template/template/docs/semantic-release-github.md diff --git a/template/docs/upgrading.md b/template/template/docs/upgrading.md similarity index 100% rename from template/docs/upgrading.md rename to template/template/docs/upgrading.md diff --git a/template/docs/using-template.md b/template/template/docs/using-template.md similarity index 100% rename from template/docs/using-template.md rename to template/template/docs/using-template.md diff --git a/template/justfile b/template/template/justfile similarity index 100% rename from template/justfile rename to template/template/justfile diff --git a/template/package.json b/template/template/package.json similarity index 100% rename from template/package.json rename to template/template/package.json diff --git a/template/scripts/init-dev.sh b/template/template/scripts/init-dev.sh similarity index 100% rename from template/scripts/init-dev.sh rename to template/template/scripts/init-dev.sh diff --git a/template/tasks.py b/template/template/tasks.py similarity index 100% rename from template/tasks.py rename to template/template/tasks.py diff --git a/template/template/{% raw %}{{_copier_conf.answers_file}}{% endraw %}.jinja b/template/template/{% raw %}{{_copier_conf.answers_file}}{% endraw %}.jinja new file mode 100644 index 0000000..69d12b7 --- /dev/null +++ b/template/template/{% raw %}{{_copier_conf.answers_file}}{% endraw %}.jinja @@ -0,0 +1,2 @@ +# Changes here will be overwritten by Copier +{% raw %}{{ _copier_answers|to_nice_yaml -}}{% endraw %} diff --git a/template/{{_copier_conf.answers_file}} b/template/{{_copier_conf.answers_file}}.jinja similarity index 100% rename from template/{{_copier_conf.answers_file}} rename to template/{{_copier_conf.answers_file}}.jinja From bd83beb265f397d3e1ad4bbdd7e6f0db2a3abebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roch=C3=A9=20Compaan?= Date: Fri, 6 Mar 2026 19:26:36 +0200 Subject: [PATCH 02/10] fix(template): add root guide/workflow and preserve README rendering --- Makefile | 6 +++ .../workflows/template-correctness.yaml | 45 +++++++++++++++++++ template/README.md.jinja | 27 +++++++++++ .../template/{README.md => README.md.jinja} | 3 +- 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 template/.github/workflows/template-correctness.yaml create mode 100644 template/README.md.jinja rename template/template/{README.md => README.md.jinja} (94%) diff --git a/Makefile b/Makefile index 51e7c57..ab313ee 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,8 @@ test-template-render-github: -d copier__enable_secret_scanning=true \ -d copier__task_runner="task"; \ test -f "$$out_dir/copier.yml"; \ + test -f "$$out_dir/README.md"; \ + test -f "$$out_dir/.github/workflows/template-correctness.yaml"; \ test -f "$$out_dir/.copier-answers.yml"; \ test -f "$$out_dir/template/README.md"; \ test -f "$$out_dir/template/tasks.py"; \ @@ -33,6 +35,7 @@ test-template-render-github: test -f "$$out_dir/template/.gitlab-ci.yml"; \ test -f "$$out_dir/template/{{_copier_conf.answers_file}}"; \ rg -Fq '{{ copier__project_name }}' "$$out_dir/template/README.md"; \ + rg -Fq 'copier copy . /path/to/new-project --trust' "$$out_dir/README.md"; \ rg -q '^copier__project_name_raw:' "$$out_dir/copier.yml"; \ render_dir="$$(mktemp -d /tmp/scaf-template-rendered-gh-XXXXXX)"; \ copier copy "$$out_dir" "$$render_dir" --trust --defaults \ @@ -62,6 +65,8 @@ test-template-render-gitlab: -d copier__enable_secret_scanning=true \ -d copier__task_runner="just"; \ test -f "$$out_dir/copier.yml"; \ + test -f "$$out_dir/README.md"; \ + test -f "$$out_dir/.github/workflows/template-correctness.yaml"; \ test -f "$$out_dir/.copier-answers.yml"; \ test -f "$$out_dir/template/README.md"; \ test -f "$$out_dir/template/tasks.py"; \ @@ -72,6 +77,7 @@ test-template-render-gitlab: test -d "$$out_dir/template/.github"; \ test -f "$$out_dir/template/{{_copier_conf.answers_file}}"; \ rg -Fq '{{ copier__project_name }}' "$$out_dir/template/README.md"; \ + rg -Fq 'copier copy . /path/to/new-project --trust' "$$out_dir/README.md"; \ rg -q '^copier__project_name_raw:' "$$out_dir/copier.yml"; \ render_dir="$$(mktemp -d /tmp/scaf-template-rendered-gl-XXXXXX)"; \ copier copy "$$out_dir" "$$render_dir" --trust --defaults \ diff --git a/template/.github/workflows/template-correctness.yaml b/template/.github/workflows/template-correctness.yaml new file mode 100644 index 0000000..f4aa1eb --- /dev/null +++ b/template/.github/workflows/template-correctness.yaml @@ -0,0 +1,45 @@ +name: Template Correctness + +on: + push: + branches: [main] + pull_request: + +jobs: + render-template: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install copier + run: pip install copier + + - name: Render sample project + run: | + copier copy . /tmp/sample-project --trust --defaults \ + -d copier__project_name_raw="Sample Project" \ + -d copier__project_slug="sample_project" \ + -d copier__description="Sample generated project" \ + -d copier__author_name="Sample Author" \ + -d copier__email="sample@example.com" \ + -d copier__version="0.1.0" \ + -d copier__configure_repo=false \ + -d copier__ci_provider="github" \ + -d copier__enable_semantic_release=false \ + -d copier__enable_secret_scanning=false \ + -d copier__task_runner="task" + + - name: Validate generated project files + run: | + test -f /tmp/sample-project/README.md + test -f /tmp/sample-project/.copier-answers.yml + test -f /tmp/sample-project/Taskfile.yml + test ! -f /tmp/sample-project/Makefile + test ! -f /tmp/sample-project/justfile diff --git a/template/README.md.jinja b/template/README.md.jinja new file mode 100644 index 0000000..fe1ab7f --- /dev/null +++ b/template/README.md.jinja @@ -0,0 +1,27 @@ +# {{ copier__project_name }} + +{{ copier__description }} + +This repository is a Copier template. It renders project scaffolding from the `template/` directory. + +## Use This Template + +```bash +# From a local clone +copier copy . /path/to/new-project --trust + +# Or from a remote repository URL +copier copy /path/to/new-project --trust +``` + +## What Gets Rendered + +- Project metadata and docs +- CI scaffolding +- Optional semantic-release wiring +- Optional secret scanning +- Task runner files (`make`, `task`, or `just`, pruned by selected option) + +## Template Validation + +This repository includes `.github/workflows/template-correctness.yaml`, which renders a sample project on every push/PR to verify the template remains valid. diff --git a/template/template/README.md b/template/template/README.md.jinja similarity index 94% rename from template/template/README.md rename to template/template/README.md.jinja index 18996c8..c4bf84d 100644 --- a/template/template/README.md +++ b/template/template/README.md.jinja @@ -1,4 +1,4 @@ -# {{ copier__project_name }} +{% raw %}# {{ copier__project_name }} {{ copier__description }} @@ -43,3 +43,4 @@ task check - [Upgrading Projects](docs/upgrading.md) {% if copier__ci_provider == "github" and copier__enable_semantic_release %}- [GitHub Semantic Release Setup](docs/semantic-release-github.md) {% endif %} +{% endraw %} From b25872c1ec3d75dd12268c8449a67162f44b7669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roch=C3=A9=20Compaan?= Date: Sat, 7 Mar 2026 14:10:25 +0200 Subject: [PATCH 03/10] fix(template): run first-stage tasks for generated template repo --- Makefile | 21 ++++++++++++------ copier.yml | 3 +++ .../{init-dev.sh => init-dev.sh.jinja} | 0 .../template/{tasks.py => tasks.py.jinja} | 22 +++++++++++-------- 4 files changed, 30 insertions(+), 16 deletions(-) rename template/template/scripts/{init-dev.sh => init-dev.sh.jinja} (100%) rename template/template/{tasks.py => tasks.py.jinja} (91%) diff --git a/Makefile b/Makefile index ab313ee..705ad61 100644 --- a/Makefile +++ b/Makefile @@ -26,13 +26,17 @@ test-template-render-github: test -f "$$out_dir/.copier-answers.yml"; \ test -f "$$out_dir/template/README.md"; \ test -f "$$out_dir/template/tasks.py"; \ - test -f "$$out_dir/template/Makefile"; \ test -f "$$out_dir/template/Taskfile.yml"; \ - test -f "$$out_dir/template/justfile"; \ + test ! -f "$$out_dir/template/Makefile"; \ + test ! -f "$$out_dir/template/justfile"; \ test -f "$$out_dir/template/.github/workflows/template-correctness.yaml"; \ test -f "$$out_dir/template/.github/workflows/secret-scan.yaml"; \ - test -f "$$out_dir/template/.github/workflows/semantic-release.yaml"; \ - test -f "$$out_dir/template/.gitlab-ci.yml"; \ + test ! -f "$$out_dir/template/.github/workflows/semantic-release.yaml"; \ + test ! -f "$$out_dir/template/.github/workflows/semantic-pull-request.yaml"; \ + test ! -f "$$out_dir/template/.gitlab-ci.yml"; \ + test ! -f "$$out_dir/template/package.json"; \ + test ! -f "$$out_dir/template/dependencies-init.txt"; \ + test ! -f "$$out_dir/template/dependencies-dev-init.txt"; \ test -f "$$out_dir/template/{{_copier_conf.answers_file}}"; \ rg -Fq '{{ copier__project_name }}' "$$out_dir/template/README.md"; \ rg -Fq 'copier copy . /path/to/new-project --trust' "$$out_dir/README.md"; \ @@ -70,11 +74,14 @@ test-template-render-gitlab: test -f "$$out_dir/.copier-answers.yml"; \ test -f "$$out_dir/template/README.md"; \ test -f "$$out_dir/template/tasks.py"; \ - test -f "$$out_dir/template/Makefile"; \ - test -f "$$out_dir/template/Taskfile.yml"; \ test -f "$$out_dir/template/justfile"; \ + test ! -f "$$out_dir/template/Makefile"; \ + test ! -f "$$out_dir/template/Taskfile.yml"; \ test -f "$$out_dir/template/.gitlab-ci.yml"; \ - test -d "$$out_dir/template/.github"; \ + test ! -d "$$out_dir/template/.github"; \ + test ! -f "$$out_dir/template/package.json"; \ + test ! -f "$$out_dir/template/dependencies-init.txt"; \ + test ! -f "$$out_dir/template/dependencies-dev-init.txt"; \ test -f "$$out_dir/template/{{_copier_conf.answers_file}}"; \ rg -Fq '{{ copier__project_name }}' "$$out_dir/template/README.md"; \ rg -Fq 'copier copy . /path/to/new-project --trust' "$$out_dir/README.md"; \ diff --git a/copier.yml b/copier.yml index 4baa8cc..4881afc 100644 --- a/copier.yml +++ b/copier.yml @@ -1,6 +1,9 @@ _templates_suffix: ".jinja" _subdirectory: template +_tasks: + - python template/tasks.py + copier__project_name_raw: type: str default: "My Scaf Template" diff --git a/template/template/scripts/init-dev.sh b/template/template/scripts/init-dev.sh.jinja similarity index 100% rename from template/template/scripts/init-dev.sh rename to template/template/scripts/init-dev.sh.jinja diff --git a/template/template/tasks.py b/template/template/tasks.py.jinja similarity index 91% rename from template/template/tasks.py rename to template/template/tasks.py.jinja index 137926d..f09230e 100644 --- a/template/template/tasks.py +++ b/template/template/tasks.py.jinja @@ -16,7 +16,8 @@ CREATE_REPO = {{ "True" if copier__create_repo else "False" }} REPO_VISIBILITY = "{{ copier__repo_visibility }}" -ROOT = pathlib.Path(".") +PROJECT_ROOT = pathlib.Path.cwd() +TEMPLATE_ROOT = pathlib.Path(__file__).resolve().parent TERMINATOR = "\x1b[0m" WARNING = "\x1b[1;33m [WARNING]: " INFO = "\x1b[1;33m [INFO]: " @@ -24,7 +25,7 @@ def remove(path: str) -> None: - p = ROOT / path + p = TEMPLATE_ROOT / path if p.exists(): if p.is_file() or p.is_symlink(): p.unlink() @@ -37,21 +38,23 @@ def remove(path: str) -> None: p.rmdir() -def run(cmd: list[str]) -> None: - subprocess.run(cmd, check=True) +def run(cmd: list[str], *, cwd: pathlib.Path | None = None) -> None: + subprocess.run(cmd, check=True, cwd=cwd) def run_init_script() -> None: - run(["bash", "./scripts/init-dev.sh"]) + run(["bash", "./scripts/init-dev.sh"], cwd=TEMPLATE_ROOT) def init_git_repo() -> None: - if (ROOT / ".git").exists(): + if (PROJECT_ROOT / ".git").exists(): return print(INFO + "Initializing git repository..." + TERMINATOR) - print(INFO + f"Current working directory: {os.getcwd()}" + TERMINATOR) + print(INFO + f"Current working directory: {PROJECT_ROOT}" + TERMINATOR) subprocess.run( - shlex.split("git -c init.defaultBranch=main init . --quiet"), check=True + shlex.split("git -c init.defaultBranch=main init . --quiet"), + check=True, + cwd=PROJECT_ROOT, ) print(SUCCESS + "Git repository initialized." + TERMINATOR) @@ -66,6 +69,7 @@ def configure_git_remote() -> None: shlex.split("git remote get-url origin"), capture_output=True, text=True, + cwd=PROJECT_ROOT, ) if existing_origin.returncode == 0: current_origin = existing_origin.stdout.strip() @@ -76,7 +80,7 @@ def configure_git_remote() -> None: ) return command = f"git remote add origin {repo_url}" - subprocess.run(shlex.split(command), check=True) + subprocess.run(shlex.split(command), check=True, cwd=PROJECT_ROOT) print(SUCCESS + f"Remote origin={repo_url} added." + TERMINATOR) else: print( From aeac2f56fcb878dff75a7dee82724701ab9068f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roch=C3=A9=20Compaan?= Date: Sat, 7 Mar 2026 14:54:41 +0200 Subject: [PATCH 04/10] fix(tasks): skip starter-stage npm init in nested template --- copier.yml | 2 +- template/template/scripts/init-dev.sh.jinja | 4 +++- template/template/tasks.py.jinja | 11 ++++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/copier.yml b/copier.yml index 4881afc..bb36d3f 100644 --- a/copier.yml +++ b/copier.yml @@ -2,7 +2,7 @@ _templates_suffix: ".jinja" _subdirectory: template _tasks: - - python template/tasks.py + - SCAF_TEMPLATE_STARTER_STAGE=1 python template/tasks.py copier__project_name_raw: type: str diff --git a/template/template/scripts/init-dev.sh.jinja b/template/template/scripts/init-dev.sh.jinja index 7d3d47c..d87ccab 100755 --- a/template/template/scripts/init-dev.sh.jinja +++ b/template/template/scripts/init-dev.sh.jinja @@ -6,7 +6,9 @@ if command -v pre-commit >/dev/null 2>&1 && git rev-parse --is-inside-work-tree fi {% if copier__enable_semantic_release %} -if command -v docker >/dev/null 2>&1; then +if [[ "${SCAF_SKIP_INIT_DEV_NPM:-0}" == "1" ]]; then + echo "Skipping semantic-release dependency install for starter-stage render" +elif command -v docker >/dev/null 2>&1; then docker run --rm --user "$(id -u):$(id -g)" -w "/app" -v "$(pwd):/app" -e npm_config_cache=/tmp/.npm node:lts /bin/bash -c \ "xargs npm install < dependencies-init.txt" docker run --rm --user "$(id -u):$(id -g)" -w "/app" -v "$(pwd):/app" -e npm_config_cache=/tmp/.npm node:lts /bin/bash -c \ diff --git a/template/template/tasks.py.jinja b/template/template/tasks.py.jinja index f09230e..9566481 100644 --- a/template/template/tasks.py.jinja +++ b/template/template/tasks.py.jinja @@ -38,12 +38,17 @@ def remove(path: str) -> None: p.rmdir() -def run(cmd: list[str], *, cwd: pathlib.Path | None = None) -> None: - subprocess.run(cmd, check=True, cwd=cwd) +def run( + cmd: list[str], *, cwd: pathlib.Path | None = None, env: dict[str, str] | None = None +) -> None: + subprocess.run(cmd, check=True, cwd=cwd, env=env) def run_init_script() -> None: - run(["bash", "./scripts/init-dev.sh"], cwd=TEMPLATE_ROOT) + env = os.environ.copy() + if env.get("SCAF_TEMPLATE_STARTER_STAGE") == "1": + env["SCAF_SKIP_INIT_DEV_NPM"] = "1" + run(["bash", "./scripts/init-dev.sh"], cwd=TEMPLATE_ROOT, env=env) def init_git_repo() -> None: From 9a5cf8947aeeeac38bcb91422417672770d3d497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roch=C3=A9=20Compaan?= Date: Sat, 7 Mar 2026 15:00:09 +0200 Subject: [PATCH 05/10] fix(init): skip deps install for unresolved template files --- template/template/scripts/init-dev.sh.jinja | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/template/template/scripts/init-dev.sh.jinja b/template/template/scripts/init-dev.sh.jinja index d87ccab..acd1452 100755 --- a/template/template/scripts/init-dev.sh.jinja +++ b/template/template/scripts/init-dev.sh.jinja @@ -6,16 +6,22 @@ if command -v pre-commit >/dev/null 2>&1 && git rev-parse --is-inside-work-tree fi {% if copier__enable_semantic_release %} +has_unrendered_jinja() { + grep -Eq '[{][{]|[{][%]' dependencies-init.txt dependencies-dev-init.txt +} + if [[ "${SCAF_SKIP_INIT_DEV_NPM:-0}" == "1" ]]; then echo "Skipping semantic-release dependency install for starter-stage render" +elif has_unrendered_jinja; then + echo "Skipping semantic-release dependency install for unresolved template files" +elif command -v npm >/dev/null 2>&1; then + xargs npm install < dependencies-init.txt + xargs npm install --save-dev < dependencies-dev-init.txt elif command -v docker >/dev/null 2>&1; then docker run --rm --user "$(id -u):$(id -g)" -w "/app" -v "$(pwd):/app" -e npm_config_cache=/tmp/.npm node:lts /bin/bash -c \ "xargs npm install < dependencies-init.txt" docker run --rm --user "$(id -u):$(id -g)" -w "/app" -v "$(pwd):/app" -e npm_config_cache=/tmp/.npm node:lts /bin/bash -c \ "xargs npm install --save-dev < dependencies-dev-init.txt" -elif command -v npm >/dev/null 2>&1; then - xargs npm install < dependencies-init.txt - xargs npm install --save-dev < dependencies-dev-init.txt else echo "docker and npm not found; skipping semantic-release dependency install" fi From c0a6cb8743dd7c8bfc9a6bacd0aed076d4fed46e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roch=C3=A9=20Compaan?= Date: Sat, 7 Mar 2026 15:21:53 +0200 Subject: [PATCH 06/10] refactor(tasks): split starter and template post-copy scripts --- Makefile | 4 +- copier.yml | 2 +- template/.scaf/starter-post-copy.sh.jinja | 211 ++++++++++++++++++ template/copier.yml | 2 +- template/template/.scaf/post-copy.sh.jinja | 163 ++++++++++++++ template/template/scripts/init-dev.sh.jinja | 10 +- template/template/tasks.py.jinja | 227 -------------------- 7 files changed, 379 insertions(+), 240 deletions(-) create mode 100644 template/.scaf/starter-post-copy.sh.jinja create mode 100644 template/template/.scaf/post-copy.sh.jinja delete mode 100644 template/template/tasks.py.jinja diff --git a/Makefile b/Makefile index 705ad61..70d0b95 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ test-template-render-github: test -f "$$out_dir/.github/workflows/template-correctness.yaml"; \ test -f "$$out_dir/.copier-answers.yml"; \ test -f "$$out_dir/template/README.md"; \ - test -f "$$out_dir/template/tasks.py"; \ + test -f "$$out_dir/template/.scaf/post-copy.sh"; \ test -f "$$out_dir/template/Taskfile.yml"; \ test ! -f "$$out_dir/template/Makefile"; \ test ! -f "$$out_dir/template/justfile"; \ @@ -73,7 +73,7 @@ test-template-render-gitlab: test -f "$$out_dir/.github/workflows/template-correctness.yaml"; \ test -f "$$out_dir/.copier-answers.yml"; \ test -f "$$out_dir/template/README.md"; \ - test -f "$$out_dir/template/tasks.py"; \ + test -f "$$out_dir/template/.scaf/post-copy.sh"; \ test -f "$$out_dir/template/justfile"; \ test ! -f "$$out_dir/template/Makefile"; \ test ! -f "$$out_dir/template/Taskfile.yml"; \ diff --git a/copier.yml b/copier.yml index bb36d3f..3ecb524 100644 --- a/copier.yml +++ b/copier.yml @@ -2,7 +2,7 @@ _templates_suffix: ".jinja" _subdirectory: template _tasks: - - SCAF_TEMPLATE_STARTER_STAGE=1 python template/tasks.py + - bash .scaf/starter-post-copy.sh copier__project_name_raw: type: str diff --git a/template/.scaf/starter-post-copy.sh.jinja b/template/.scaf/starter-post-copy.sh.jinja new file mode 100644 index 0000000..68a91ea --- /dev/null +++ b/template/.scaf/starter-post-copy.sh.jinja @@ -0,0 +1,211 @@ +#!/usr/bin/env bash +set -euo pipefail + +CI_PROVIDER="{{ copier__ci_provider }}" +SEMANTIC_RELEASE="{{ 'true' if copier__enable_semantic_release else 'false' }}" +SECRET_SCANNING="{{ 'true' if copier__enable_secret_scanning else 'false' }}" +TASK_RUNNER="{{ copier__task_runner }}" +CONFIGURE_REPO="{{ 'true' if copier__configure_repo else 'false' }}" +REPO_PROVIDER="{{ copier__repo_provider }}" +REPO_ORG="{{ copier__repo_org }}" +REPO_NAME="{{ copier__repo_name }}" +REPO_URL="{{ copier__repo_url }}" +CREATE_REPO="{{ 'true' if copier__create_repo else 'false' }}" +REPO_VISIBILITY="{{ copier__repo_visibility }}" + +TERMINATOR="\033[0m" +WARNING="\033[1;33m [WARNING]: " +INFO="\033[1;33m [INFO]: " +SUCCESS="\033[1;32m [SUCCESS]: " + +is_true() { + [[ "${1:-false}" == "true" ]] +} + +log_info() { + printf "%b%s%b\n" "$INFO" "$1" "$TERMINATOR" +} + +log_warning() { + printf "%b%s%b\n" "$WARNING" "$1" "$TERMINATOR" +} + +log_success() { + printf "%b%s%b\n" "$SUCCESS" "$1" "$TERMINATOR" +} + +remove_path() { + local path="$1" + [[ -e "$path" ]] || return 0 + rm -rf "$path" +} + +install_semantic_release_deps() { + local init_file="$1" + local dev_file="$2" + + if command -v npm >/dev/null 2>&1; then + [[ -s "$init_file" ]] && xargs npm install < "$init_file" + [[ -s "$dev_file" ]] && xargs npm install --save-dev < "$dev_file" + return + fi + + if command -v docker >/dev/null 2>&1; then + docker run --rm --user "$(id -u):$(id -g)" -w "/app" -v "$(pwd):/app" -e npm_config_cache=/tmp/.npm node:lts /bin/bash -c \ + "if [ -s \"$init_file\" ]; then xargs npm install < \"$init_file\"; fi; if [ -s \"$dev_file\" ]; then xargs npm install --save-dev < \"$dev_file\"; fi" + return + fi + + log_warning "docker and npm not found; skipping semantic-release dependency install" +} + +run_template_init() { + ( + cd template + + if command -v pre-commit >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + pre-commit install || echo "pre-commit install failed; continuing" + fi + + if is_true "$SEMANTIC_RELEASE"; then + local tmp_init_file tmp_dev_file tmp_dev_filtered + tmp_init_file="$(mktemp ./.scaf-deps-init.XXXXXX)" + tmp_dev_file="$(mktemp ./.scaf-deps-dev.XXXXXX)" + tmp_dev_filtered="${tmp_dev_file}.filtered" + + sed -E '/[{][{]/d; /[{][%]/d; /^[[:space:]]*$/d' dependencies-init.txt > "$tmp_init_file" + sed -E '/[{][{]/d; /[{][%]/d; /^[[:space:]]*$/d' dependencies-dev-init.txt > "$tmp_dev_file" + + if [[ "$CI_PROVIDER" == "github" ]]; then + awk '!/@semantic-release\/gitlab/' "$tmp_dev_file" > "$tmp_dev_filtered" + mv "$tmp_dev_filtered" "$tmp_dev_file" + elif [[ "$CI_PROVIDER" == "gitlab" ]]; then + awk '!/@semantic-release\/github/' "$tmp_dev_file" > "$tmp_dev_filtered" + mv "$tmp_dev_filtered" "$tmp_dev_file" + fi + + install_semantic_release_deps "$tmp_init_file" "$tmp_dev_file" + rm -f "$tmp_init_file" "$tmp_dev_file" "$tmp_dev_filtered" + fi + + echo "Local development setup complete." + ) +} + +init_git_repo() { + if [[ -d .git ]]; then + return + fi + log_info "Initializing git repository..." + log_info "Current working directory: $(pwd)" + git -c init.defaultBranch=main init . --quiet + log_success "Git repository initialized." +} + +configure_git_remote() { + if ! is_true "$CONFIGURE_REPO"; then + return + fi + + local repo_url + repo_url="$(printf "%s" "$REPO_URL" | xargs)" + if [[ -z "$repo_url" ]]; then + log_warning "No repo_url provided. Skipping git remote configuration." + return + fi + + log_info "repo_url: $repo_url" + if git remote get-url origin >/dev/null 2>&1; then + local current_origin + current_origin="$(git remote get-url origin)" + log_info "Remote origin already configured (${current_origin}). Skipping add." + return + fi + + git remote add origin "$repo_url" + log_success "Remote origin=${repo_url} added." +} + +maybe_create_repo() { + if ! is_true "$CREATE_REPO" || ! is_true "$CONFIGURE_REPO"; then + return + fi + + if [[ -z "${REPO_ORG// }" || -z "${REPO_NAME// }" ]]; then + log_warning "Repo org/name not set. Skipping repo creation." + return + fi + + local repo_full_name="${REPO_ORG}/${REPO_NAME}" + + if [[ "$REPO_PROVIDER" == "github" ]]; then + if ! command -v gh >/dev/null 2>&1; then + log_warning "gh CLI is not installed. Skipping repo creation." + return + fi + if GH_HOST=github.com gh repo view "$repo_full_name" >/dev/null 2>&1; then + log_info "GitHub repository ${repo_full_name} already exists." + return + fi + log_info "Creating GitHub repository ${repo_full_name} (${REPO_VISIBILITY})..." + if ! create_error="$(GH_HOST=github.com gh repo create "$repo_full_name" "--${REPO_VISIBILITY}" 2>&1)"; then + log_warning "Failed to create GitHub repository ${repo_full_name}: ${create_error}" + return + fi + log_success "GitHub repository ${repo_full_name} created." + return + fi + + if [[ "$REPO_PROVIDER" == "gitlab" ]]; then + if ! command -v glab >/dev/null 2>&1; then + log_warning "glab CLI is not installed. Skipping repo creation." + return + fi + if GITLAB_HOST=gitlab.com glab repo view "$repo_full_name" >/dev/null 2>&1; then + log_info "GitLab repository ${repo_full_name} already exists." + return + fi + log_info "Creating GitLab repository ${repo_full_name} (${REPO_VISIBILITY})..." + if ! create_error="$(GITLAB_HOST=gitlab.com glab repo create "$repo_full_name" "--${REPO_VISIBILITY}" 2>&1)"; then + log_warning "Failed to create GitLab repository ${repo_full_name}: ${create_error}" + return + fi + log_success "GitLab repository ${repo_full_name} created." + return + fi + + log_warning "Repo creation not implemented for provider '${REPO_PROVIDER}'. Skipping." +} + +main() { + init_git_repo + maybe_create_repo + configure_git_remote + run_template_init + + if [[ "$TASK_RUNNER" != "make" ]]; then remove_path "template/Makefile"; fi + if [[ "$TASK_RUNNER" != "task" ]]; then remove_path "template/Taskfile.yml"; fi + if [[ "$TASK_RUNNER" != "just" ]]; then remove_path "template/justfile"; fi + + if [[ "$CI_PROVIDER" != "github" ]]; then remove_path "template/.github"; fi + if [[ "$CI_PROVIDER" != "gitlab" ]]; then remove_path "template/.gitlab-ci.yml"; fi + + if ! is_true "$SEMANTIC_RELEASE"; then + remove_path "template/package.json" + remove_path "template/dependencies-init.txt" + remove_path "template/dependencies-dev-init.txt" + remove_path "template/.releaserc.json" + remove_path "template/CHANGELOG.md" + remove_path "template/.github/workflows/semantic-release.yaml" + remove_path "template/.github/workflows/semantic-pull-request.yaml" + fi + + if ! is_true "$SECRET_SCANNING"; then + remove_path "template/.github/workflows/secret-scan.yaml" + fi + + remove_path ".scaf/starter-post-copy.sh" + rmdir ".scaf" 2>/dev/null || true +} + +main "$@" diff --git a/template/copier.yml b/template/copier.yml index a19a821..236c7b5 100644 --- a/template/copier.yml +++ b/template/copier.yml @@ -2,7 +2,7 @@ _templates_suffix: "" _subdirectory: template _tasks: - - python tasks.py + - bash .scaf/post-copy.sh copier__project_name_raw: type: str diff --git a/template/template/.scaf/post-copy.sh.jinja b/template/template/.scaf/post-copy.sh.jinja new file mode 100644 index 0000000..d085c2b --- /dev/null +++ b/template/template/.scaf/post-copy.sh.jinja @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +set -euo pipefail + +CI_PROVIDER="{{ copier__ci_provider }}" +SEMANTIC_RELEASE="{{ 'true' if copier__enable_semantic_release else 'false' }}" +SECRET_SCANNING="{{ 'true' if copier__enable_secret_scanning else 'false' }}" +TASK_RUNNER="{{ copier__task_runner }}" +CONFIGURE_REPO="{{ 'true' if copier__configure_repo else 'false' }}" +REPO_PROVIDER="{{ copier__repo_provider }}" +REPO_ORG="{{ copier__repo_org }}" +REPO_NAME="{{ copier__repo_name }}" +REPO_URL="{{ copier__repo_url }}" +CREATE_REPO="{{ 'true' if copier__create_repo else 'false' }}" +REPO_VISIBILITY="{{ copier__repo_visibility }}" + +TERMINATOR="\033[0m" +WARNING="\033[1;33m [WARNING]: " +INFO="\033[1;33m [INFO]: " +SUCCESS="\033[1;32m [SUCCESS]: " + +is_true() { + [[ "${1:-false}" == "true" ]] +} + +log_info() { + printf "%b%s%b\n" "$INFO" "$1" "$TERMINATOR" +} + +log_warning() { + printf "%b%s%b\n" "$WARNING" "$1" "$TERMINATOR" +} + +log_success() { + printf "%b%s%b\n" "$SUCCESS" "$1" "$TERMINATOR" +} + +remove_path() { + local path="$1" + [[ -e "$path" ]] || return 0 + rm -rf "$path" +} + +run_init_script() { + bash ./scripts/init-dev.sh +} + +init_git_repo() { + if [[ -d .git ]]; then + return + fi + log_info "Initializing git repository..." + log_info "Current working directory: $(pwd)" + git -c init.defaultBranch=main init . --quiet + log_success "Git repository initialized." +} + +configure_git_remote() { + if ! is_true "$CONFIGURE_REPO"; then + return + fi + + local repo_url + repo_url="$(printf "%s" "$REPO_URL" | xargs)" + if [[ -z "$repo_url" ]]; then + log_warning "No repo_url provided. Skipping git remote configuration." + return + fi + + log_info "repo_url: $repo_url" + if git remote get-url origin >/dev/null 2>&1; then + local current_origin + current_origin="$(git remote get-url origin)" + log_info "Remote origin already configured (${current_origin}). Skipping add." + return + fi + + git remote add origin "$repo_url" + log_success "Remote origin=${repo_url} added." +} + +maybe_create_repo() { + if ! is_true "$CREATE_REPO" || ! is_true "$CONFIGURE_REPO"; then + return + fi + + if [[ -z "${REPO_ORG// }" || -z "${REPO_NAME// }" ]]; then + log_warning "Repo org/name not set. Skipping repo creation." + return + fi + + local repo_full_name="${REPO_ORG}/${REPO_NAME}" + + if [[ "$REPO_PROVIDER" == "github" ]]; then + if ! command -v gh >/dev/null 2>&1; then + log_warning "gh CLI is not installed. Skipping repo creation." + return + fi + if GH_HOST=github.com gh repo view "$repo_full_name" >/dev/null 2>&1; then + log_info "GitHub repository ${repo_full_name} already exists." + return + fi + log_info "Creating GitHub repository ${repo_full_name} (${REPO_VISIBILITY})..." + if ! create_error="$(GH_HOST=github.com gh repo create "$repo_full_name" "--${REPO_VISIBILITY}" 2>&1)"; then + log_warning "Failed to create GitHub repository ${repo_full_name}: ${create_error}" + return + fi + log_success "GitHub repository ${repo_full_name} created." + return + fi + + if [[ "$REPO_PROVIDER" == "gitlab" ]]; then + if ! command -v glab >/dev/null 2>&1; then + log_warning "glab CLI is not installed. Skipping repo creation." + return + fi + if GITLAB_HOST=gitlab.com glab repo view "$repo_full_name" >/dev/null 2>&1; then + log_info "GitLab repository ${repo_full_name} already exists." + return + fi + log_info "Creating GitLab repository ${repo_full_name} (${REPO_VISIBILITY})..." + if ! create_error="$(GITLAB_HOST=gitlab.com glab repo create "$repo_full_name" "--${REPO_VISIBILITY}" 2>&1)"; then + log_warning "Failed to create GitLab repository ${repo_full_name}: ${create_error}" + return + fi + log_success "GitLab repository ${repo_full_name} created." + return + fi + + log_warning "Repo creation not implemented for provider '${REPO_PROVIDER}'. Skipping." +} + +main() { + init_git_repo + maybe_create_repo + configure_git_remote + run_init_script + + if [[ "$TASK_RUNNER" != "make" ]]; then remove_path "Makefile"; fi + if [[ "$TASK_RUNNER" != "task" ]]; then remove_path "Taskfile.yml"; fi + if [[ "$TASK_RUNNER" != "just" ]]; then remove_path "justfile"; fi + + if [[ "$CI_PROVIDER" != "github" ]]; then remove_path ".github"; fi + if [[ "$CI_PROVIDER" != "gitlab" ]]; then remove_path ".gitlab-ci.yml"; fi + + if ! is_true "$SEMANTIC_RELEASE"; then + remove_path "package.json" + remove_path "dependencies-init.txt" + remove_path "dependencies-dev-init.txt" + remove_path ".releaserc.json" + remove_path "CHANGELOG.md" + remove_path ".github/workflows/semantic-release.yaml" + remove_path ".github/workflows/semantic-pull-request.yaml" + fi + + if ! is_true "$SECRET_SCANNING"; then + remove_path ".github/workflows/secret-scan.yaml" + fi + + remove_path ".scaf/post-copy.sh" + rmdir ".scaf" 2>/dev/null || true +} + +main "$@" diff --git a/template/template/scripts/init-dev.sh.jinja b/template/template/scripts/init-dev.sh.jinja index acd1452..2a8aaa0 100755 --- a/template/template/scripts/init-dev.sh.jinja +++ b/template/template/scripts/init-dev.sh.jinja @@ -6,15 +6,7 @@ if command -v pre-commit >/dev/null 2>&1 && git rev-parse --is-inside-work-tree fi {% if copier__enable_semantic_release %} -has_unrendered_jinja() { - grep -Eq '[{][{]|[{][%]' dependencies-init.txt dependencies-dev-init.txt -} - -if [[ "${SCAF_SKIP_INIT_DEV_NPM:-0}" == "1" ]]; then - echo "Skipping semantic-release dependency install for starter-stage render" -elif has_unrendered_jinja; then - echo "Skipping semantic-release dependency install for unresolved template files" -elif command -v npm >/dev/null 2>&1; then +if command -v npm >/dev/null 2>&1; then xargs npm install < dependencies-init.txt xargs npm install --save-dev < dependencies-dev-init.txt elif command -v docker >/dev/null 2>&1; then diff --git a/template/template/tasks.py.jinja b/template/template/tasks.py.jinja deleted file mode 100644 index 9566481..0000000 --- a/template/template/tasks.py.jinja +++ /dev/null @@ -1,227 +0,0 @@ -import os -import pathlib -import shlex -import shutil -import subprocess - -CI_PROVIDER = "{{ copier__ci_provider }}" -SEMANTIC_RELEASE = {{ "True" if copier__enable_semantic_release else "False" }} -SECRET_SCANNING = {{ "True" if copier__enable_secret_scanning else "False" }} -TASK_RUNNER = "{{ copier__task_runner }}" -CONFIGURE_REPO = {{ "True" if copier__configure_repo else "False" }} -REPO_PROVIDER = "{{ copier__repo_provider }}" -REPO_ORG = "{{ copier__repo_org }}" -REPO_NAME = "{{ copier__repo_name }}" -REPO_URL = "{{ copier__repo_url }}" -CREATE_REPO = {{ "True" if copier__create_repo else "False" }} -REPO_VISIBILITY = "{{ copier__repo_visibility }}" - -PROJECT_ROOT = pathlib.Path.cwd() -TEMPLATE_ROOT = pathlib.Path(__file__).resolve().parent -TERMINATOR = "\x1b[0m" -WARNING = "\x1b[1;33m [WARNING]: " -INFO = "\x1b[1;33m [INFO]: " -SUCCESS = "\x1b[1;32m [SUCCESS]: " - - -def remove(path: str) -> None: - p = TEMPLATE_ROOT / path - if p.exists(): - if p.is_file() or p.is_symlink(): - p.unlink() - else: - for child in sorted(p.rglob("*"), reverse=True): - if child.is_file() or child.is_symlink(): - child.unlink() - elif child.is_dir(): - child.rmdir() - p.rmdir() - - -def run( - cmd: list[str], *, cwd: pathlib.Path | None = None, env: dict[str, str] | None = None -) -> None: - subprocess.run(cmd, check=True, cwd=cwd, env=env) - - -def run_init_script() -> None: - env = os.environ.copy() - if env.get("SCAF_TEMPLATE_STARTER_STAGE") == "1": - env["SCAF_SKIP_INIT_DEV_NPM"] = "1" - run(["bash", "./scripts/init-dev.sh"], cwd=TEMPLATE_ROOT, env=env) - - -def init_git_repo() -> None: - if (PROJECT_ROOT / ".git").exists(): - return - print(INFO + "Initializing git repository..." + TERMINATOR) - print(INFO + f"Current working directory: {PROJECT_ROOT}" + TERMINATOR) - subprocess.run( - shlex.split("git -c init.defaultBranch=main init . --quiet"), - check=True, - cwd=PROJECT_ROOT, - ) - print(SUCCESS + "Git repository initialized." + TERMINATOR) - - -def configure_git_remote() -> None: - if not CONFIGURE_REPO: - return - repo_url = REPO_URL.strip() - if repo_url: - print(INFO + f"repo_url: {repo_url}" + TERMINATOR) - existing_origin = subprocess.run( - shlex.split("git remote get-url origin"), - capture_output=True, - text=True, - cwd=PROJECT_ROOT, - ) - if existing_origin.returncode == 0: - current_origin = existing_origin.stdout.strip() - print( - INFO - + f"Remote origin already configured ({current_origin}). Skipping add." - + TERMINATOR - ) - return - command = f"git remote add origin {repo_url}" - subprocess.run(shlex.split(command), check=True, cwd=PROJECT_ROOT) - print(SUCCESS + f"Remote origin={repo_url} added." + TERMINATOR) - else: - print( - WARNING - + "No repo_url provided. Skipping git remote configuration." - + TERMINATOR - ) - - -def maybe_create_repo() -> None: - if not CREATE_REPO: - return - - if not CONFIGURE_REPO: - return - if not REPO_ORG.strip() or not REPO_NAME.strip(): - print(WARNING + "Repo org/name not set. Skipping repo creation." + TERMINATOR) - return - repo_name = f"{REPO_ORG.strip()}/{REPO_NAME.strip()}" - - if REPO_PROVIDER == "github": - if not shutil.which("gh"): - print(WARNING + "gh CLI is not installed. Skipping repo creation." + TERMINATOR) - return - - gh_env = os.environ.copy() - gh_env["GH_HOST"] = "github.com" - - repo_exists = subprocess.run( - ["gh", "repo", "view", repo_name], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - env=gh_env, - ) - if repo_exists.returncode == 0: - print(INFO + f"GitHub repository {repo_name} already exists." + TERMINATOR) - return - - print( - INFO - + f"Creating GitHub repository {repo_name} ({REPO_VISIBILITY})..." - + TERMINATOR - ) - create_repo = subprocess.run( - ["gh", "repo", "create", repo_name, f"--{REPO_VISIBILITY}"], - capture_output=True, - text=True, - env=gh_env, - ) - if create_repo.returncode != 0: - error = create_repo.stderr.strip() or create_repo.stdout.strip() or "unknown error" - print( - WARNING - + f"Failed to create GitHub repository {repo_name}: {error}" - + TERMINATOR - ) - return - print(SUCCESS + f"GitHub repository {repo_name} created." + TERMINATOR) - return - - if REPO_PROVIDER == "gitlab": - if not shutil.which("glab"): - print(WARNING + "glab CLI is not installed. Skipping repo creation." + TERMINATOR) - return - - glab_env = os.environ.copy() - glab_env["GITLAB_HOST"] = "gitlab.com" - repo_exists = subprocess.run( - ["glab", "repo", "view", repo_name], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - env=glab_env, - ) - if repo_exists.returncode == 0: - print(INFO + f"GitLab repository {repo_name} already exists." + TERMINATOR) - return - - print( - INFO - + f"Creating GitLab repository {repo_name} ({REPO_VISIBILITY})..." - + TERMINATOR - ) - create_repo = subprocess.run( - ["glab", "repo", "create", repo_name, f"--{REPO_VISIBILITY}"], - capture_output=True, - text=True, - env=glab_env, - ) - if create_repo.returncode != 0: - error = create_repo.stderr.strip() or create_repo.stdout.strip() or "unknown error" - print( - WARNING - + f"Failed to create GitLab repository {repo_name}: {error}" - + TERMINATOR - ) - return - print(SUCCESS + f"GitLab repository {repo_name} created." + TERMINATOR) - return - - print( - WARNING - + f"Repo creation not implemented for provider '{REPO_PROVIDER}'. Skipping." - + TERMINATOR - ) - - -def main() -> None: - init_git_repo() - maybe_create_repo() - configure_git_remote() - run_init_script() - - if TASK_RUNNER != "make": - remove("Makefile") - if TASK_RUNNER != "task": - remove("Taskfile.yml") - if TASK_RUNNER != "just": - remove("justfile") - - if CI_PROVIDER != "github": - remove(".github") - if CI_PROVIDER != "gitlab": - remove(".gitlab-ci.yml") - - if not SEMANTIC_RELEASE: - remove("package.json") - remove("dependencies-init.txt") - remove("dependencies-dev-init.txt") - remove(".releaserc.json") - remove("CHANGELOG.md") - remove(".github/workflows/semantic-release.yaml") - remove(".github/workflows/semantic-pull-request.yaml") - - if not SECRET_SCANNING: - remove(".github/workflows/secret-scan.yaml") - - -if __name__ == "__main__": - main() From 88eb148c9432110d3e7fd145262db99da701c235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roch=C3=A9=20Compaan?= Date: Sat, 7 Mar 2026 15:27:26 +0200 Subject: [PATCH 07/10] chore(template): remove template sample name question --- copier.yml | 5 ----- template/copier.yml | 5 ----- 2 files changed, 10 deletions(-) diff --git a/copier.yml b/copier.yml index 3ecb524..2b7f840 100644 --- a/copier.yml +++ b/copier.yml @@ -138,8 +138,3 @@ copier__task_runner: - make - task - just - -copier__template_sample_name: - type: str - default: "sample" - help: "Sample project name used by template correctness CI." diff --git a/template/copier.yml b/template/copier.yml index 236c7b5..ffbd94c 100644 --- a/template/copier.yml +++ b/template/copier.yml @@ -138,8 +138,3 @@ copier__task_runner: - make - task - just - -copier__template_sample_name: - type: str - default: "sample" - help: "Sample project name used by template correctness CI." From c111f5d893a6a0204da1dcb1112504dbd1d141f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roch=C3=A9=20Compaan?= Date: Sat, 7 Mar 2026 15:29:17 +0200 Subject: [PATCH 08/10] fix(tasks): create initial commit after scaffold init --- template/.scaf/starter-post-copy.sh.jinja | 26 ++++++++++++++++++++++ template/template/.scaf/post-copy.sh.jinja | 26 ++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/template/.scaf/starter-post-copy.sh.jinja b/template/.scaf/starter-post-copy.sh.jinja index 68a91ea..370062d 100644 --- a/template/.scaf/starter-post-copy.sh.jinja +++ b/template/.scaf/starter-post-copy.sh.jinja @@ -12,6 +12,9 @@ REPO_NAME="{{ copier__repo_name }}" REPO_URL="{{ copier__repo_url }}" CREATE_REPO="{{ 'true' if copier__create_repo else 'false' }}" REPO_VISIBILITY="{{ copier__repo_visibility }}" +AUTHOR_NAME="{{ copier__author_name }}" +AUTHOR_EMAIL="{{ copier__email }}" +REPO_INITIALIZED="false" TERMINATOR="\033[0m" WARNING="\033[1;33m [WARNING]: " @@ -99,9 +102,31 @@ init_git_repo() { log_info "Initializing git repository..." log_info "Current working directory: $(pwd)" git -c init.defaultBranch=main init . --quiet + REPO_INITIALIZED="true" log_success "Git repository initialized." } +maybe_initial_commit() { + if ! is_true "$REPO_INITIALIZED"; then + return + fi + + if git rev-parse --verify HEAD >/dev/null 2>&1; then + return + fi + + git add -A + if [[ -z "$(git status --porcelain)" ]]; then + return + fi + + if ! git -c "user.name=${AUTHOR_NAME}" -c "user.email=${AUTHOR_EMAIL}" commit --no-verify -m "chore: initialize from template" >/dev/null 2>&1; then + log_warning "Initial commit failed. Configure git user.name and user.email, then commit manually." + return + fi + log_success "Initial commit created." +} + configure_git_remote() { if ! is_true "$CONFIGURE_REPO"; then return @@ -206,6 +231,7 @@ main() { remove_path ".scaf/starter-post-copy.sh" rmdir ".scaf" 2>/dev/null || true + maybe_initial_commit } main "$@" diff --git a/template/template/.scaf/post-copy.sh.jinja b/template/template/.scaf/post-copy.sh.jinja index d085c2b..d5f98e0 100644 --- a/template/template/.scaf/post-copy.sh.jinja +++ b/template/template/.scaf/post-copy.sh.jinja @@ -12,6 +12,9 @@ REPO_NAME="{{ copier__repo_name }}" REPO_URL="{{ copier__repo_url }}" CREATE_REPO="{{ 'true' if copier__create_repo else 'false' }}" REPO_VISIBILITY="{{ copier__repo_visibility }}" +AUTHOR_NAME="{{ copier__author_name }}" +AUTHOR_EMAIL="{{ copier__email }}" +REPO_INITIALIZED="false" TERMINATOR="\033[0m" WARNING="\033[1;33m [WARNING]: " @@ -51,9 +54,31 @@ init_git_repo() { log_info "Initializing git repository..." log_info "Current working directory: $(pwd)" git -c init.defaultBranch=main init . --quiet + REPO_INITIALIZED="true" log_success "Git repository initialized." } +maybe_initial_commit() { + if ! is_true "$REPO_INITIALIZED"; then + return + fi + + if git rev-parse --verify HEAD >/dev/null 2>&1; then + return + fi + + git add -A + if [[ -z "$(git status --porcelain)" ]]; then + return + fi + + if ! git -c "user.name=${AUTHOR_NAME}" -c "user.email=${AUTHOR_EMAIL}" commit --no-verify -m "chore: initialize from template" >/dev/null 2>&1; then + log_warning "Initial commit failed. Configure git user.name and user.email, then commit manually." + return + fi + log_success "Initial commit created." +} + configure_git_remote() { if ! is_true "$CONFIGURE_REPO"; then return @@ -158,6 +183,7 @@ main() { remove_path ".scaf/post-copy.sh" rmdir ".scaf" 2>/dev/null || true + maybe_initial_commit } main "$@" From 8efed12c58277c42306a412320aef5536b91cb55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roch=C3=A9=20Compaan?= Date: Sat, 7 Mar 2026 15:46:44 +0200 Subject: [PATCH 09/10] refactor(tasks): switch phase post-copy scripts to python --- Makefile | 4 +- copier.yml | 2 +- template/.scaf/starter-post-copy.py.jinja | 317 +++++++++++++++++++++ template/.scaf/starter-post-copy.sh.jinja | 237 --------------- template/copier.yml | 2 +- template/template/.scaf/post-copy.py.jinja | 220 ++++++++++++++ template/template/.scaf/post-copy.sh.jinja | 189 ------------ 7 files changed, 541 insertions(+), 430 deletions(-) create mode 100644 template/.scaf/starter-post-copy.py.jinja delete mode 100644 template/.scaf/starter-post-copy.sh.jinja create mode 100644 template/template/.scaf/post-copy.py.jinja delete mode 100644 template/template/.scaf/post-copy.sh.jinja diff --git a/Makefile b/Makefile index 70d0b95..6bfed8b 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ test-template-render-github: test -f "$$out_dir/.github/workflows/template-correctness.yaml"; \ test -f "$$out_dir/.copier-answers.yml"; \ test -f "$$out_dir/template/README.md"; \ - test -f "$$out_dir/template/.scaf/post-copy.sh"; \ + test -f "$$out_dir/template/.scaf/post-copy.py"; \ test -f "$$out_dir/template/Taskfile.yml"; \ test ! -f "$$out_dir/template/Makefile"; \ test ! -f "$$out_dir/template/justfile"; \ @@ -73,7 +73,7 @@ test-template-render-gitlab: test -f "$$out_dir/.github/workflows/template-correctness.yaml"; \ test -f "$$out_dir/.copier-answers.yml"; \ test -f "$$out_dir/template/README.md"; \ - test -f "$$out_dir/template/.scaf/post-copy.sh"; \ + test -f "$$out_dir/template/.scaf/post-copy.py"; \ test -f "$$out_dir/template/justfile"; \ test ! -f "$$out_dir/template/Makefile"; \ test ! -f "$$out_dir/template/Taskfile.yml"; \ diff --git a/copier.yml b/copier.yml index 2b7f840..23b0b86 100644 --- a/copier.yml +++ b/copier.yml @@ -2,7 +2,7 @@ _templates_suffix: ".jinja" _subdirectory: template _tasks: - - bash .scaf/starter-post-copy.sh + - python .scaf/starter-post-copy.py copier__project_name_raw: type: str diff --git a/template/.scaf/starter-post-copy.py.jinja b/template/.scaf/starter-post-copy.py.jinja new file mode 100644 index 0000000..e7096c7 --- /dev/null +++ b/template/.scaf/starter-post-copy.py.jinja @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +import os +import pathlib +import shutil +import subprocess +import tempfile + +CI_PROVIDER = "{{ copier__ci_provider }}" +SEMANTIC_RELEASE = {{ "True" if copier__enable_semantic_release else "False" }} +SECRET_SCANNING = {{ "True" if copier__enable_secret_scanning else "False" }} +TASK_RUNNER = "{{ copier__task_runner }}" +CONFIGURE_REPO = {{ "True" if copier__configure_repo else "False" }} +REPO_PROVIDER = "{{ copier__repo_provider }}" +REPO_ORG = "{{ copier__repo_org }}" +REPO_NAME = "{{ copier__repo_name }}" +REPO_URL = "{{ copier__repo_url }}" +CREATE_REPO = {{ "True" if copier__create_repo else "False" }} +REPO_VISIBILITY = "{{ copier__repo_visibility }}" +AUTHOR_NAME = "{{ copier__author_name }}" +AUTHOR_EMAIL = "{{ copier__email }}" + +PROJECT_ROOT = pathlib.Path.cwd() +TEMPLATE_ROOT = PROJECT_ROOT / "template" +TERMINATOR = "\x1b[0m" +WARNING = "\x1b[1;33m [WARNING]: " +INFO = "\x1b[1;33m [INFO]: " +SUCCESS = "\x1b[1;32m [SUCCESS]: " + + +def run( + cmd: list[str], *, cwd: pathlib.Path | None = None, capture_output: bool = False +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + cmd, + check=True, + cwd=cwd, + capture_output=capture_output, + text=True, + ) + + +def try_run(cmd: list[str], *, cwd: pathlib.Path | None = None) -> bool: + return subprocess.run(cmd, cwd=cwd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 + + +def remove(path: pathlib.Path) -> None: + if not path.exists(): + return + if path.is_file() or path.is_symlink(): + path.unlink() + else: + shutil.rmtree(path) + + +def read_packages(path: pathlib.Path) -> list[str]: + if not path.exists(): + return [] + return [line.strip() for line in path.read_text().splitlines() if line.strip() and not line.startswith("#")] + + +def has_unrendered_jinja(path: pathlib.Path) -> bool: + if not path.exists(): + return False + content = path.read_text() + return ("{" + "{") in content or ("{" + "%") in content + + +def init_git_repo() -> bool: + if (PROJECT_ROOT / ".git").exists(): + return False + print(INFO + "Initializing git repository..." + TERMINATOR) + print(INFO + f"Current working directory: {PROJECT_ROOT}" + TERMINATOR) + run(["git", "-c", "init.defaultBranch=main", "init", ".", "--quiet"], cwd=PROJECT_ROOT) + print(SUCCESS + "Git repository initialized." + TERMINATOR) + return True + + +def maybe_create_repo() -> None: + if not CREATE_REPO or not CONFIGURE_REPO: + return + if not REPO_ORG.strip() or not REPO_NAME.strip(): + print(WARNING + "Repo org/name not set. Skipping repo creation." + TERMINATOR) + return + repo_name = f"{REPO_ORG.strip()}/{REPO_NAME.strip()}" + + if REPO_PROVIDER == "github": + if not shutil.which("gh"): + print(WARNING + "gh CLI is not installed. Skipping repo creation." + TERMINATOR) + return + if try_run(["gh", "repo", "view", repo_name], cwd=PROJECT_ROOT): + print(INFO + f"GitHub repository {repo_name} already exists." + TERMINATOR) + return + print(INFO + f"Creating GitHub repository {repo_name} ({REPO_VISIBILITY})..." + TERMINATOR) + created = subprocess.run( + ["gh", "repo", "create", repo_name, f"--{REPO_VISIBILITY}"], + capture_output=True, + text=True, + ) + if created.returncode != 0: + error = created.stderr.strip() or created.stdout.strip() or "unknown error" + print(WARNING + f"Failed to create GitHub repository {repo_name}: {error}" + TERMINATOR) + return + print(SUCCESS + f"GitHub repository {repo_name} created." + TERMINATOR) + return + + if REPO_PROVIDER == "gitlab": + if not shutil.which("glab"): + print(WARNING + "glab CLI is not installed. Skipping repo creation." + TERMINATOR) + return + if try_run(["glab", "repo", "view", repo_name], cwd=PROJECT_ROOT): + print(INFO + f"GitLab repository {repo_name} already exists." + TERMINATOR) + return + print(INFO + f"Creating GitLab repository {repo_name} ({REPO_VISIBILITY})..." + TERMINATOR) + created = subprocess.run( + ["glab", "repo", "create", repo_name, f"--{REPO_VISIBILITY}"], + capture_output=True, + text=True, + ) + if created.returncode != 0: + error = created.stderr.strip() or created.stdout.strip() or "unknown error" + print(WARNING + f"Failed to create GitLab repository {repo_name}: {error}" + TERMINATOR) + return + print(SUCCESS + f"GitLab repository {repo_name} created." + TERMINATOR) + return + + print( + WARNING + + f"Repo creation not implemented for provider '{REPO_PROVIDER}'. Skipping." + + TERMINATOR + ) + + +def configure_git_remote() -> None: + if not CONFIGURE_REPO: + return + + repo_url = REPO_URL.strip() + if not repo_url: + print(WARNING + "No repo_url provided. Skipping git remote configuration." + TERMINATOR) + return + + print(INFO + f"repo_url: {repo_url}" + TERMINATOR) + if try_run(["git", "remote", "get-url", "origin"], cwd=PROJECT_ROOT): + current_origin = run(["git", "remote", "get-url", "origin"], cwd=PROJECT_ROOT, capture_output=True) + print( + INFO + + f"Remote origin already configured ({current_origin.stdout.strip()}). Skipping add." + + TERMINATOR + ) + return + + run(["git", "remote", "add", "origin", repo_url], cwd=PROJECT_ROOT) + print(SUCCESS + f"Remote origin={repo_url} added." + TERMINATOR) + + +def install_semantic_release_deps(init_file: pathlib.Path, dev_file: pathlib.Path) -> None: + init_packages = read_packages(init_file) + dev_packages = read_packages(dev_file) + + if shutil.which("npm"): + if init_packages: + run(["npm", "install", *init_packages], cwd=TEMPLATE_ROOT) + if dev_packages: + run(["npm", "install", "--save-dev", *dev_packages], cwd=TEMPLATE_ROOT) + return + + if shutil.which("docker"): + run( + [ + "docker", + "run", + "--rm", + "--user", + f"{os.getuid()}:{os.getgid()}", + "-w", + "/app", + "-v", + f"{TEMPLATE_ROOT}:/app", + "-e", + "npm_config_cache=/tmp/.npm", + "node:lts", + "/bin/bash", + "-lc", + ( + f"if [ -s {init_file.name!r} ]; then xargs npm install < {init_file.name!r}; fi; " + f"if [ -s {dev_file.name!r} ]; then xargs npm install --save-dev < {dev_file.name!r}; fi" + ), + ], + cwd=TEMPLATE_ROOT, + ) + return + + print(WARNING + "docker and npm not found; skipping semantic-release dependency install" + TERMINATOR) + + +def run_template_init() -> None: + if shutil.which("pre-commit") and try_run(["git", "rev-parse", "--is-inside-work-tree"], cwd=TEMPLATE_ROOT): + try: + run(["pre-commit", "install"], cwd=TEMPLATE_ROOT) + except subprocess.CalledProcessError: + print("pre-commit install failed; continuing") + + if SEMANTIC_RELEASE: + init_deps = TEMPLATE_ROOT / "dependencies-init.txt" + dev_deps = TEMPLATE_ROOT / "dependencies-dev-init.txt" + + if has_unrendered_jinja(init_deps) or has_unrendered_jinja(dev_deps): + with tempfile.NamedTemporaryFile( + mode="w", dir=TEMPLATE_ROOT, prefix=".scaf-deps-init.", delete=False + ) as init_tmp: + for line in read_packages(init_deps): + if ("{" + "{") in line or ("{" + "%") in line: + continue + init_tmp.write(f"{line}\n") + init_tmp_path = pathlib.Path(init_tmp.name) + + with tempfile.NamedTemporaryFile( + mode="w", dir=TEMPLATE_ROOT, prefix=".scaf-deps-dev.", delete=False + ) as dev_tmp: + for line in read_packages(dev_deps): + if ("{" + "{") in line or ("{" + "%") in line: + continue + if CI_PROVIDER == "github" and line == "@semantic-release/gitlab": + continue + if CI_PROVIDER == "gitlab" and line == "@semantic-release/github": + continue + dev_tmp.write(f"{line}\n") + dev_tmp_path = pathlib.Path(dev_tmp.name) + + try: + install_semantic_release_deps(init_tmp_path, dev_tmp_path) + finally: + remove(init_tmp_path) + remove(dev_tmp_path) + else: + install_semantic_release_deps(init_deps, dev_deps) + + print("Local development setup complete.") + + +def prune_starter_template() -> None: + if TASK_RUNNER != "make": + remove(TEMPLATE_ROOT / "Makefile") + if TASK_RUNNER != "task": + remove(TEMPLATE_ROOT / "Taskfile.yml") + if TASK_RUNNER != "just": + remove(TEMPLATE_ROOT / "justfile") + + if CI_PROVIDER != "github": + remove(TEMPLATE_ROOT / ".github") + if CI_PROVIDER != "gitlab": + remove(TEMPLATE_ROOT / ".gitlab-ci.yml") + + if not SEMANTIC_RELEASE: + remove(TEMPLATE_ROOT / "package.json") + remove(TEMPLATE_ROOT / "dependencies-init.txt") + remove(TEMPLATE_ROOT / "dependencies-dev-init.txt") + remove(TEMPLATE_ROOT / ".releaserc.json") + remove(TEMPLATE_ROOT / "CHANGELOG.md") + remove(TEMPLATE_ROOT / ".github/workflows/semantic-release.yaml") + remove(TEMPLATE_ROOT / ".github/workflows/semantic-pull-request.yaml") + + if not SECRET_SCANNING: + remove(TEMPLATE_ROOT / ".github/workflows/secret-scan.yaml") + + +def maybe_initial_commit(initialized_repo: bool) -> None: + if not initialized_repo: + return + if try_run(["git", "rev-parse", "--verify", "HEAD"], cwd=PROJECT_ROOT): + return + + run(["git", "add", "-A"], cwd=PROJECT_ROOT) + status = run(["git", "status", "--porcelain"], cwd=PROJECT_ROOT, capture_output=True) + if not status.stdout.strip(): + return + + commit_result = subprocess.run( + [ + "git", + "-c", + f"user.name={AUTHOR_NAME}", + "-c", + f"user.email={AUTHOR_EMAIL}", + "commit", + "--no-verify", + "-m", + "chore: initialize from template", + ], + cwd=PROJECT_ROOT, + ) + if commit_result.returncode != 0: + print( + WARNING + + "Initial commit failed. Configure git user.name and user.email, then commit manually." + + TERMINATOR + ) + return + print(SUCCESS + "Initial commit created." + TERMINATOR) + + +def main() -> None: + initialized_repo = init_git_repo() + maybe_create_repo() + configure_git_remote() + run_template_init() + prune_starter_template() + remove(PROJECT_ROOT / ".scaf/starter-post-copy.py") + try: + (PROJECT_ROOT / ".scaf").rmdir() + except OSError: + pass + maybe_initial_commit(initialized_repo) + + +if __name__ == "__main__": + main() diff --git a/template/.scaf/starter-post-copy.sh.jinja b/template/.scaf/starter-post-copy.sh.jinja deleted file mode 100644 index 370062d..0000000 --- a/template/.scaf/starter-post-copy.sh.jinja +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -CI_PROVIDER="{{ copier__ci_provider }}" -SEMANTIC_RELEASE="{{ 'true' if copier__enable_semantic_release else 'false' }}" -SECRET_SCANNING="{{ 'true' if copier__enable_secret_scanning else 'false' }}" -TASK_RUNNER="{{ copier__task_runner }}" -CONFIGURE_REPO="{{ 'true' if copier__configure_repo else 'false' }}" -REPO_PROVIDER="{{ copier__repo_provider }}" -REPO_ORG="{{ copier__repo_org }}" -REPO_NAME="{{ copier__repo_name }}" -REPO_URL="{{ copier__repo_url }}" -CREATE_REPO="{{ 'true' if copier__create_repo else 'false' }}" -REPO_VISIBILITY="{{ copier__repo_visibility }}" -AUTHOR_NAME="{{ copier__author_name }}" -AUTHOR_EMAIL="{{ copier__email }}" -REPO_INITIALIZED="false" - -TERMINATOR="\033[0m" -WARNING="\033[1;33m [WARNING]: " -INFO="\033[1;33m [INFO]: " -SUCCESS="\033[1;32m [SUCCESS]: " - -is_true() { - [[ "${1:-false}" == "true" ]] -} - -log_info() { - printf "%b%s%b\n" "$INFO" "$1" "$TERMINATOR" -} - -log_warning() { - printf "%b%s%b\n" "$WARNING" "$1" "$TERMINATOR" -} - -log_success() { - printf "%b%s%b\n" "$SUCCESS" "$1" "$TERMINATOR" -} - -remove_path() { - local path="$1" - [[ -e "$path" ]] || return 0 - rm -rf "$path" -} - -install_semantic_release_deps() { - local init_file="$1" - local dev_file="$2" - - if command -v npm >/dev/null 2>&1; then - [[ -s "$init_file" ]] && xargs npm install < "$init_file" - [[ -s "$dev_file" ]] && xargs npm install --save-dev < "$dev_file" - return - fi - - if command -v docker >/dev/null 2>&1; then - docker run --rm --user "$(id -u):$(id -g)" -w "/app" -v "$(pwd):/app" -e npm_config_cache=/tmp/.npm node:lts /bin/bash -c \ - "if [ -s \"$init_file\" ]; then xargs npm install < \"$init_file\"; fi; if [ -s \"$dev_file\" ]; then xargs npm install --save-dev < \"$dev_file\"; fi" - return - fi - - log_warning "docker and npm not found; skipping semantic-release dependency install" -} - -run_template_init() { - ( - cd template - - if command -v pre-commit >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - pre-commit install || echo "pre-commit install failed; continuing" - fi - - if is_true "$SEMANTIC_RELEASE"; then - local tmp_init_file tmp_dev_file tmp_dev_filtered - tmp_init_file="$(mktemp ./.scaf-deps-init.XXXXXX)" - tmp_dev_file="$(mktemp ./.scaf-deps-dev.XXXXXX)" - tmp_dev_filtered="${tmp_dev_file}.filtered" - - sed -E '/[{][{]/d; /[{][%]/d; /^[[:space:]]*$/d' dependencies-init.txt > "$tmp_init_file" - sed -E '/[{][{]/d; /[{][%]/d; /^[[:space:]]*$/d' dependencies-dev-init.txt > "$tmp_dev_file" - - if [[ "$CI_PROVIDER" == "github" ]]; then - awk '!/@semantic-release\/gitlab/' "$tmp_dev_file" > "$tmp_dev_filtered" - mv "$tmp_dev_filtered" "$tmp_dev_file" - elif [[ "$CI_PROVIDER" == "gitlab" ]]; then - awk '!/@semantic-release\/github/' "$tmp_dev_file" > "$tmp_dev_filtered" - mv "$tmp_dev_filtered" "$tmp_dev_file" - fi - - install_semantic_release_deps "$tmp_init_file" "$tmp_dev_file" - rm -f "$tmp_init_file" "$tmp_dev_file" "$tmp_dev_filtered" - fi - - echo "Local development setup complete." - ) -} - -init_git_repo() { - if [[ -d .git ]]; then - return - fi - log_info "Initializing git repository..." - log_info "Current working directory: $(pwd)" - git -c init.defaultBranch=main init . --quiet - REPO_INITIALIZED="true" - log_success "Git repository initialized." -} - -maybe_initial_commit() { - if ! is_true "$REPO_INITIALIZED"; then - return - fi - - if git rev-parse --verify HEAD >/dev/null 2>&1; then - return - fi - - git add -A - if [[ -z "$(git status --porcelain)" ]]; then - return - fi - - if ! git -c "user.name=${AUTHOR_NAME}" -c "user.email=${AUTHOR_EMAIL}" commit --no-verify -m "chore: initialize from template" >/dev/null 2>&1; then - log_warning "Initial commit failed. Configure git user.name and user.email, then commit manually." - return - fi - log_success "Initial commit created." -} - -configure_git_remote() { - if ! is_true "$CONFIGURE_REPO"; then - return - fi - - local repo_url - repo_url="$(printf "%s" "$REPO_URL" | xargs)" - if [[ -z "$repo_url" ]]; then - log_warning "No repo_url provided. Skipping git remote configuration." - return - fi - - log_info "repo_url: $repo_url" - if git remote get-url origin >/dev/null 2>&1; then - local current_origin - current_origin="$(git remote get-url origin)" - log_info "Remote origin already configured (${current_origin}). Skipping add." - return - fi - - git remote add origin "$repo_url" - log_success "Remote origin=${repo_url} added." -} - -maybe_create_repo() { - if ! is_true "$CREATE_REPO" || ! is_true "$CONFIGURE_REPO"; then - return - fi - - if [[ -z "${REPO_ORG// }" || -z "${REPO_NAME// }" ]]; then - log_warning "Repo org/name not set. Skipping repo creation." - return - fi - - local repo_full_name="${REPO_ORG}/${REPO_NAME}" - - if [[ "$REPO_PROVIDER" == "github" ]]; then - if ! command -v gh >/dev/null 2>&1; then - log_warning "gh CLI is not installed. Skipping repo creation." - return - fi - if GH_HOST=github.com gh repo view "$repo_full_name" >/dev/null 2>&1; then - log_info "GitHub repository ${repo_full_name} already exists." - return - fi - log_info "Creating GitHub repository ${repo_full_name} (${REPO_VISIBILITY})..." - if ! create_error="$(GH_HOST=github.com gh repo create "$repo_full_name" "--${REPO_VISIBILITY}" 2>&1)"; then - log_warning "Failed to create GitHub repository ${repo_full_name}: ${create_error}" - return - fi - log_success "GitHub repository ${repo_full_name} created." - return - fi - - if [[ "$REPO_PROVIDER" == "gitlab" ]]; then - if ! command -v glab >/dev/null 2>&1; then - log_warning "glab CLI is not installed. Skipping repo creation." - return - fi - if GITLAB_HOST=gitlab.com glab repo view "$repo_full_name" >/dev/null 2>&1; then - log_info "GitLab repository ${repo_full_name} already exists." - return - fi - log_info "Creating GitLab repository ${repo_full_name} (${REPO_VISIBILITY})..." - if ! create_error="$(GITLAB_HOST=gitlab.com glab repo create "$repo_full_name" "--${REPO_VISIBILITY}" 2>&1)"; then - log_warning "Failed to create GitLab repository ${repo_full_name}: ${create_error}" - return - fi - log_success "GitLab repository ${repo_full_name} created." - return - fi - - log_warning "Repo creation not implemented for provider '${REPO_PROVIDER}'. Skipping." -} - -main() { - init_git_repo - maybe_create_repo - configure_git_remote - run_template_init - - if [[ "$TASK_RUNNER" != "make" ]]; then remove_path "template/Makefile"; fi - if [[ "$TASK_RUNNER" != "task" ]]; then remove_path "template/Taskfile.yml"; fi - if [[ "$TASK_RUNNER" != "just" ]]; then remove_path "template/justfile"; fi - - if [[ "$CI_PROVIDER" != "github" ]]; then remove_path "template/.github"; fi - if [[ "$CI_PROVIDER" != "gitlab" ]]; then remove_path "template/.gitlab-ci.yml"; fi - - if ! is_true "$SEMANTIC_RELEASE"; then - remove_path "template/package.json" - remove_path "template/dependencies-init.txt" - remove_path "template/dependencies-dev-init.txt" - remove_path "template/.releaserc.json" - remove_path "template/CHANGELOG.md" - remove_path "template/.github/workflows/semantic-release.yaml" - remove_path "template/.github/workflows/semantic-pull-request.yaml" - fi - - if ! is_true "$SECRET_SCANNING"; then - remove_path "template/.github/workflows/secret-scan.yaml" - fi - - remove_path ".scaf/starter-post-copy.sh" - rmdir ".scaf" 2>/dev/null || true - maybe_initial_commit -} - -main "$@" diff --git a/template/copier.yml b/template/copier.yml index ffbd94c..99f6476 100644 --- a/template/copier.yml +++ b/template/copier.yml @@ -2,7 +2,7 @@ _templates_suffix: "" _subdirectory: template _tasks: - - bash .scaf/post-copy.sh + - python .scaf/post-copy.py copier__project_name_raw: type: str diff --git a/template/template/.scaf/post-copy.py.jinja b/template/template/.scaf/post-copy.py.jinja new file mode 100644 index 0000000..be3aa56 --- /dev/null +++ b/template/template/.scaf/post-copy.py.jinja @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +import pathlib +import shutil +import subprocess + +CI_PROVIDER = "{{ copier__ci_provider }}" +SEMANTIC_RELEASE = {{ "True" if copier__enable_semantic_release else "False" }} +SECRET_SCANNING = {{ "True" if copier__enable_secret_scanning else "False" }} +TASK_RUNNER = "{{ copier__task_runner }}" +CONFIGURE_REPO = {{ "True" if copier__configure_repo else "False" }} +REPO_PROVIDER = "{{ copier__repo_provider }}" +REPO_ORG = "{{ copier__repo_org }}" +REPO_NAME = "{{ copier__repo_name }}" +REPO_URL = "{{ copier__repo_url }}" +CREATE_REPO = {{ "True" if copier__create_repo else "False" }} +REPO_VISIBILITY = "{{ copier__repo_visibility }}" +AUTHOR_NAME = "{{ copier__author_name }}" +AUTHOR_EMAIL = "{{ copier__email }}" + +PROJECT_ROOT = pathlib.Path.cwd() +TERMINATOR = "\x1b[0m" +WARNING = "\x1b[1;33m [WARNING]: " +INFO = "\x1b[1;33m [INFO]: " +SUCCESS = "\x1b[1;32m [SUCCESS]: " + + +def run( + cmd: list[str], *, cwd: pathlib.Path | None = None, capture_output: bool = False +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + cmd, + check=True, + cwd=cwd, + capture_output=capture_output, + text=True, + ) + + +def try_run(cmd: list[str], *, cwd: pathlib.Path | None = None) -> bool: + return subprocess.run(cmd, cwd=cwd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 + + +def remove(path: pathlib.Path) -> None: + if not path.exists(): + return + if path.is_file() or path.is_symlink(): + path.unlink() + else: + shutil.rmtree(path) + + +def init_git_repo() -> bool: + if (PROJECT_ROOT / ".git").exists(): + return False + print(INFO + "Initializing git repository..." + TERMINATOR) + print(INFO + f"Current working directory: {PROJECT_ROOT}" + TERMINATOR) + run(["git", "-c", "init.defaultBranch=main", "init", ".", "--quiet"], cwd=PROJECT_ROOT) + print(SUCCESS + "Git repository initialized." + TERMINATOR) + return True + + +def maybe_create_repo() -> None: + if not CREATE_REPO or not CONFIGURE_REPO: + return + if not REPO_ORG.strip() or not REPO_NAME.strip(): + print(WARNING + "Repo org/name not set. Skipping repo creation." + TERMINATOR) + return + repo_name = f"{REPO_ORG.strip()}/{REPO_NAME.strip()}" + + if REPO_PROVIDER == "github": + if not shutil.which("gh"): + print(WARNING + "gh CLI is not installed. Skipping repo creation." + TERMINATOR) + return + if try_run(["gh", "repo", "view", repo_name], cwd=PROJECT_ROOT): + print(INFO + f"GitHub repository {repo_name} already exists." + TERMINATOR) + return + print(INFO + f"Creating GitHub repository {repo_name} ({REPO_VISIBILITY})..." + TERMINATOR) + created = subprocess.run( + ["gh", "repo", "create", repo_name, f"--{REPO_VISIBILITY}"], + capture_output=True, + text=True, + ) + if created.returncode != 0: + error = created.stderr.strip() or created.stdout.strip() or "unknown error" + print(WARNING + f"Failed to create GitHub repository {repo_name}: {error}" + TERMINATOR) + return + print(SUCCESS + f"GitHub repository {repo_name} created." + TERMINATOR) + return + + if REPO_PROVIDER == "gitlab": + if not shutil.which("glab"): + print(WARNING + "glab CLI is not installed. Skipping repo creation." + TERMINATOR) + return + if try_run(["glab", "repo", "view", repo_name], cwd=PROJECT_ROOT): + print(INFO + f"GitLab repository {repo_name} already exists." + TERMINATOR) + return + print(INFO + f"Creating GitLab repository {repo_name} ({REPO_VISIBILITY})..." + TERMINATOR) + created = subprocess.run( + ["glab", "repo", "create", repo_name, f"--{REPO_VISIBILITY}"], + capture_output=True, + text=True, + ) + if created.returncode != 0: + error = created.stderr.strip() or created.stdout.strip() or "unknown error" + print(WARNING + f"Failed to create GitLab repository {repo_name}: {error}" + TERMINATOR) + return + print(SUCCESS + f"GitLab repository {repo_name} created." + TERMINATOR) + return + + print( + WARNING + + f"Repo creation not implemented for provider '{REPO_PROVIDER}'. Skipping." + + TERMINATOR + ) + + +def configure_git_remote() -> None: + if not CONFIGURE_REPO: + return + + repo_url = REPO_URL.strip() + if not repo_url: + print(WARNING + "No repo_url provided. Skipping git remote configuration." + TERMINATOR) + return + + print(INFO + f"repo_url: {repo_url}" + TERMINATOR) + if try_run(["git", "remote", "get-url", "origin"], cwd=PROJECT_ROOT): + current_origin = run(["git", "remote", "get-url", "origin"], cwd=PROJECT_ROOT, capture_output=True) + print( + INFO + + f"Remote origin already configured ({current_origin.stdout.strip()}). Skipping add." + + TERMINATOR + ) + return + + run(["git", "remote", "add", "origin", repo_url], cwd=PROJECT_ROOT) + print(SUCCESS + f"Remote origin={repo_url} added." + TERMINATOR) + + +def run_init_script() -> None: + run(["bash", "./scripts/init-dev.sh"], cwd=PROJECT_ROOT) + + +def prune_files() -> None: + if TASK_RUNNER != "make": + remove(PROJECT_ROOT / "Makefile") + if TASK_RUNNER != "task": + remove(PROJECT_ROOT / "Taskfile.yml") + if TASK_RUNNER != "just": + remove(PROJECT_ROOT / "justfile") + + if CI_PROVIDER != "github": + remove(PROJECT_ROOT / ".github") + if CI_PROVIDER != "gitlab": + remove(PROJECT_ROOT / ".gitlab-ci.yml") + + if not SEMANTIC_RELEASE: + remove(PROJECT_ROOT / "package.json") + remove(PROJECT_ROOT / "dependencies-init.txt") + remove(PROJECT_ROOT / "dependencies-dev-init.txt") + remove(PROJECT_ROOT / ".releaserc.json") + remove(PROJECT_ROOT / "CHANGELOG.md") + remove(PROJECT_ROOT / ".github/workflows/semantic-release.yaml") + remove(PROJECT_ROOT / ".github/workflows/semantic-pull-request.yaml") + + if not SECRET_SCANNING: + remove(PROJECT_ROOT / ".github/workflows/secret-scan.yaml") + + +def maybe_initial_commit(initialized_repo: bool) -> None: + if not initialized_repo: + return + if try_run(["git", "rev-parse", "--verify", "HEAD"], cwd=PROJECT_ROOT): + return + + run(["git", "add", "-A"], cwd=PROJECT_ROOT) + status = run(["git", "status", "--porcelain"], cwd=PROJECT_ROOT, capture_output=True) + if not status.stdout.strip(): + return + + commit_result = subprocess.run( + [ + "git", + "-c", + f"user.name={AUTHOR_NAME}", + "-c", + f"user.email={AUTHOR_EMAIL}", + "commit", + "--no-verify", + "-m", + "chore: initialize from template", + ], + cwd=PROJECT_ROOT, + ) + if commit_result.returncode != 0: + print( + WARNING + + "Initial commit failed. Configure git user.name and user.email, then commit manually." + + TERMINATOR + ) + return + print(SUCCESS + "Initial commit created." + TERMINATOR) + + +def main() -> None: + initialized_repo = init_git_repo() + maybe_create_repo() + configure_git_remote() + run_init_script() + prune_files() + remove(PROJECT_ROOT / ".scaf/post-copy.py") + try: + (PROJECT_ROOT / ".scaf").rmdir() + except OSError: + pass + maybe_initial_commit(initialized_repo) + + +if __name__ == "__main__": + main() diff --git a/template/template/.scaf/post-copy.sh.jinja b/template/template/.scaf/post-copy.sh.jinja deleted file mode 100644 index d5f98e0..0000000 --- a/template/template/.scaf/post-copy.sh.jinja +++ /dev/null @@ -1,189 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -CI_PROVIDER="{{ copier__ci_provider }}" -SEMANTIC_RELEASE="{{ 'true' if copier__enable_semantic_release else 'false' }}" -SECRET_SCANNING="{{ 'true' if copier__enable_secret_scanning else 'false' }}" -TASK_RUNNER="{{ copier__task_runner }}" -CONFIGURE_REPO="{{ 'true' if copier__configure_repo else 'false' }}" -REPO_PROVIDER="{{ copier__repo_provider }}" -REPO_ORG="{{ copier__repo_org }}" -REPO_NAME="{{ copier__repo_name }}" -REPO_URL="{{ copier__repo_url }}" -CREATE_REPO="{{ 'true' if copier__create_repo else 'false' }}" -REPO_VISIBILITY="{{ copier__repo_visibility }}" -AUTHOR_NAME="{{ copier__author_name }}" -AUTHOR_EMAIL="{{ copier__email }}" -REPO_INITIALIZED="false" - -TERMINATOR="\033[0m" -WARNING="\033[1;33m [WARNING]: " -INFO="\033[1;33m [INFO]: " -SUCCESS="\033[1;32m [SUCCESS]: " - -is_true() { - [[ "${1:-false}" == "true" ]] -} - -log_info() { - printf "%b%s%b\n" "$INFO" "$1" "$TERMINATOR" -} - -log_warning() { - printf "%b%s%b\n" "$WARNING" "$1" "$TERMINATOR" -} - -log_success() { - printf "%b%s%b\n" "$SUCCESS" "$1" "$TERMINATOR" -} - -remove_path() { - local path="$1" - [[ -e "$path" ]] || return 0 - rm -rf "$path" -} - -run_init_script() { - bash ./scripts/init-dev.sh -} - -init_git_repo() { - if [[ -d .git ]]; then - return - fi - log_info "Initializing git repository..." - log_info "Current working directory: $(pwd)" - git -c init.defaultBranch=main init . --quiet - REPO_INITIALIZED="true" - log_success "Git repository initialized." -} - -maybe_initial_commit() { - if ! is_true "$REPO_INITIALIZED"; then - return - fi - - if git rev-parse --verify HEAD >/dev/null 2>&1; then - return - fi - - git add -A - if [[ -z "$(git status --porcelain)" ]]; then - return - fi - - if ! git -c "user.name=${AUTHOR_NAME}" -c "user.email=${AUTHOR_EMAIL}" commit --no-verify -m "chore: initialize from template" >/dev/null 2>&1; then - log_warning "Initial commit failed. Configure git user.name and user.email, then commit manually." - return - fi - log_success "Initial commit created." -} - -configure_git_remote() { - if ! is_true "$CONFIGURE_REPO"; then - return - fi - - local repo_url - repo_url="$(printf "%s" "$REPO_URL" | xargs)" - if [[ -z "$repo_url" ]]; then - log_warning "No repo_url provided. Skipping git remote configuration." - return - fi - - log_info "repo_url: $repo_url" - if git remote get-url origin >/dev/null 2>&1; then - local current_origin - current_origin="$(git remote get-url origin)" - log_info "Remote origin already configured (${current_origin}). Skipping add." - return - fi - - git remote add origin "$repo_url" - log_success "Remote origin=${repo_url} added." -} - -maybe_create_repo() { - if ! is_true "$CREATE_REPO" || ! is_true "$CONFIGURE_REPO"; then - return - fi - - if [[ -z "${REPO_ORG// }" || -z "${REPO_NAME// }" ]]; then - log_warning "Repo org/name not set. Skipping repo creation." - return - fi - - local repo_full_name="${REPO_ORG}/${REPO_NAME}" - - if [[ "$REPO_PROVIDER" == "github" ]]; then - if ! command -v gh >/dev/null 2>&1; then - log_warning "gh CLI is not installed. Skipping repo creation." - return - fi - if GH_HOST=github.com gh repo view "$repo_full_name" >/dev/null 2>&1; then - log_info "GitHub repository ${repo_full_name} already exists." - return - fi - log_info "Creating GitHub repository ${repo_full_name} (${REPO_VISIBILITY})..." - if ! create_error="$(GH_HOST=github.com gh repo create "$repo_full_name" "--${REPO_VISIBILITY}" 2>&1)"; then - log_warning "Failed to create GitHub repository ${repo_full_name}: ${create_error}" - return - fi - log_success "GitHub repository ${repo_full_name} created." - return - fi - - if [[ "$REPO_PROVIDER" == "gitlab" ]]; then - if ! command -v glab >/dev/null 2>&1; then - log_warning "glab CLI is not installed. Skipping repo creation." - return - fi - if GITLAB_HOST=gitlab.com glab repo view "$repo_full_name" >/dev/null 2>&1; then - log_info "GitLab repository ${repo_full_name} already exists." - return - fi - log_info "Creating GitLab repository ${repo_full_name} (${REPO_VISIBILITY})..." - if ! create_error="$(GITLAB_HOST=gitlab.com glab repo create "$repo_full_name" "--${REPO_VISIBILITY}" 2>&1)"; then - log_warning "Failed to create GitLab repository ${repo_full_name}: ${create_error}" - return - fi - log_success "GitLab repository ${repo_full_name} created." - return - fi - - log_warning "Repo creation not implemented for provider '${REPO_PROVIDER}'. Skipping." -} - -main() { - init_git_repo - maybe_create_repo - configure_git_remote - run_init_script - - if [[ "$TASK_RUNNER" != "make" ]]; then remove_path "Makefile"; fi - if [[ "$TASK_RUNNER" != "task" ]]; then remove_path "Taskfile.yml"; fi - if [[ "$TASK_RUNNER" != "just" ]]; then remove_path "justfile"; fi - - if [[ "$CI_PROVIDER" != "github" ]]; then remove_path ".github"; fi - if [[ "$CI_PROVIDER" != "gitlab" ]]; then remove_path ".gitlab-ci.yml"; fi - - if ! is_true "$SEMANTIC_RELEASE"; then - remove_path "package.json" - remove_path "dependencies-init.txt" - remove_path "dependencies-dev-init.txt" - remove_path ".releaserc.json" - remove_path "CHANGELOG.md" - remove_path ".github/workflows/semantic-release.yaml" - remove_path ".github/workflows/semantic-pull-request.yaml" - fi - - if ! is_true "$SECRET_SCANNING"; then - remove_path ".github/workflows/secret-scan.yaml" - fi - - remove_path ".scaf/post-copy.sh" - rmdir ".scaf" 2>/dev/null || true - maybe_initial_commit -} - -main "$@" From 1ee5d5616bde69bb6d5ed1fd18be16564949e8f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roch=C3=A9=20Compaan?= Date: Sat, 7 Mar 2026 16:18:28 +0200 Subject: [PATCH 10/10] fix(tests): drop rg dependency and pin template ref to HEAD --- Makefile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 6bfed8b..9eeabce 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ test-template-render: test-template-render-github test-template-render-gitlab test-template-render-github: @set -euo pipefail; \ out_dir="$$(mktemp -d /tmp/scaf-template-github-XXXXXX)"; \ - copier copy . "$$out_dir" --trust --defaults \ + copier copy . "$$out_dir" --vcs-ref=HEAD --trust --defaults \ -d copier__project_name_raw="Sample GitHub Template" \ -d copier__project_slug="sample_github_template" \ -d copier__description="Sample github generated project" \ @@ -38,9 +38,9 @@ test-template-render-github: test ! -f "$$out_dir/template/dependencies-init.txt"; \ test ! -f "$$out_dir/template/dependencies-dev-init.txt"; \ test -f "$$out_dir/template/{{_copier_conf.answers_file}}"; \ - rg -Fq '{{ copier__project_name }}' "$$out_dir/template/README.md"; \ - rg -Fq 'copier copy . /path/to/new-project --trust' "$$out_dir/README.md"; \ - rg -q '^copier__project_name_raw:' "$$out_dir/copier.yml"; \ + grep -Fq '{{ copier__project_name }}' "$$out_dir/template/README.md"; \ + grep -Fq 'copier copy . /path/to/new-project --trust' "$$out_dir/README.md"; \ + grep -Eq '^copier__project_name_raw:' "$$out_dir/copier.yml"; \ render_dir="$$(mktemp -d /tmp/scaf-template-rendered-gh-XXXXXX)"; \ copier copy "$$out_dir" "$$render_dir" --trust --defaults \ -d copier__configure_repo=false \ @@ -57,7 +57,7 @@ test-template-render-github: test-template-render-gitlab: @set -euo pipefail; \ out_dir="$$(mktemp -d /tmp/scaf-template-gitlab-XXXXXX)"; \ - copier copy . "$$out_dir" --trust --defaults \ + copier copy . "$$out_dir" --vcs-ref=HEAD --trust --defaults \ -d copier__project_name_raw="Sample GitLab Template" \ -d copier__project_slug="sample_gitlab_template" \ -d copier__description="Sample gitlab generated project" \ @@ -83,9 +83,9 @@ test-template-render-gitlab: test ! -f "$$out_dir/template/dependencies-init.txt"; \ test ! -f "$$out_dir/template/dependencies-dev-init.txt"; \ test -f "$$out_dir/template/{{_copier_conf.answers_file}}"; \ - rg -Fq '{{ copier__project_name }}' "$$out_dir/template/README.md"; \ - rg -Fq 'copier copy . /path/to/new-project --trust' "$$out_dir/README.md"; \ - rg -q '^copier__project_name_raw:' "$$out_dir/copier.yml"; \ + grep -Fq '{{ copier__project_name }}' "$$out_dir/template/README.md"; \ + grep -Fq 'copier copy . /path/to/new-project --trust' "$$out_dir/README.md"; \ + grep -Eq '^copier__project_name_raw:' "$$out_dir/copier.yml"; \ render_dir="$$(mktemp -d /tmp/scaf-template-rendered-gl-XXXXXX)"; \ copier copy "$$out_dir" "$$render_dir" --trust --defaults \ -d copier__configure_repo=false \