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
41 changes: 41 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CodeQL

# Static analysis for this repository. Generated projects get an equivalent
# workflow of their own; this is the template repository applying its own
# advice to itself.
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "29 10 * * 0"

permissions:
contents: read

jobs:
analyze:
name: Analyze Python
runs-on: ubuntu-latest
permissions:
security-events: write
packages: read
actions: read
contents: read
steps:
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: python
build-mode: none
queries: security-and-quality

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:python"
43 changes: 43 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Security Policy

## Scope

This repository is a project template. It ships GitHub Actions workflows,
dependency declarations and configuration that are copied into other people's
projects, so a problem here propagates to everything generated from it.

Reports about the template itself are in scope: a workflow that leaks
credentials, an over-permissioned token, a dependency pinned to something
malicious, or a generated default that is unsafe.

## Supported versions

The latest tag receives fixes. Older tags do not.

## Reporting a vulnerability

Please **do not** open a public issue.

Report privately through
[GitHub Security Advisories](https://github.com/leynier/python-template/security/advisories/new),
or by email to <leynier41@gmail.com>.

Please include:

- What the problem is and what an attacker could do with it.
- Which file or generated output is affected.
- Steps to reproduce, ideally the `copier` answers that produce the bad output.

You can expect an initial response within 7 days.

## Security posture of this repository

- GitHub Actions are pinned to release tags, and
[zizmor](https://github.com/zizmorcore/zizmor) audits every workflow on
change. The pinning policy is declared in `zizmor.yml`.
- Workflows declare least-privilege `permissions:`.
- Dependabot has a cooldown before adopting newly published releases.
- CodeQL and OpenSSF Scorecard run against this repository.

Generated projects get the same treatment, plus PyPI Trusted Publishing with
Sigstore attestations instead of long-lived API tokens.
2 changes: 1 addition & 1 deletion template/pyproject.toml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Documentation = "https://{{ github_username }}.github.io/{{ github_repo }}"

{% endif -%}
[build-system]
requires = ["uv_build>=0.12,<0.13"]
requires = ["uv_build>=0.12.1,<0.13"]
build-backend = "uv_build"

[tool.uv.build-backend]
Expand Down
45 changes: 45 additions & 0 deletions tests/test_repo_practices_what_it_preaches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""The template repository must apply its own advice to itself.

OpenSSF Scorecard flagged this repository for a missing security policy and for
having no static analysis, while the template happily handed both to every
project it generated. These tests stop that drift from coming back.
"""

import pathlib

import pytest

REPO = pathlib.Path(__file__).resolve().parent.parent


@pytest.mark.parametrize(
"path",
[
"SECURITY.md",
"changelog.md",
"readme.md",
"LICENSE",
"zizmor.yml",
],
)
def test_repository_has_the_files_it_generates(path: str) -> None:
assert (REPO / path).is_file(), f"{path} is missing from the template repository"


@pytest.mark.parametrize("workflow", ["ci.yml", "codeql.yml", "scorecard.yml"])
def test_repository_runs_the_workflows_it_generates(workflow: str) -> None:
assert (REPO / ".github" / "workflows" / workflow).is_file()


def test_required_status_check_exists() -> None:
"""Branch protection requires a check named "CI"; something must produce it.

Renaming or dropping this job would block every future merge, which is
exactly what happened with the job name inherited from 2021.
"""
import yaml

ci = yaml.safe_load((REPO / ".github" / "workflows" / "ci.yml").read_text())
names = {job.get("name") for job in ci["jobs"].values()}

assert "CI" in names, f"no job produces the required 'CI' check; found {names}"