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
20 changes: 16 additions & 4 deletions .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,25 @@ on:
tags:
- 'v*.*.*'

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

jobs:
changelog:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
# Mint a short-lived, scoped token from the fleet automation GitHub App. A PR
# opened with the default GITHUB_TOKEN does not trigger CI or the label-gated
# publish, so the automation needs a distinct identity; the App token gives it
# one without a long-lived personal access token.
- name: Generate a token from the fleet automation GitHub App
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- name: Checkout repository
uses: actions/checkout@v7
with:
Expand Down Expand Up @@ -63,7 +75,7 @@ jobs:
- name: Create Pull Request for CHANGELOG
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.CHANGELOG_AUTOMATION_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
add-paths: |
CHANGELOG.md
commit-message: "chore(release): update CHANGELOG.md for ${{ steps.changelog.outputs.version }}"
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/commit-message.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
types: [opened, edited, synchronize, reopened]
branches: [main]

# Least-privilege default: read-only token.
permissions:
contents: read

jobs:
validate-commit-message:
name: Validate Commit Message
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
types: [opened, edited, synchronize, reopened]
branches: [main]

# Least-privilege default: read-only token; the job below widens to pull-requests:read.
permissions:
contents: read

jobs:
validate-pr-title:
name: Validate PR Title
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
branches:
- main

# Least-privilege default: read-only token; the create-release job widens to contents:write.
permissions:
contents: read

jobs:
create-release:
# Only run if PR was merged and has the changelog label
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

# Least-privilege default: read-only token. No job here writes.
permissions:
contents: read

jobs:
# Fast tests run on every push and PR across all OS and Python versions
test-fast:
Expand Down Expand Up @@ -92,6 +96,26 @@ jobs:
# renovate: datasource=pypi depName=nox
uvx nox@2026.7.11 -s fix

# Secret scanning. Runs the gitleaks binary directly (not gitleaks-action, which
# requires a paid license for private org repos). It is a dependency of the ci-passed
# roll-up below, so a detected secret fails the single required merge check.
secret-scan:
name: Secret scanning (gitleaks)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Install gitleaks
run: |
# renovate: datasource=github-releases depName=gitleaks/gitleaks
GITLEAKS_VERSION=8.24.3
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar -xz -C /usr/local/bin gitleaks

- name: Scan for secrets
run: gitleaks detect --source . --redact --verbose

# Single required status check. The branch ruleset requires this one job, not the
# individual jobs above: test-fast is a matrix (its check-run names vary by OS/Python and
# differ between draft and ready PRs) and test-full is skipped on draft PRs, so neither
Expand All @@ -106,6 +130,7 @@ jobs:
- test-fast
- test-full
- lint
- secret-scan
runs-on: ubuntu-latest
steps:
- name: Fail if any required job did not pass
Expand Down
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ repos:
- id: commitizen
stages: [commit-msg]

# Secret scanning: block secrets before they enter history. prek runs the official
# gitleaks hook (its golang language is supported). The gitleaks CI job is the
# un-bypassable backstop; this hook is the earliest catch, so it stays at pre-commit
# even though the refusing whole-project hooks (interrogate) run at pre-push.
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.3
hooks:
- id: gitleaks

# Version-sensitive linters run from the project environment so their single
# source of truth is uv.lock. --locked makes a stale uv.lock fail loudly
# instead of silently diverging from CI. (Dependabot's "uv" ecosystem then
Expand Down
23 changes: 23 additions & 0 deletions copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,20 @@ license:
- "GPL-3.0"
- "Proprietary"

repo_visibility:
type: str
# Drives which paid, visibility-sensitive services are provisioned. Almost every
# "paid" security service (CodeQL, Codecov, GitHub-native secret scanning) is paid
# only because the repository is private -- each is free on public repos. Capturing
# visibility once lets the template default those services on for public repos and
# substitute the free OSS equivalents (ruff S, gitleaks) on private ones, instead of
# exposing one flag per service.
help: "Repository visibility (public repos get free paid-tier services; private repos get OSS equivalents)"
default: "public"
choices:
- "public"
- "private"

min_python_version:
type: str
help: "Minimum Python version"
Expand All @@ -139,6 +153,15 @@ include_actions:
help: "Include GitHub Actions CI/CD workflows?"
default: true

include_codecov:
type: bool
# Codecov coverage upload. Free on public repos, a paid org plan on private ones, so
# the default follows repo_visibility. Overridable: a private project willing to pay
# can set this true. Gated on include_actions because the upload runs in CI.
help: "Upload coverage to Codecov? (free on public repos, paid on private)"
default: "{{ repo_visibility == 'public' }}"
when: "{{ include_actions }}"

uv_version:
type: str
# Pinned exact uv version for the CI setup-uv steps. Pinning an exact X.Y.Z
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ select = [
"C4", # flake8-comprehensions
"UP", # pyupgrade
"PL", # Pylint
"S", # flake8-bandit (security)
"T10", # flake8-debugger
"T201", # Print statement
]
Expand All @@ -137,7 +138,7 @@ ignore = [
]

[tool.ruff.lint.per-file-ignores]
"tests/**/*" = ["T201", "PLR2004", "PLC0415"]
"tests/**/*" = ["T201", "PLR2004", "PLC0415", "S101", "S603", "S607", "S506"] # asserts, subprocess to git/uv/copier, and yaml.load of generated files are expected in the template test harness

[tool.rumdl]
flavor = "mkdocs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ renovate.json
.github/ISSUE_TEMPLATE/config.yml
.github/ISSUE_TEMPLATE/feature_request.yml
.github/PULL_REQUEST_TEMPLATE.md
SECURITY.md
.claude/skills/** # Skill files managed by template (the tracked copy)
.github/skills/** # Byte-identical Copilot mirror; gitignored, so an
# update never delivers it -- copier works through
Expand Down Expand Up @@ -95,6 +96,9 @@ docs/pages/how-to/contribute.md
.github/workflows/nightly.yml # conditional: include_actions
.github/workflows/changelog.yml # conditional: include_actions
.github/workflows/commit-message.yml # conditional: include_actions
.github/workflows/codeql.yml # conditional: include_actions + public repo_visibility
.github/workflows/scorecard.yml # conditional: include_actions + public repo_visibility
CODEOWNERS
```

---
Expand Down
2 changes: 2 additions & 0 deletions template/.copier-answers.yml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ author_name: {{ author_name }}
description: {{ description }}
github_username: {{ github_username }}
include_actions: {{ include_actions }}
include_codecov: {{ include_codecov }}
include_examples: {{ include_examples }}
license: {{ license }}
max_python_version: '{{ max_python_version }}'
Expand All @@ -16,4 +17,5 @@ package_name: {{ package_name }}
project_name: {{ project_name }}
project_slug: {{ project_slug }}
renovate_preset: '{{ renovate_preset }}'
repo_visibility: {{ repo_visibility }}
uv_version: '{{ uv_version }}'
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ renovate.json
.github/ISSUE_TEMPLATE/config.yml
.github/ISSUE_TEMPLATE/feature_request.yml
.github/PULL_REQUEST_TEMPLATE.md
SECURITY.md
.claude/skills/** # Skill files managed by template (the tracked copy)
.github/skills/** # Byte-identical Copilot mirror; gitignored, so an
# update never delivers it -- copier works through
Expand Down Expand Up @@ -95,6 +96,9 @@ docs/pages/how-to/contribute.md
.github/workflows/nightly.yml # conditional: include_actions
.github/workflows/changelog.yml # conditional: include_actions
.github/workflows/commit-message.yml # conditional: include_actions
.github/workflows/codeql.yml # conditional: include_actions + public repo_visibility
.github/workflows/scorecard.yml # conditional: include_actions + public repo_visibility
CODEOWNERS
```

---
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
name: Build and create changelog PR
name: Create changelog PR

on:
push:
tags:
- 'v*.*.*'

env:
dists-artifact-name: python-package-distributions
# Least-privilege default: read-only token unless a job widens it explicitly. The PR
# is opened with a GitHub App token (below), not GITHUB_TOKEN, so no job needs write.
permissions:
contents: read

jobs:
changelog:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
# Mint a short-lived, scoped token from the fleet automation GitHub App. A PR
# opened with the default GITHUB_TOKEN does not trigger CI or the label-gated
# publish, so the automation needs a distinct identity; the App token gives it
# one without a long-lived personal access token.
- name: Generate a token from the fleet automation GitHub App
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: {% raw %}${{ secrets.APP_ID }}{% endraw %}
private-key: {% raw %}${{ secrets.APP_PRIVATE_KEY }}{% endraw %}

- name: Checkout repository
uses: actions/checkout@v7
with:
Expand Down Expand Up @@ -69,7 +79,7 @@ jobs:
- name: Create Pull Request for CHANGELOG
uses: peter-evans/create-pull-request@v8
with:
token: {% raw %}${{ secrets.CHANGELOG_AUTOMATION_TOKEN }}{% endraw %}
token: {% raw %}${{ steps.app-token.outputs.token }}{% endraw %}
add-paths: |
CHANGELOG.md
commit-message: "chore(release): update CHANGELOG.md for {% raw %}${{ steps.changelog.outputs.version }}{% endraw %}"
Expand All @@ -89,35 +99,3 @@ jobs:
labels: |
changelog
automated

build:
runs-on: ubuntu-latest
needs:
- changelog
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
ref: main

- name: Install the latest version of uv
uses: astral-sh/setup-uv@v7
with:
# renovate: datasource=github-releases depName=astral-sh/uv
version: "{{ uv_version }}"
enable-cache: true
cache-dependency-glob: "pyproject.toml"
github-token: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %}

- name: Build package
run: uv build --python {{ min_python_version }} --python-preference only-managed --sdist --wheel . --out-dir dist

- name: Check distributions
run: uvx twine check dist/*

- name: Store the distribution packages
uses: actions/upload-artifact@v7
with:
name: {% raw %}${{ env.dists-artifact-name }}{% endraw %}
path: dist/*
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
types: [opened, edited, synchronize, reopened]
branches: [main]

# Least-privilege default: read-only token.
permissions:
contents: read

jobs:
validate-commit-message:
name: Validate Commit Message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
- cron: "30 2 * * *"
workflow_dispatch: # Allow manual triggering

# Least-privilege default: read-only token; create-issue-on-failure widens to issues:write.
permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
Expand All @@ -15,9 +19,9 @@ jobs:
{%- for v in versions if v >= min_python_version and v <= max_python_version -%}
"{{ v }}"{{ ", " if not loop.last else "" }}
{%- endfor -%}]
env:
{% if include_codecov %} env:
CODECOV_TOKEN: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %}
steps:
{% endif %} steps:
- uses: actions/checkout@v7

- name: Install uv
Expand All @@ -41,12 +45,12 @@ jobs:
- name: Run docstring tests
run: nox -s test_docstrings

- name: Upload coverage reports
{% if include_codecov %} - name: Upload coverage reports
uses: codecov/codecov-action@v7
if: {% raw %}${{ matrix.python-version == '3.12' && env.CODECOV_TOKEN != '' }}{% endraw %}
with:
token: {% raw %}${{ env.CODECOV_TOKEN }}{% endraw %}

{% endif %}
create-issue-on-failure:
needs: test
if: {% raw %}${{ failure() }}{% endraw %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
types: [opened, edited, synchronize, reopened]
branches: [main]

# Least-privilege default: read-only token; the job below widens to pull-requests:read.
permissions:
contents: read

jobs:
validate-pr-title:
name: Validate PR Title
Expand Down
Loading
Loading