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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
* fix/readme-badge: the Copier badge in generated READMEs links to karshosphere/template-python.
* fix/project-builds: generated projects install, start and build, with a uv.lock, the right colorlog package, a working FastAPI lifespan and a pinned uv in the dev container.
* feature/run-script: generated projects ship a run.sh for building, formatting with ruff, testing with pytest and building a wheel and sdist, with starter tests and ruff-clean code.
* feature/github-ci: generated projects ship the shared GitHub CI, a VERSION file and the shared Renovate settings, which copier update leaves alone.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,14 @@ Every generated project has a `run.sh` at its root:
* `./run.sh --format` formats the Python sources with ruff; `--format-dry-run` lists unformatted files and fails if there are any.
* `./run.sh --test` runs the tests with pytest.
* `./run.sh --release` builds a wheel and an sdist into `dist/`.


### GitHub CI

Generated projects ship with the shared workflow from [template-github-ci](https://github.com/karshosphere/template-github-ci), vendored into `template/.github/`. It expects the project's `run.sh` and `.devcontainer/`, requires a `VERSION` bump on every pull request, and publishes a GitHub release on each merge to main.

To bring in a newer release of the workflow, from this repository's root:

```bash
$ copier update -a .github/.copier-ci-answers.yml template
```
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v1.1.0
v1.2.0
5 changes: 5 additions & 0 deletions copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ _min_copier_version: "9.10.0"
_subdirectory: template
_answers_file: .copier-generic-project/.copier-py-answers.yml

# the project owns these once created
_skip_if_exists:
- VERSION
- renovate.json

#===============================================================================
# Interactive

Expand Down
25 changes: 1 addition & 24 deletions renovate.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"helpers:pinGitHubActionDigests",
"docker:pinDigests",
":semanticCommitScopeDisabled"
],
"timezone": "Australia/Melbourne",
"schedule": ["before 6am on sunday"],
"minimumReleaseAge": "7 days",
"internalChecksFilter": "strict",
"dependencyDashboard": true,
"automerge": false,
"semanticCommits": "enabled",
"semanticCommitType": "chore",
"customManagers": [
{
"customType": "regex",
"description": "versions marked with a renovate comment on the line above",
"managerFilePatterns": ["/^\\.github/workflows/[^/]+\\.ya?ml$/", "/(^|/)Dockerfile$/"],
"matchStrings": [
"# renovate: datasource=(?<datasource>[a-z-]+) depName=(?<depName>\\S+)\\s+(?:ARG |ENV )?[\\w-]+[:=] *\"?(?<currentValue>[^\\s\"@]+)(?:@(?<currentDigest>sha256:[a-f0-9]+))?\"?"
]
}
]
"extends": ["github>karshosphere/template-github-ci"]
}
4 changes: 4 additions & 0 deletions template/.github/.copier-ci-answers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# .github/.copier-ci-answers.yml.jinja
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
_commit: v1.1.1
_src_path: gh:karshosphere/template-github-ci
146 changes: 146 additions & 0 deletions template/.github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
name: ci

# pull_request rather than pull_request_target, so a fork's code never runs with this repository's secrets
on:
pull_request:
push:
branches: [main]

permissions:
contents: read

concurrency:
# pull requests share a group per branch so newer pushes replace older runs; each commit on main
# gets its own group, since a pending run in a shared group is dropped and its tag would be skipped
group: ci-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
# renovate: datasource=node-version depName=node
NODE_VERSION: 24.22.0
# renovate: datasource=npm depName=@devcontainers/cli
DEVCONTAINER_CLI_VERSION: 0.89.0

jobs:
version:
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# full history, so every existing release tag is available to compare against
fetch-depth: 0
- name: VERSION holds a single vX.Y.Z version
run: |
if ! grep -Eqx 'v[0-9]+\.[0-9]+\.[0-9]+' VERSION || [ "$(grep -c "" VERSION)" -ne 1 ]; then
echo "::error file=VERSION::VERSION must be one line of the form vX.Y.Z"
exit 1
fi
- name: VERSION is higher than every release tag
run: |
version=$(tr -d '[:space:]' < VERSION)
latest=$(git tag --list --sort=-v:refname | { grep -Ex 'v[0-9]+\.[0-9]+\.[0-9]+' || true; } | sed -n 1p)
if [ -n "$latest" ]; then
highest=$(printf '%s\n' "$latest" "$version" | sort -V | tail -n 1)
if [ "$version" = "$latest" ] || [ "$highest" != "$version" ]; then
echo "::error file=VERSION::VERSION is $version but $latest is already released; bump it above $latest"
exit 1
fi
fi

checks:
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: run.sh is present and executable
run: |
[ -x run.sh ] || {
echo "::error file=run.sh::run.sh is missing or not executable"
exit 1
}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ env.NODE_VERSION }}
- run: npm install --global "@devcontainers/cli@$DEVCONTAINER_CLI_VERSION"
- name: Start the dev container
run: devcontainer up --workspace-folder .
- name: Format
run: devcontainer exec --workspace-folder . ./run.sh --format-dry-run
- name: Build
run: devcontainer exec --workspace-folder . ./run.sh --build
- name: Test
run: devcontainer exec --workspace-folder . ./run.sh --test
- name: Release build
run: devcontainer exec --workspace-folder . ./run.sh --release
- if: github.event_name == 'push'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist
path: dist/
# a project releasing no targets leaves dist/ empty
if-no-files-found: ignore

release:
if: github.event_name == 'push'
needs: [version, checks]
runs-on: ubuntu-26.04
permissions:
# publishes the release, which also creates its tag
contents: write
# reads the merged pull request's description for the release notes
pull-requests: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- id: version
run: echo "tag=$(tr -d "[:space:]" < VERSION)" >> "$GITHUB_OUTPUT"
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
# a pattern rather than a name, so a missing artifact means no files instead of a failure
pattern: dist
merge-multiple: true
path: dist
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
TAG: ${{ steps.version.outputs.tag }}
with:
script: |
const fs = require('fs');
const path = require('path');
const tag = process.env.TAG;

const { data: pulls } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
...context.repo,
commit_sha: context.sha,
});
const merged = pulls.find(pull => pull.merge_commit_sha === context.sha) ?? pulls.find(pull => pull.merged_at);
const notes = merged?.body || context.payload.head_commit?.message || '';

const files = fs.existsSync('dist')
? fs.readdirSync('dist', { withFileTypes: true }).filter(entry => entry.isFile()).map(entry => entry.name)
: [];

// a draft holds the tag back until every file is attached, so a failed upload leaves no half-made release
const { data: release } = await github.rest.repos.createRelease({
...context.repo,
tag_name: tag,
target_commitish: context.sha,
name: tag,
body: notes,
draft: true,
});
for (const name of files) {
const data = fs.readFileSync(path.join('dist', name));
await github.rest.repos.uploadReleaseAsset({
...context.repo,
release_id: release.id,
name,
data,
headers: { 'content-type': 'application/octet-stream', 'content-length': data.length },
});
}
await github.rest.repos.updateRelease({ ...context.repo, release_id: release.id, draft: false });
core.notice(`Released ${tag} with ${files.length} file(s)`);
1 change: 1 addition & 0 deletions template/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v0.1.0
4 changes: 4 additions & 0 deletions template/renovate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>karshosphere/template-github-ci"]
}