This repository contains:
- a collection of reusable workflows
- a standalone CLI called
gha-utils
It is designed for uv-based Python projects, but can be used for other projects as well. Thanks to this project, I am able to release Python packages multiple times a day with only 2-clicks.
It takes care of:
- Version bumping
- Changelog management
- Formatting autofix for: Python, Markdown, JSON, typos
- Linting: Python types with
mypy, YAML,zsh, GitHub actions, URLS & redirects, Awesome lists, secrets - Compiling of Python binaries for Linux / macOS / Windows on
x86_64&arm64 - Building of Python packages and upload to PyPi
- Produce attestations
- Git version tagging and GitHub release creation
- Synchronization of:
uv.lock,.gitignore,.mailmapand Mermaid dependency graph - Auto-locking of inactive closed issues
- Static image optimization
- Sphinx documentation building & deployment, and
autodocupdates - Label management, with file-based and content-based rules
- Awesome list template synchronization
Nothing is done behind your back. A PR is created every time a change is proposed, so you can inspect it before merging it.
gha-utils stands for GitHub action workflows utilities.
Thanks to uv, you can run it in one command, without installation or venv:
$ uvx -- gha-utils
Usage: gha-utils [OPTIONS] COMMAND [ARGS]...
Options:
--time / --no-time Measure and print elapsed execution time. [default: no-
time]
--color, --ansi / --no-color, --no-ansi
Strip out all colors and all ANSI codes from output.
[default: color]
--config CONFIG_PATH Location of the configuration file. Supports local path
with glob patterns or remote URL. [default:
~/Library/Application Support/gha-
utils/*.toml|*.yaml|*.yml|*.json|*.ini]
--no-config Ignore all configuration files and only use command line
parameters and environment variables.
--show-params Show all CLI parameters, their provenance, defaults and
value, then exit.
--table-format [asciidoc|csv|csv-excel|csv-excel-tab|csv-unix|double-grid|double-outline|fancy-grid|fancy-outline|github|grid|heavy-grid|heavy-outline|html|jira|latex|latex-booktabs|latex-longtable|latex-raw|mediawiki|mixed-grid|mixed-outline|moinmoin|orgtbl|outline|pipe|plain|presto|pretty|psql|rounded-grid|rounded-outline|rst|simple|simple-grid|simple-outline|textile|tsv|unsafehtml|vertical|youtrack]
Rendering style of tables. [default: rounded-outline]
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG. [default:
WARNING]
-v, --verbose Increase the default WARNING verbosity by one level for
each additional repetition of the option. [default: 0]
--version Show the version and exit.
-h, --help Show this message and exit.
Commands:
changelog Maintain a Markdown-formatted changelog
mailmap-sync Update Git's .mailmap file with missing contributors
metadata Output project metadata
test-plan Run a test plan from a file against a binary
$ uvx -- gha-utils --version
gha-utils, version 4.24.6
That's the best way to get started with gha-utils and experiment with it.
To ease deployment, standalone executables of gha-utils's latest version are available as direct downloads for several platforms and architectures:
That way you have a chance to try it out without installing Python or uv. Or embed it in your CI/CD pipelines running on minimal images. Or run it on old platforms without worrying about dependency hell.
Note
ABI targets:
$ file ./gha-utils-*
./gha-utils-linux-arm64.bin: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=520bfc6f2bb21f48ad568e46752888236552b26a, for GNU/Linux 3.7.0, stripped
./gha-utils-linux-x64.bin: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=56ba24bccfa917e6ce9009223e4e83924f616d46, for GNU/Linux 3.2.0, stripped
./gha-utils-macos-arm64.bin: Mach-O 64-bit executable arm64
./gha-utils-macos-x64.bin: Mach-O 64-bit executable x86_64
./gha-utils-windows-arm64.exe: PE32+ executable (console) Aarch64, for MS Windows
./gha-utils-windows-x64.exe: PE32+ executable (console) x86-64, for MS Windows
To play with the latest development version of gha-utils, you can run it directly from the repository:
$ uvx --from git+https://github.com/kdeldycke/workflows -- gha-utils --version
gha-utils, version 4.18.2
This repository contains workflows to automate most of the boring tasks in the form of reusable GitHub actions workflows.
-
Workflows are designed to be reusable in other repositories via the
usessyntax:jobs: my-job: uses: kdeldycke/workflows/.github/workflows/[email protected]
-
uvis used everywhere to install dependencies and CLIs. -
Jobs are guarded by conditions to skip unnecessary steps when not needed.
-
Versions are pinned for actions, tools and CLIs, to ensure stability, reproducibility and security.
-
We eat our own dog-food: this repository uses these workflows for itself.
-
Format Python (
format-python)- Auto-formats Python code using
autopep8,ruff, andblacken-docs - Requires:
- Python files (
**/*.{py,pyi,pyw,pyx,ipynb}) in the repository, or - documentation files (
**/*.{markdown,mdown,mkdn,mdwn,mkd,md,mdtxt,mdtext,rst,tex})
- Python files (
- Auto-formats Python code using
-
Sync
uv.lock(sync-uv-lock)- Keeps
uv.lockfile up to date with dependencies usinguv - Requires:
- Python package with a
pyproject.tomlfile
- Python package with a
- Keeps
-
Format Markdown (
format-markdown)- Auto-formats Markdown files using
mdformat - Requires:
- Markdown files (
**/*.{markdown,mdown,mkdn,mdwn,mkd,md,mdtxt,mdtext}) in the repository
- Markdown files (
- Auto-formats Markdown files using
-
Format JSON (
format-json)- Auto-formats JSON, JSONC, and JSON5 files using ESLint with
@eslint/jsonplugin - Requires:
- JSON files (
**/*.{json,jsonc,json5},**/.code-workspace,!**/package-lock.json) in the repository
- JSON files (
- Auto-formats JSON, JSONC, and JSON5 files using ESLint with
-
Update .gitignore (
update-gitignore)- Regenerates
.gitignorefrom gitignore.io templates usinggit-extras - Requires:
- A
.gitignorefile in the repository
- A
- Regenerates
-
Lock inactive threads (
lock)- Automatically locks closed issues and PRs after 90 days of inactivity using
lock-threads
- Automatically locks closed issues and PRs after 90 days of inactivity using
-
Version increments (
version-increments)- Creates PRs for minor and major version bumps using
bump-my-version - Requires:
bump-my-versionconfiguration inpyproject.toml- A
changelog.mdfile
- Skipped for:
- Schedule events
- Release commits (starting with
[changelog] Release v)
- Creates PRs for minor and major version bumps using
-
Prepare release (
prepare-release)- Creates a release PR with changelog updates and version tagging using
bump-my-versionandgha-utils changelog - Requires:
bump-my-versionconfiguration inpyproject.toml- A
changelog.mdfile
- Creates a release PR with changelog updates and version tagging using
Some of these jobs requires a docs dependency group in pyproject.toml so they can determine the right Sphinx version to install and its dependencies:
[dependency-groups]
docs = [
"furo",
"myst-parser",
"sphinx",
…
]-
Fix typos (
autofix-typo)- Automatically fixes typos in the codebase using
typos
- Automatically fixes typos in the codebase using
-
Optimize images (
optimize-images)- Compresses images in the repository using
image-actions - Requires:
- Image files (
**/*.{jpeg,jpg,png,webp,avif}) in the repository
- Image files (
- Compresses images in the repository using
-
Update
.mailmap(update-mailmap)- Keeps
.mailmapfile up to date with contributors usinggha-utils mailmap-sync - Requires:
- A
.mailmapfile in the repository root
- A
- Keeps
-
Update dependency graph (
update-deps-graph)- Generates a Mermaid dependency graph of the Python project using
pipdeptree - Requires:
- Python package with a
pyproject.tomlfile
- Python package with a
- Generates a Mermaid dependency graph of the Python project using
-
Update autodoc (
update-autodoc)- Regenerates Sphinx autodoc files using
sphinx-apidoc - Requires:
- Python package with a
pyproject.tomlfile docsdependency group- Sphinx autodoc enabled (checks for
sphinx.ext.autodocindocs/conf.py)
- Python package with a
- Regenerates Sphinx autodoc files using
-
Deploy Sphinx doc (
deploy-docs) -
Sync awesome template (
awesome-template-sync)- Syncs awesome list projects from the
awesome-templaterepository usingactions-template-sync - Requires:
- Repository name starts with
awesome- - Repository is not
awesome-templateitself
- Repository name starts with
- Syncs awesome list projects from the
-
Sync labels (
sync-labels)- Synchronizes repository labels from a YAML definition file using
action-manage-label - Automatically includes
labels-awesome.yamlforawesome-*repositories
- Synchronizes repository labels from a YAML definition file using
-
File-based PR labeller (
file-labeller)- Automatically labels PRs based on changed file paths using
labeler - Skipped for:
prepare-releasebranch- Bot-created PRs
- Automatically labels PRs based on changed file paths using
-
Content-based labeller (
content-labeller)- Automatically labels issues and PRs based on title and body content using
issue-labeler - Skipped for:
prepare-releasebranch- Bot-created PRs
- Automatically labels issues and PRs based on title and body content using
-
Tag sponsors (
sponsor-labeller)- Adds a
💖 sponsorslabel to issues and PRs from sponsors usingis-sponsor-label-action - Skipped for:
prepare-releasebranch- Bot-created PRs
- Adds a
-
Mypy lint (
mypy-lint)- Type-checks Python code using
mypy - Requires:
- Python files (
**/*.{py,pyi,pyw,pyx,ipynb}) in the repository
- Python files (
- Skipped for:
prepare-releasebranch
- Type-checks Python code using
-
Lint YAML (
lint-yaml)- Lints YAML files using
yamllint - Requires:
- YAML files (
**/*.{yaml,yml}) in the repository
- YAML files (
- Skipped for:
prepare-releasebranch- Bot-created PRs
- Lints YAML files using
-
Lint Zsh (
lint-zsh)- Syntax-checks Zsh scripts using
zsh --no-exec - Requires:
- Zsh files (
**/*.zsh) in the repository
- Zsh files (
- Skipped for:
prepare-releasebranch- Bot-created PRs
- Syntax-checks Zsh scripts using
-
Lint GitHub Actions (
lint-github-action)- Lints workflow files using
actionlintandshellcheck - Requires:
- Workflow files (
.github/workflows/**/*.{yaml,yml}) in the repository
- Workflow files (
- Skipped for:
prepare-releasebranch- Bot-created PRs
- Lints workflow files using
-
Broken links (
broken-links)- Checks for broken links in documentation using
lychee - Creates/updates issues for broken links found
- Requires:
- Documentation files (
**/*.{markdown,mdown,mkdn,mdwn,mkd,md,mdtxt,mdtext,rst,tex}) in the repository
- Documentation files (
- Skipped for:
- All PRs (only runs on push to main)
prepare-releasebranch- Post-release bump commits
- Checks for broken links in documentation using
-
Lint Awesome list (
lint-awesome)- Lints awesome lists using
awesome-lint - Requires:
- Repository name starts with
awesome- - Repository is not
awesome-templateitself
- Repository name starts with
- Skipped for:
prepare-releasebranch
- Lints awesome lists using
-
Check secrets (
check-secrets)- Scans for leaked secrets using
gitleaks - Skipped for:
prepare-releasebranch- Bot-created PRs
- Scans for leaked secrets using
Release Engineering is a full-time job, and full of edge-cases that nobody wants to deal with. This workflow automates most of it for Python projects.
-
Build package (
package-build)- Builds Python wheel and sdist packages using
uv build - Requires:
- Python package with a
pyproject.tomlfile
- Python package with a
- Builds Python wheel and sdist packages using
-
Compile binaries (
compile-binaries)- Compiles standalone binaries using
Nuitkafor Linux/macOS/Windows onx64/arm64 - Requires:
- Python package with CLI entry points defined in
pyproject.toml
- Python package with CLI entry points defined in
- Compiles standalone binaries using
-
Test binaries (
test-binaries)- Runs test plans against compiled binaries using
gha-utils test-plan - Requires:
- Compiled binaries from
compile-binariesjob - Test plan file (default:
./tests/cli-test-plan.yaml)
- Compiled binaries from
- Runs test plans against compiled binaries using
-
Git tag (
git-tag)- Creates a Git tag for the release version
- Requires:
- Push to
mainbranch - Release commits matrix from
gha-utils metadata
- Push to
-
Publish to PyPi (
pypi-publish)- Uploads packages to PyPi with attestations using
uv publish - Requires:
PYPI_TOKENsecret- Built packages from
package-buildjob
- Uploads packages to PyPi with attestations using
-
GitHub release (
github-release)- Creates a GitHub release with all artifacts attached using
action-gh-release - Requires:
- Successful
git-tagjob
- Successful
- Creates a GitHub release with all artifacts attached using
Most jobs in this repository depend on a shared parent job called project-metadata. It runs first to extracts contextual information, reconcile and combine them, and expose them for downstream jobs to consume.
This expand the capabilities of GitHub actions, since it allows to:
- Share complex data across jobs (like build matrix)
- Remove limitations of conditional jobs
- Allow for runner introspection
- Fix quirks (like missing environment variables, events/commits mismatch, merge commits, etc.)
This job relies on the gha-utils metadata command to gather data from multiple sources:
- Git: current branch, latest tag, commit messages, changed files
- GitHub: event type, actor, PR labels
- Environment: OS, architecture
pyproject.toml: project name, version, entry points
Important
This flexibility comes at the cost of:
- Making the whole workflow a bit more computationally intensive
- Introducing a small delay at the beginning of the run
- Preventing child jobs to run in parallel before its completion
But is worth it given how GitHub actions can be frustrating.
All dependencies in this project are pinned to specific versions to ensure stability, reproducibility, and security. This section explains the mechanisms in place.
| Mechanism | What it pins | How it's updated |
|---|---|---|
requirements/*.txt files |
Python CLIs used in workflows | Dependabot PRs |
uv.lock |
Project dependencies | sync-uv-lock job |
| Hard-coded versions in YAML | GitHub Actions, npm packages | Dependabot PRs |
uv --exclude-newer option |
Transitive dependencies | Time-based window |
| Tagged workflow URLs | Remote workflow references | Release process |
Each Python CLI used in workflows has its own requirements file (e.g., requirements/yamllint.txt). This allows Dependabot to track and update each tool independently.
We use these files instead of inline version strings because Dependabot cannot parse versions in run: blocks—it only supports requirements.txt and pyproject.toml files for Python projects (source).
# ❌ Dependabot cannot update this:
- run: uvx -- yamllint==1.37.1
# ✅ So we use requirements/yamllint.txt as an indirection:
- run: uvx --with-requirements …/requirements/yamllint.txt -- yamllintA root requirements.txt aggregates all files from the requirements/ folder for bulk installation.
GitHub Actions and npm packages are pinned directly in YAML files:
- uses: actions/[email protected] # Pinned action
- run: npm install [email protected] # Pinned npm packageDependabot's github-actions ecosystem handles action updates.
Warning
For npm packages, we pin versions inline since they're used sparingly, and then update them manually when needed.
To avoid update fatigue, and mitigate supply chain attacks, .github/dependabot.yaml uses cooldown periods (with prime numbers to stagger updates).
This ensures major updates get more scrutiny while patches flow through faster.
The uv.lock file pins all project dependencies, and the sync-uv-lock job keeps it in sync.
The --exclude-newer flag ignores packages released in the last 7 days, providing a buffer against freshly-published broken releases.
Workflows in this repository are self-referential, and points to themselves via raw GitHub URLs.
During development, these point to main:
--with-requirements https://github.com/kdeldycke/workflows/main/requirements/yamllint.txt
...The prepare-release job rewrites these to the release tag before tagging:
# Before release commit:
…/workflows/main/requirements/yamllint.txt
# In the tagged release commit:
…/workflows/v4.24.6/requirements/yamllint.txt
# After post-release bump:
…/workflows/main/requirements/yamllint.txtThis ensures released versions reference immutable, tagged URLs while main remains editable.
As explained above, this repository updates itself via GitHub actions. But updating its own YAML files in .github/workflows is forbidden by default, and we need extra permissions.
Usually, to grant special permissions to some jobs, you use the permissions parameter in workflow files:
on: (…)
jobs:
my-job:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps: (…)But contents: write doesn't allow write access to workflow files in .github/. The actions: write permission only covers workflow runs, not their YAML source files. Even permissions: write-all doesn't work.
You will always end up with this error:
! [remote rejected] branch_xxx -> branch_xxx (refusing to allow a GitHub App to create or update workflow `.github/workflows/my_workflow.yaml` without `workflows` permission)
error: failed to push some refs to 'https://github.com/kdeldycke/my-repo'
Note
The Settings → Actions → General → Workflow permissions setting on your repository has no effect on this issue. Even with "Read and write permissions" enabled, the default GITHUB_TOKEN cannot modify workflow files—that's a hard security boundary enforced by GitHub:

To bypass this limitation, create a custom access token called WORKFLOW_UPDATE_GITHUB_PAT. It replaces the default secrets.GITHUB_TOKEN in steps that modify workflow files.
-
Go to GitHub → Settings → Developer Settings → Personal Access Tokens → Fine-grained tokens
-
Click Generate new token
-
Configure:
Field Value Token name workflow-self-update(or similar descriptive name)Expiration Choose based on your security policy Repository access Select Only select repositories and choose the repos that need workflow self-updates -
Click Add permissions:
Permission Access Contents Read and Write Metadata Read-only (mandatory) Pull requests Read and Write Workflows Read and Write [!IMPORTANT] The Workflows permission is the key. This is the only place where you can grant it—it's not available via the
permissions:parameter in YAML files. -
Click Generate token and copy the
github_pat_XXXXvalue
- Go to your repository → Settings → Security → Secrets and variables → Actions
- Click New repository secret
- Set:
- Name:
WORKFLOW_UPDATE_GITHUB_PAT - Secret: paste the
github_pat_XXXXtoken
- Name:
Re-run your workflow. It should now update files in .github/workflows/ without the error.
Tip
For organizations: Consider using a machine user account or a dedicated service account to own the PAT, rather than tying it to an individual's account.
Warning
Token expiration: Fine-grained PATs expire. Set a calendar reminder to rotate the token before expiration, or your workflows will fail silently.
Check these projects to get real-life examples of usage and inspiration:
Awesome Falsehood - Falsehoods Programmers Believe in.
Awesome Engineering Team Management - How to transition from software development to engineering management.
Awesome IAM - Identity and Access Management knowledge for cloud platforms.
Awesome Billing - Billing & Payments knowledge for cloud platforms.
Meta Package Manager - A unifying CLI for multiple package managers.
Mail Deduplicate - A CLI to deduplicate similar emails.
dotfiles - macOS dotfiles for Python developers.
Click Extra - Extra colorization and configuration loading for Click.
workflows - Itself. Eat your own dog-food.
Extra Platforms - Detect platforms and group them by family.
Feel free to send a PR to add your project in this list if you are relying on these scripts.
All steps of the release process and version management are automated in the
changelog.yaml
and
release.yaml
workflows.
All there's left to do is to:
- check the open draft
prepare-releasePR and its changes, - click the
Ready for reviewbutton, - click the
Rebase and mergebutton, - let the workflows tag the release and set back the
mainbranch into a development state.