Skip to content
This repository was archived by the owner on Mar 27, 2026. It is now read-only.
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
48 changes: 48 additions & 0 deletions .github/scripts/build-extras.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash
# This file is part of the tschm/.config-templates repository
# (https://github.com/tschm/.config-templates).
#
# Optional hook script for installing extra dependencies
#
# Purpose: This script is called automatically during the install phase and before
# building documentation. Use it to install additional system packages or
# dependencies that your project needs (e.g., graphviz for diagrams).
#
# When it runs:
# - Called by: make install-extras (during setup phase)
# - Also runs before: make test, make book, make docs
# - Environment: GitHub Actions runner or local development machine
#
# How to use:
# 1. Add your custom installation commands below
# 2. Make sure the script is executable: chmod +x .github/scripts/build-extras.sh
# 3. Commit to your repository
#
# Examples:
# - Install system packages: apt-get, brew, yum, etc.
# - Install optional dependencies for documentation
# - Download or build tools needed by your project
#
# Note: If you customize this file in your repository, add it to the exclude list
# in action.yml to prevent it from being overwritten by template updates:
# exclude: |
# .github/scripts/build-extras.sh
#

set -euo pipefail

echo "Running build-extras.sh..."

# Add your custom installation commands here

# Example: graphviz
# Good practice to check if already installed.
#
# if ! command -v dot &> /dev/null; then
# echo "graphviz not found, installing..."
# sudo apt-get update && sudo apt-get install -y graphviz
# else
# echo "graphviz is already installed, skipping installation."
# fi

echo "Build extras setup complete."
5 changes: 3 additions & 2 deletions .github/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ template-repository: "tschm/.config-templates"
template-branch: "main"
include: |
.github
taskfiles
tests
.editorconfig
.gitignore
Expand All @@ -11,4 +10,6 @@ include: |
CONTRIBUTING.md
Makefile
ruff.toml
Taskfile.yml
exclude: |
.github/workflows/docker.yml
.github/workflows/devcontainer.yml
98 changes: 98 additions & 0 deletions .github/workflows/_devcontainer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Github Actions workflow to build and optionally publish a devcontainer image
#
# This file is part of the tschm/.config-templates repository
# (https://github.com/tschm/.config-templates).
#
# Workflow: DEVCONTAINER
#
# Purpose:
# Build a devcontainer image (optionally multi-platform) and optionally publish it
# to a container registry. Designed to be reusable via workflow_call so callers
# can override image name, registry, platforms, and whether to publish.
#
# Inputs:
# - publish: boolean (default: false)
# Whether to push the built image to the registry.
# - registry: string (default: ghcr.io)
# Container registry to use for publishing (e.g. ghcr.io, docker.io).
# - image_name:string (default: devcontainer)
# Full image name or repository/image suffix to tag the built image.
# - config_file: string (default: .devcontainer/devcontainer.json)
# Path to the devcontainer.json file to build the image from.
# - image_tags: string (default: latest,${{ github.sha }})
# Comma-separated list of tags to apply to the built image.
# Components / Steps:
# - Checkout repository
# - Login to the configured container registry (uses the caller's secrets)
# - Get lowercase repository owner for image naming
# - Build (and optionally push) the devcontainer image using devcontainers/ci
#
# Notes / Recommendations:
# - Callers should provide secrets (tokens) via the calling workflow's `secrets`.
# - Keep the default registry as a sensible default; callers can override via inputs.
# - Use the `image_name` input to change image name per-repo or per-call.
# - For publishing to GHCR use the repo owner and secrets.GITHUB_TOKEN or a PAT
# with packages:write permission as the calling workflow's secrets.
#
name: REUSABLE DEVCONTAINER BUILD
permissions:
contents: read
packages: write

on:
workflow_call:
inputs:
publish:
description: 'Whether to publish the built image to the registry'
required: false
default: false
type: boolean
registry:
description: 'Container registry to publish the image to'
required: false
default: 'ghcr.io'
type: string
image_name:
description: 'Name of the image to publish'
required: false
default: devcontainer
type: string
config_file:
description: 'Path to the devcontainer.json file'
required: false
default: '.devcontainer/devcontainer.json'
type: string
image_tags:
description: 'Tags to apply to the built image (comma-separated), defaults to latest and commit SHA'
required: false
default: latest,${{ github.sha }}
type: string

jobs:
devcontainer-build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

# Login to the chosen container registry. Caller should provide appropriate secrets.
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ inputs.registry }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

# Convert repository owner to lowercase for Docker image naming
- name: Get lowercase repository owner
id: repo_owner
run: echo "owner_lc=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT

# Build the devcontainer image. `devcontainers/ci` supports building and pushing.
- name: Build Devcontainer Image
uses: devcontainers/ci@v0.3
with:
configFile: ${{ inputs.config_file }}
push: ${{ inputs.publish && 'always' || 'never' }}
imageName: ${{ inputs.registry }}/${{ steps.repo_owner.outputs.owner_lc }}/${{ inputs.image_name }}
imageTag: ${{ inputs.image_tags }}
148 changes: 142 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
#
# This workflow implements a secure, maintainable release pipeline
# by separating concerns:
# - 🔍 Pre-flight checks (Tag/Release existence)
# - 🔖 Tagging the release (Git tag)
# - 🏗️ Building the package with Hatch
# - 🚀 Publishing to PyPI using OIDC (no passwords or secrets)
# - 🐳 Building and publishing Devcontainer image (if present)
# - 🚀 Publishing to PyPI using OIDC (no passwords or secrets) or custom feed
# - 📦 Creating a GitHub Release with artifacts (dist + devcontainer image)
#
# 🔐 Security:
# - No PyPI credentials are stored; relies on Trusted Publishing via GitHub OIDC.
# - For custom feeds, secrets are used, default username is `__token__`, which is set in action by default anyway.
#
# 📄 Requirements:
# - `pyproject.toml` with a top-level `version = "..."`
Expand All @@ -21,7 +25,7 @@
# - Go to the "Actions" tab
# - Run this workflow manually with a tag input like `v1.2.3`

name: Release Workflow
name: RELEASE

on:
workflow_dispatch:
Expand All @@ -30,19 +34,63 @@ on:
description: 'Release tag (e.g. v1.2.3)'
required: true
type: string
release_devcontainer:
description: 'Release devcontainer image'
required: false
default: false
type: boolean

permissions:
contents: write # Needed to create releases
id-token: write # Needed for OIDC authentication with PyPI
packages: write # Needed to publish devcontainer image

jobs:
tag:
name: Create Git Tag
runs-on: ubuntu-latest
outputs:
devcontainer_exists: ${{ steps.check_devcontainer.outputs.exists }}
image_name: ${{ steps.image_name.outputs.name }}
steps:
- name: Checkout Code
uses: actions/checkout@v6

- name: Validate Tag and Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.event.inputs.tag }}
run: |
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
echo "::error::Tag '$TAG' already exists. Please use a new tag."
exit 1
fi
if gh release view "$TAG" >/dev/null 2>&1; then
DRAFT_STATUS=$(gh release view "$TAG" --json isDraft --jq '.isDraft')
if [ "$DRAFT_STATUS" != "true" ]; then
echo "::error::Release '$TAG' already exists and is not a draft. Please use a new tag."
exit 1
else
echo "::warning::Release '$TAG' exists as a draft. Will proceed to update it."
fi
fi

- name: Check for .devcontainer
id: check_devcontainer
run: |
if [[ "${{ github.event.inputs.release_devcontainer }}" == "true" ]] && [[ -d ".devcontainer" ]]; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi

- name: Calculate Image Name
id: image_name
run: |
REPO="${{ github.event.repository.name }}"
CLEAN="${REPO#.}"
echo "name=${CLEAN}/devcontainer" >> $GITHUB_OUTPUT

- name: Create Git Tag
run: |
git config user.name "${{ github.actor }}"
Expand Down Expand Up @@ -85,8 +133,8 @@ jobs:
name: dist
path: dist

release:
name: Create GitHub Release
draft-release:
name: Draft GitHub Release
runs-on: ubuntu-latest
needs: [tag, build]

Expand All @@ -106,13 +154,16 @@ jobs:
name: ${{ github.event.inputs.tag }}
generate_release_notes: true
files: "dist/**"
draft: true

# Parse .env and decide at step-level whether to publish
# Decide at step-level whether to publish
pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
environment: release
needs: [build, tag]
needs: [build, tag, draft-release]
outputs:
should_publish: ${{ steps.check_dist.outputs.should_publish }}

steps:
- name: Checkout Code
Expand Down Expand Up @@ -141,9 +192,94 @@ jobs:
cat $GITHUB_OUTPUT

# this should not take place, as "Private :: Do Not Upload" set in pyproject.toml
# repository-url and password only used for custom feeds, not for PyPI with OIDC
- name: Publish to PyPI
if: ${{ steps.check_dist.outputs.should_publish == 'true' }}
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
skip-existing: true
repository-url: ${{ vars.PYPI_REPOSITORY_URL }}
password: ${{ secrets.PYPI_TOKEN }}

publish-devcontainer:
name: Publish Devcontainer
needs: [tag, build, draft-release]
if: needs.tag.outputs.devcontainer_exists == 'true'
uses: ./.github/workflows/_devcontainer.yml
with:
publish: true
image_name: ${{ needs.tag.outputs.image_name }}
registry: ghcr.io
image_tags: ${{ github.event.inputs.tag }}
secrets: inherit

finalise-release:
name: Finalise Release
runs-on: ubuntu-latest
needs: [tag, draft-release, pypi, publish-devcontainer]
if: |
always() &&
needs.tag.result == 'success' &&
needs.draft-release.result == 'success' &&
(needs.pypi.result == 'success' || needs.pypi.result == 'skipped') &&
(needs.publish-devcontainer.result == 'success' || needs.publish-devcontainer.result == 'skipped')
steps:
- name: Checkout Code
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.14'

- name: Generate Devcontainer Link
id: devcontainer_link
if: needs.publish-devcontainer.result == 'success'
run: |
OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
IMAGE_NAME="${{ needs.tag.outputs.image_name }}"
FULL_IMAGE="ghcr.io/$OWNER_LC/$IMAGE_NAME:${{ github.event.inputs.tag }}"
{
echo "message<<EOF"
echo "### Devcontainer Image"
echo ""
echo "\`$FULL_IMAGE\`"
echo "EOF"
} >> $GITHUB_OUTPUT

- name: Generate PyPI Link
id: pypi_link
if: needs.pypi.outputs.should_publish == 'true' && needs.pypi.result == 'success'
run: |
PACKAGE_NAME=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['name'])")
VERSION="${{ github.event.inputs.tag }}"
VERSION=${VERSION#v}

REPO_URL="${{ vars.PYPI_REPOSITORY_URL || '' }}"
if [ -z "$REPO_URL" ]; then
LINK="https://pypi.org/project/$PACKAGE_NAME/$VERSION/"
NAME="PyPI Package"
else
LINK="$REPO_URL"
NAME="Custom Feed Package"
fi

{
echo "message<<EOF"
echo "### $NAME"
echo ""
echo "[$PACKAGE_NAME]($LINK)"
echo "EOF"
} >> $GITHUB_OUTPUT

- name: Publish Release
uses: softprops/action-gh-release@v2.4.2
with:
tag_name: ${{ github.event.inputs.tag }}
draft: false
append_body: true
body: |
${{ steps.devcontainer_link.outputs.message }}
${{ steps.pypi_link.outputs.message }}

2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ repos:
- id: check-yaml

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.14.6'
rev: 'v0.14.7'
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix, --unsafe-fixes ]
Expand Down
Loading