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
8 changes: 8 additions & 0 deletions copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,11 @@ citation:
type: bool
help: Include CITATION.cff (Zenodo-ready, version-synced on release)?
default: false

update_bot:
type: bool
help: >-
Open template updates as pull requests via a GitHub App? Needs an App with Workflows write,
because the built-in token may not change files under .github/workflows. Without it, updates
arrive as an issue with the command to run locally
default: false
81 changes: 71 additions & 10 deletions profiles/python/.github/workflows/template-update.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
# Backports from the template: `copier update` replays changes made in
# coregraft since this repository was generated, and opens a pull request.
# coregraft since this repository was generated.
#
# Local edits are preserved: copier regenerates the old template version from
# .copier-answers.yml, diffs it against this repository to capture what you
# changed, applies the new template version, then re-applies your diff.
# Anything it cannot merge arrives as conflict markers for a human to resolve,
# so the pull request is a proposal, never an automatic overwrite.
# so the result is a proposal, never an automatic overwrite.
#
# Runs weekly and on demand. Needs no secret: the built-in token opens the PR.
# Two delivery modes, because GitHub refuses to let the built-in token create
# or update anything under .github/workflows/, and template updates routinely
# do exactly that:
#
# * With a GitHub App that has Workflows write, this opens a pull request.
# * Without one, it opens an issue naming the version and the command, and a
# maintainer runs the update locally where their own credentials apply.
#
# Both use the same `copier update`, so the merge quality is identical; only
# the automation differs. See the README's "Updates from the template".
#
# Runs weekly and on demand.
name: template-update

on:
Expand All @@ -18,6 +29,7 @@ on:
permissions:
contents: write
pull-requests: write
issues: write

concurrency:
group: template-update
Expand All @@ -26,6 +38,9 @@ concurrency:
jobs:
update:
runs-on: ubuntu-latest
env:
# `secrets` cannot be read in a job-level `if`, but it can here.
HAS_APP: ${{ secrets.RELEASE_APP_ID != '' }}
steps:
- name: Check out
uses: actions/checkout@v4
Expand All @@ -35,11 +50,6 @@ jobs:
- name: Set up the environment
uses: ./.github/actions/setup-python-env

- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Check for a newer template version
id: check
run: |
Expand All @@ -57,18 +67,35 @@ jobs:
echo "Already on $current."
fi

# >>> update_bot
- name: Generate app token
id: app-token
if: steps.check.outputs.outdated == 'true' && env.HAS_APP == 'true'
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

- name: Configure git identity
if: steps.app-token.outcome == 'success'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

# --conflict inline leaves git-style markers in place of a failed merge,
# so the pull request shows exactly what needs a decision. --trust is
# required because the template personalises through a task.
- name: Apply the template update
if: steps.check.outputs.outdated == 'true'
if: steps.app-token.outcome == 'success'
run: |
uvx copier update --skip-answered --trust --conflict inline --defaults

- name: Open a pull request
if: steps.check.outputs.outdated == 'true'
id: pr
if: steps.app-token.outcome == 'success'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.app-token.outputs.token }} # the built-in token cannot write workflows
branch: template-update
title: "chore: update from template ${{ steps.check.outputs.latest }}"
commit-message: "chore: update from template ${{ steps.check.outputs.latest }}"
Expand All @@ -80,3 +107,37 @@ jobs:
Review as you would any pull request; nothing here is applied automatically.
labels: template-update
delete-branch: true
# <<< update_bot

# Fallback. Also covers the case where the bot is configured in the
# template but the secret is missing here: a step that never ran has an
# empty conclusion, so this still fires.
- name: Open an issue instead
if: steps.check.outputs.outdated == 'true' && steps.pr.conclusion != 'success'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CURRENT: ${{ steps.check.outputs.current }}
LATEST: ${{ steps.check.outputs.latest }}
run: |
title="Template update available: $LATEST"
if gh issue list --state open --label template-update --json title --jq '.[].title' \
| grep -qxF "$title"; then
echo "An issue for $LATEST is already open."
exit 0
fi
gh label create template-update --color 0e8a16 \
--description "Updates from the template" 2>/dev/null || true
gh issue create --title "$title" --label template-update --body "$(cat <<EOF
This repository is on \`$CURRENT\`; the template is on \`$LATEST\`.

Apply the update locally:

\`\`\`bash
uvx copier update --skip-answered --trust --conflict inline
\`\`\`

Then review the result, resolve any \`<<<<<<<\` conflict markers, and open a pull request as usual. Your own local changes are preserved: copier re-applies them on top of the new template version rather than overwriting them.

**Why this is an issue and not a pull request.** Template updates usually change files under \`.github/workflows/\`, and GitHub refuses to let the built-in token write those. Automating it needs a GitHub App with **Workflows: write**, installed here, with \`RELEASE_APP_ID\` and \`RELEASE_APP_PRIVATE_KEY\` available as secrets. See the README's "Updates from the template".
EOF
)"
27 changes: 23 additions & 4 deletions profiles/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,41 @@ Everything else, or `make help` for the full list:
| `make install` | Creates the environment and installs the git hooks |
| `make docs` | Serves the documentation locally |
| `make build` | Builds a wheel into `dist/` |
# >>> benchmarks
<!-- >>> benchmarks -->
| `make bench` | Runs the performance benchmarks |
# <<< benchmarks
<!-- <<< benchmarks -->

`make check` lints untracked files on purpose: `pre-commit run -a` skips them, which is how a new file passes locally and then fails in CI the moment it is committed.

Commits follow [Conventional Commits](https://www.conventionalcommits.org/); releases, the changelog and versioned documentation are automated on merge to main. Until a release App is configured, the release workflow skips itself instead of failing.

# >>> benchmarks
<!-- >>> benchmarks -->

## Benchmarks

`make bench` runs the performance benchmarks in `tests/benchmarks/`. On pull requests they also run in CI and upload their results.

To track them over time, create a project on [Bencher](https://bencher.dev) (free for public repositories), add a `BENCHER_API_TOKEN` secret and set `BENCHER_PROJECT` in `.github/workflows/main.yml`. Bencher then comments each pull request with a statistical comparison against the base branch. Without the secret the benchmarks still run; only the tracking step is skipped.

# <<< benchmarks
<!-- <<< benchmarks -->

## Updates from the template

This repository was generated from [coregraft](https://github.com/OO-LD/coregraft) and records which version in `.copier-answers.yml`. A weekly workflow checks whether the template has moved on and, if so, replays its changes here. Your own edits are preserved: copier re-applies them on top of the new template version rather than over it, and anything it cannot merge arrives as `<<<<<<<` conflict markers for a human to decide.

By default the update arrives as an **issue** naming the new version and the command to run:

```bash
uvx copier update --skip-answered --trust --conflict inline
```

To get it as a **pull request** instead, this repository needs a GitHub App, because GitHub refuses to let the built-in token create or update anything under `.github/workflows/`, and template updates routinely do. One-time setup:

1. Use or create a GitHub App with **Contents: read and write**, **Pull requests: read and write** and **Workflows: read and write**. The last one is the whole point; without it the push is rejected. Organisations often already have a release App, which may need the Workflows permission added and the updated permission accepted on each installation.
2. Install the App on this repository.
3. Provide `RELEASE_APP_ID` and `RELEASE_APP_PRIVATE_KEY` as repository secrets, or share the organisation secrets with this repository. Installing the App and sharing the secret are two separate settings.

The workflow detects the secret at runtime, so it opens a pull request once the App is available and falls back to an issue when it is not. Either way the merge is the same; only the automation differs.

---

Expand Down
81 changes: 71 additions & 10 deletions profiles/schema/.github/workflows/template-update.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
# Backports from the template: `copier update` replays changes made in
# coregraft since this repository was generated, and opens a pull request.
# coregraft since this repository was generated.
#
# Local edits are preserved: copier regenerates the old template version from
# .copier-answers.yml, diffs it against this repository to capture what you
# changed, applies the new template version, then re-applies your diff.
# Anything it cannot merge arrives as conflict markers for a human to resolve,
# so the pull request is a proposal, never an automatic overwrite.
# so the result is a proposal, never an automatic overwrite.
#
# Runs weekly and on demand. Needs no secret: the built-in token opens the PR.
# Two delivery modes, because GitHub refuses to let the built-in token create
# or update anything under .github/workflows/, and template updates routinely
# do exactly that:
#
# * With a GitHub App that has Workflows write, this opens a pull request.
# * Without one, it opens an issue naming the version and the command, and a
# maintainer runs the update locally where their own credentials apply.
#
# Both use the same `copier update`, so the merge quality is identical; only
# the automation differs. See the README's "Updates from the template".
#
# Runs weekly and on demand.
name: template-update

on:
Expand All @@ -18,6 +29,7 @@ on:
permissions:
contents: write
pull-requests: write
issues: write

concurrency:
group: template-update
Expand All @@ -26,6 +38,9 @@ concurrency:
jobs:
update:
runs-on: ubuntu-latest
env:
# `secrets` cannot be read in a job-level `if`, but it can here.
HAS_APP: ${{ secrets.RELEASE_APP_ID != '' }}
steps:
- name: Check out
uses: actions/checkout@v4
Expand All @@ -35,11 +50,6 @@ jobs:
- name: Set up the environment
uses: ./.github/actions/setup-python-env

- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Check for a newer template version
id: check
run: |
Expand All @@ -57,18 +67,35 @@ jobs:
echo "Already on $current."
fi

# >>> update_bot
- name: Generate app token
id: app-token
if: steps.check.outputs.outdated == 'true' && env.HAS_APP == 'true'
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}

- name: Configure git identity
if: steps.app-token.outcome == 'success'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

# --conflict inline leaves git-style markers in place of a failed merge,
# so the pull request shows exactly what needs a decision. --trust is
# required because the template personalises through a task.
- name: Apply the template update
if: steps.check.outputs.outdated == 'true'
if: steps.app-token.outcome == 'success'
run: |
uvx copier update --skip-answered --trust --conflict inline --defaults

- name: Open a pull request
if: steps.check.outputs.outdated == 'true'
id: pr
if: steps.app-token.outcome == 'success'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.app-token.outputs.token }} # the built-in token cannot write workflows
branch: template-update
title: "chore: update from template ${{ steps.check.outputs.latest }}"
commit-message: "chore: update from template ${{ steps.check.outputs.latest }}"
Expand All @@ -80,3 +107,37 @@ jobs:
Review as you would any pull request; nothing here is applied automatically.
labels: template-update
delete-branch: true
# <<< update_bot

# Fallback. Also covers the case where the bot is configured in the
# template but the secret is missing here: a step that never ran has an
# empty conclusion, so this still fires.
- name: Open an issue instead
if: steps.check.outputs.outdated == 'true' && steps.pr.conclusion != 'success'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CURRENT: ${{ steps.check.outputs.current }}
LATEST: ${{ steps.check.outputs.latest }}
run: |
title="Template update available: $LATEST"
if gh issue list --state open --label template-update --json title --jq '.[].title' \
| grep -qxF "$title"; then
echo "An issue for $LATEST is already open."
exit 0
fi
gh label create template-update --color 0e8a16 \
--description "Updates from the template" 2>/dev/null || true
gh issue create --title "$title" --label template-update --body "$(cat <<EOF
This repository is on \`$CURRENT\`; the template is on \`$LATEST\`.

Apply the update locally:

\`\`\`bash
uvx copier update --skip-answered --trust --conflict inline
\`\`\`

Then review the result, resolve any \`<<<<<<<\` conflict markers, and open a pull request as usual. Your own local changes are preserved: copier re-applies them on top of the new template version rather than overwriting them.

**Why this is an issue and not a pull request.** Template updates usually change files under \`.github/workflows/\`, and GitHub refuses to let the built-in token write those. Automating it needs a GitHub App with **Workflows: write**, installed here, with \`RELEASE_APP_ID\` and \`RELEASE_APP_PRIVATE_KEY\` available as secrets. See the README's "Updates from the template".
EOF
)"
18 changes: 18 additions & 0 deletions profiles/schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ Schemas live in `schemas/` (`*.schema.json` with optional `*.instance.json` exam

Commits follow [Conventional Commits](https://www.conventionalcommits.org/); releases, the changelog and versioned documentation are automated on merge to main. Until a release App is configured, the release workflow skips itself instead of failing.

## Updates from the template

This repository was generated from [coregraft](https://github.com/OO-LD/coregraft) and records which version in `.copier-answers.yml`. A weekly workflow checks whether the template has moved on and, if so, replays its changes here. Your own edits are preserved: copier re-applies them on top of the new template version rather than over it, and anything it cannot merge arrives as `<<<<<<<` conflict markers for a human to decide.

By default the update arrives as an **issue** naming the new version and the command to run:

```bash
uvx copier update --skip-answered --trust --conflict inline
```

To get it as a **pull request** instead, this repository needs a GitHub App, because GitHub refuses to let the built-in token create or update anything under `.github/workflows/`, and template updates routinely do. One-time setup:

1. Use or create a GitHub App with **Contents: read and write**, **Pull requests: read and write** and **Workflows: read and write**. The last one is the whole point; without it the push is rejected. Organisations often already have a release App, which may need the Workflows permission added and the updated permission accepted on each installation.
2. Install the App on this repository.
3. Provide `RELEASE_APP_ID` and `RELEASE_APP_PRIVATE_KEY` as repository secrets, or share the organisation secrets with this repository. Installing the App and sharing the secret are two separate settings.

The workflow detects the secret at runtime, so it opens a pull request once the App is available and falls back to an issue when it is not. Either way the merge is the same; only the automation differs.

---

Generated from [coregraft](https://github.com/OO-LD/coregraft).
17 changes: 14 additions & 3 deletions scripts/apply_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@
"benchmarks": ["pytest.benchmark.ini", "tests/benchmarks"],
}

# Conditional blocks come in two spellings. `# >>> key` suits anything with
# hash comments; Markdown needs `<!-- >>> key -->`, because a line starting
# with "# " is a level-one heading there and trips every linter in the source
# file, even though the marker never survives into a generated repository.
OPEN = r"(?:#|<!--) >>>"
SHUT = r"(?:#|<!--) <<<"
ANY = r"(?:#|<!--) (?:>>>|<<<)"
CLOSE = r"(?: -->)?"

TEXT_SUFFIXES = {
".py",
".md",
Expand Down Expand Up @@ -95,14 +104,16 @@ def strip_markers(answers: dict[str, str]) -> None:
continue
if ">>>" not in text:
continue
for key in re.findall(r"# >>> (\w+)", text):
for key in re.findall(rf"{OPEN} (\w+)", text):
if truthy(answers.get(key, "")):
text = re.sub(rf"[ \t]*# (?:>>>|<<<) {key}\n", "", text)
text = re.sub(rf"[ \t]*{ANY} {key}{CLOSE}\n\n?", "", text)
else:
# Consume one blank line before the block too, otherwise
# removing a trailing block leaves the file ending in a blank
# line and end-of-file-fixer rewrites it on the first commit.
text = re.sub(rf"\n?[ \t]*# >>> {key}\n.*?[ \t]*# <<< {key}\n", "\n", text, flags=re.S)
text = re.sub(
rf"\n?[ \t]*{OPEN} {key}{CLOSE}\n.*?[ \t]*{SHUT} {key}{CLOSE}\n\n?", "\n", text, flags=re.S
)
# Whatever the surgery left behind, the file ends with exactly one newline.
path.write_text(text.rstrip("\n") + "\n", encoding="utf-8")

Expand Down
Loading
Loading