Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .git-cliff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ This **{{ release_type }} release** includes {{ commits | length }} commit{%- if

### {{ group | striptags | trim | upper_first }}
{%- for commit in commits %}
- {{ commit.message | upper_first }}{%- if commit.github.pr_number %} ([#{{ commit.github.pr_number }}](https://github.com/stateful-y/python-package-copier/pull/{{ commit.github.pr_number }})){%- endif %}{%- if commit.github.username %} by @{{ commit.github.username }}{%- endif %}
- {{ commit.message | upper_first }}{%- if commit.remote.pr_number %} ([#{{ commit.remote.pr_number }}](https://github.com/stateful-y/python-package-copier/pull/{{ commit.remote.pr_number }})){%- endif %}{%- if commit.remote.username %} by @{{ commit.remote.username }}{%- endif %}
{%- endfor %}
{%- endfor %}
{%- set new_contributors = commits | filter(attribute="github.username") | map(attribute="github.username") | unique %}
{%- set new_contributors = commits | filter(attribute="remote.username") | map(attribute="remote.username") | unique %}
{%- if new_contributors | length > 0 %}

### Contributors
Expand Down
7 changes: 7 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,10 @@ just fix # Auto-format and lint
just serve # Preview docs
uv sync --group test # Install test deps
```

## Skills
Skills in `.claude/skills/` mirror `.github/skills/` (kept for GitHub Copilot). Edit both when changing one. This is now enforced: `tests/test_repo_workflows.py` asserts the two trees track the same files and are byte-identical. The check is scoped to files git tracks, because both trees deliberately ignore `openspec-*` skills as tool state the openspec CLI writes into whichever copy it likes.

Skills that ship to *generated* projects live in `template/.github/skills/` and are a separate set — they are not mirrored here. Within `template/`, the `.github/skills/` and `.claude/skills/` copies must stay byte-identical. Note that `test_claude_skills_generated` checks only that both trees carry the same skill *names* and that each has a `SKILL.md`; two copies whose contents diverged would still pass it.

`polish-changelog` is **not on `main`**. Its content lives on the unmerged branch `feat/changelog-polish-skill` and in `~/.claude/skills/polish-changelog/` (global, out-of-repo, updated by hand), so it is available in every repo but tracked in none. A canonical `changelog-polish` capability spec exists in the OpenSpec tree, which is gitignored and therefore local-only, describing behaviour the repo does not currently ship. Either land that branch or treat the global copy as the only source.
3 changes: 3 additions & 0 deletions .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ jobs:

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
# renovate: datasource=github-releases depName=astral-sh/uv
version: "0.10.0"

- name: Run hooks on generated CHANGELOG
run: |
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CodeQL

on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# Weekly, so a newly-disclosed query pattern still runs against unchanged code.
- cron: "27 3 * * 1"

# Least-privilege default: read-only token; the analyze job widens as needed.
permissions:
contents: read

jobs:
analyze:
name: Analyze (Python)
runs-on: ubuntu-latest
permissions:
# Required to upload CodeQL results to the Security tab.
security-events: write
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: python

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v3
3 changes: 3 additions & 0 deletions .github/workflows/commit-message.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ jobs:
- name: Install uv
if: ${{ github.event.pull_request.commits == 1 }}
uses: astral-sh/setup-uv@v7
with:
# renovate: datasource=github-releases depName=astral-sh/uv
version: "0.10.0"

- name: Set up Python
if: ${{ github.event.pull_request.commits == 1 }}
Expand Down
101 changes: 101 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Nightly Build

# The canary this repository ships to every generated project and did not run itself.
#
# This repo's suite generates projects and runs `nox` inside them, so it depends on the
# whole external toolchain (copier, uv, ruff, prek, zensical) resolving at head. That
# makes it the fleet's most exposed repo to upstream drift and, until now, the only one
# with no scheduled run: a template regression surfaced as red nightlies in seven
# downstream repos before this repo noticed, so the signal arrived everywhere except
# where the cause was.
#
# Unlike the generated version the matrix is fixed here rather than derived from copier
# answers, because this repository is not itself a generated project.

on:
schedule:
# Run every day at 2:30 AM UTC
- cron: "30 2 * * *"
workflow_dispatch: # Allow manual triggering

permissions:
contents: read

jobs:
test:
name: Nightly suite (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v7

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
# renovate: datasource=github-releases depName=astral-sh/uv
version: "0.10.0"
enable-cache: true
cache-dependency-glob: "pyproject.toml"

- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

# The full suite, slow and integration tests included: those are the ones that
# generate a project and run nox inside it, which is the part upstream drift
# actually breaks. A fast-only nightly would be green through exactly the
# failures this job exists to catch.
- name: Run the full test suite
# renovate: datasource=pypi depName=nox
run: uvx nox@2026.7.11 -s test-${{ matrix.python-version }}

create-issue-on-failure:
needs: test
if: ${{ failure() }}
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/checkout@v7

- name: Create issue on failure
uses: actions/github-script@v9
with:
script: |
const title = '🔴 Nightly build failed';
const body = `The nightly build failed on ${{ github.sha }}.

**Workflow run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
**Branch:** ${{ github.ref_name }}
**Commit:** ${{ github.sha }}

Please investigate and fix the issue.`;

// Check if there's already an open issue for nightly build failures
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'nightly-build-failure'
});

if (issues.data.length === 0) {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['nightly-build-failure', 'bug']
});
} else {
// Add a comment to the existing issue
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issues.data[0].number,
body: `Another nightly build failure occurred:\n\n${body}`
});
}
41 changes: 41 additions & 0 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: OpenSSF Scorecard

on:
branch_protection_rule:
schedule:
# Weekly independent grade of the repo's security posture.
- cron: "21 4 * * 2"
push:
branches: [main]

# Least-privilege default: read-only token; the analysis job widens as needed.
permissions:
contents: read

jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
# Upload the results to the Security tab and publish to the OpenSSF API.
security-events: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Run analysis
uses: ossf/scorecard-action@v2.4.4
with:
results_file: results.sarif
results_format: sarif
repo_token: ${{ github.token }}
# Publish results to the OpenSSF Scorecard API (public repos only).
publish_results: true

- name: Upload SARIF to code scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
9 changes: 9 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
# renovate: datasource=github-releases depName=astral-sh/uv
version: "0.10.0"
enable-cache: true
cache-dependency-glob: "pyproject.toml"

Expand Down Expand Up @@ -68,6 +70,8 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
# renovate: datasource=github-releases depName=astral-sh/uv
version: "0.10.0"
enable-cache: true
cache-dependency-glob: "pyproject.toml"

Expand All @@ -87,6 +91,9 @@ jobs:

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
# renovate: datasource=github-releases depName=astral-sh/uv
version: "0.10.0"

- name: Set up Python
run: uv python install 3.12
Expand Down Expand Up @@ -132,6 +139,8 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
# renovate: datasource=github-releases depName=astral-sh/uv
version: "0.10.0"
enable-cache: true
cache-dependency-glob: "pyproject.toml"

Expand Down
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,11 @@ Desktop.ini
/.github/prompts/
/.github/skills/openspec-*/

# AI Assistant Instructions
/CLAUDE.md
# AI assistant instructions are NOT ignored. CLAUDE.md is project content: it is the
# file that tells every assistant how this repo works, and ignoring it meant it never
# entered a pull request, was never reviewed, did not survive a clone, and drifted from
# its tracked Copilot counterpart by exactly the section instructing that both be kept
# in step. An instruction file outside version control cannot be tested.
/CLAUDE_INSTRUCTIONS.md
/.claude.md
/claude-instructions.md
Expand Down
53 changes: 53 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Python Package Copier

## Project Overview
This is a **Copier template** for generating modern Python packages.
- **Root**: configuration for *this* repo (tests, docs for the template).
- **`template/`**: Jinja2 source files for *generated* projects. Changes here affect output.
- **`copier.yml`**: Defines user prompts/variables (e.g., `{{ package_name }}`).

## Critical Workflows
- **Package Management**: Uses **uv** exclusively. No `pip` or `venv`.
- **Task Running**: `uvx nox` (global install) or `just`.
- **Testing**:
- **Fast (Unit)**: `just test-fast` (Checks file structure/content generation).
- **Slow (Integration)**: `just test-slow` (Runs `nox` *inside* generated projects).
- **Fixture**: Use `copie` fixture (wraps `CopierTestFixture`) in `tests/conftest.py`.
- **Lint/Fix**: `just fix` (Runs `uv run prek`; prek and the lint tools are all pinned by `uv.lock`).

## Architecture & Patterns
- **Template Files**: End in `.jinja`. Variables: `{{ min_python_version }}`.
- **Conditional Dirs**: directory names like `{% if include_examples %}examples{% endif %}/`.
- **Tech Stack (Target)**: `uv`, `hatchling` (build), `ruff` (linter), `ty` (types), `nox` (tasks).
- **Nox Configuration**:
- **Root**: `noxfile.py` tests the template.
- **Template**: `template/noxfile.py.jinja` tests the *generated* package.
- **Important**: `nox` is NOT a project dependency; it's run via `uvx`.

## Development Rules
1. **Adding Features**:
- Update `copier.yml` (prompts).
- Update `template/` files.
- Update `tests/conftest.py` default answers.
- Add test case in `tests/test_template.py`.
2. **Dependencies**:
- Root `pyproject.toml`: Only for testing the template (copier, pytest).
- Template `pyproject.toml.jinja`: Definition for generated packages.
3. **CI/CD**:
- Template uses `tests.yml` (fast/full split).
- Generated projects get `tests.yml`, `changelog.yml` (git-cliff), `publish-release.yml`.

## Common Commands
```bash
just test-fast # Run unit tests (fast feedback)
just fix # Auto-format and lint
just serve # Preview docs
uv sync --group test # Install test deps
```

## Skills
Skills in `.claude/skills/` mirror `.github/skills/` (kept for GitHub Copilot). Edit both when changing one. This is now enforced: `tests/test_repo_workflows.py` asserts the two trees track the same files and are byte-identical. The check is scoped to files git tracks, because both trees deliberately ignore `openspec-*` skills as tool state the openspec CLI writes into whichever copy it likes.

Skills that ship to *generated* projects live in `template/.github/skills/` and are a separate set — they are not mirrored here. Within `template/`, the `.github/skills/` and `.claude/skills/` copies must stay byte-identical. Note that `test_claude_skills_generated` checks only that both trees carry the same skill *names* and that each has a `SKILL.md`; two copies whose contents diverged would still pass it.

`polish-changelog` is **not on `main`**. Its content lives on the unmerged branch `feat/changelog-polish-skill` and in `~/.claude/skills/polish-changelog/` (global, out-of-repo, updated by hand), so it is available in every repo but tracked in none. A canonical `changelog-polish` capability spec exists in the OpenSpec tree, which is gitignored and therefore local-only, describing behaviour the repo does not currently ship. Either land that branch or treat the global copy as the only source.
17 changes: 17 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Security Policy

## Supported versions

Security fixes are released for the latest tagged version of this template. Because this is a
Copier template rather than a published package, a fix reaches a generated project only when
that project runs `copier update`; projects are not patched in place.

## Reporting a vulnerability

Please report security vulnerabilities privately through GitHub's
[private vulnerability reporting](https://github.com/stateful-y/python-package-copier/security/advisories/new)
rather than opening a public issue.

We aim to acknowledge a report within a few working days and will keep you updated as
we investigate and prepare a fix. Please give us a reasonable window to release a patch
before any public disclosure.
41 changes: 41 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Codecov reports coverage for this repository; it does not judge it.
#
# Two facts make a pass/fail verdict meaningless here, and neither is a defect to be
# fixed by tightening a threshold:
#
# 1. Coverage measures the wrong thing, unavoidably. `[tool.coverage.run] source` is
# ["tests"], because this repository has no `src/`. That answers "did this test file
# execute", which a passing test answers by definition. The product is `template/`:
# Jinja sources that coverage cannot instrument, executed only inside generated
# projects in temporary directories during the integration tests. A generated
# project measures `source = ["<package_name>"]` and the number means something
# there. Here it does not.
#
# 2. Nothing enforces it, by design. The coverage job is deliberately excluded from the
# `ci-passed` roll-up so that a Codecov outage -- the very thing the OIDC upload is a
# canary for -- cannot block merges. That decision is correct and stays.
#
# Left at defaults, Codecov combined those two into a comment reading ":x: Patch
# coverage is 87% ... Please review" on a pull request whose checks were entirely
# green and which nothing could stop merging. A red signal that can never mean
# anything trains readers to ignore the source, including on the day it matters.
#
# Both statuses are off rather than `informational`. `informational` only stops the
# status blocking; Codecov still computes the target comparison and the pull request
# comment still renders ":x: ... Please review". Since the grade is what is meaningless
# here, the grade is what goes. The comment stays for the per-line annotations, which
# is how the unrun failure branches in the parity tests were found.
#
# NOTE: Codecov reads repository configuration from the DEFAULT BRANCH, so this file
# has no effect until it is merged. The comment on the pull request that introduced it
# will still show the old framing.
coverage:
status:
project: false
patch: false

# The comment is worth keeping: its uncovered-line table is how the unrun failure
# branches in the parity tests were found. Only the pass/fail framing goes.
comment:
layout: "condensed_header, files, condensed_footer"
require_changes: true
Loading
Loading