Container and PyPI Workflows
github-webhook-server has built-in workflows for container images and Python package publishing. Once a repository is configured, the server reacts to pull requests, merge events, comment commands, and pushed tags to build images, validate Python packaging, publish to PyPI, and send Slack notifications.
Note: These workflows are implemented in the server itself, not in GitHub Actions. In this repository, there are no release or publish workflows under
.github/workflows; the behavior comes from webhook handlers in the application code.
What Triggers What
Here is the high-level behavior:
pull_requestopened or synchronized: runs thebuild-containercheck ifcontaineris configured, and runs thepython-module-installcheck ifpypiis configured.issue_commentwith/build-and-push-container: builds and pushes a PR-tagged image.pull_requestmerged: builds and pushes a branch or stable image automatically.pushforrefs/tags/*: publishes to PyPI ifpypiis configured, and builds/pushes a release image ifcontainer.release: true.- Regular branch pushes: skipped for publishing.
A practical way to think about it is:
- PRs are for validation.
- Comment commands are for ad hoc pushes.
- Merges publish branch or stable images.
- Git tags publish releases.
Configuration
You can set these values in the central config.yaml or in a repository-local .github-webhook-server.yaml.
The relevant example from examples/.github-webhook-server.yaml looks like this:
slack-webhook-url: https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK
pypi:
token: pypi-your-token-here
events:
- push
- pull_request
- pull_request_review
- pull_request_review_comment
- pull_request_review_thread
- issue_comment
- check_run
- status
container:
username: your-registry-username
password: your-registry-password
repository: quay.io/your-org/your-repo
tag: latest
release: true
build-args:
- "BUILD_ARG=value"
args:
- "--platform=linux/amd64"
What these keys do:
events: if you explicitly list events, includepushfor tag releases,pull_requestfor PR checks and merge publishing, andissue_commentfor manual/build-and-push-container.container.usernameandcontainer.password: credentials used for pushing images and cleaning up PR tags.container.repository: the full image repository, such asghcr.io/org/imageorquay.io/org/image.container.tag: the stable tag used when a PR is merged intomainormaster. If omitted, the code defaults tolatest.container.release: enables extra container publishing on git tag pushes.container.build-args: converted into--build-argflags forpodman build.container.args: passed through as extrapodman buildflags, useful for values like--platform=linux/amd64or--format docker.pypi.token: the PyPI API token used fortwine upload.slack-webhook-url: enables Slack notifications for successful PyPI publishes and container push outcomes.
Note: Configuration values are resolved in this order: repository
.github-webhook-server.yaml, then the repository entry in centralconfig.yaml, then top-level defaults in centralconfig.yaml.Note: If you use the server’s repository settings sync, enabling
containeradds thebuild-containercheck to required status checks, and enablingpypiaddspython-module-install.
Container Workflow
Container builds use podman, not Docker. The server checks out a repository worktree, builds from the repository’s Dockerfile, and tags the image based on the event that triggered the build.
How image tags are chosen
The tag-selection logic in the code is:
if is_merged:
pull_request_branch = pull_request.base.ref
tag = (
pull_request_branch
if pull_request_branch not in (OTHER_MAIN_BRANCH, "main")
else self.container_tag
)
else:
tag = f"pr-{pull_request.number}"
That produces these user-visible results:
- PR validation builds use
pr-<number>. - Manual
/build-and-push-containerpushes also usepr-<number>. - Merges into
mainormasterusecontainer.tagsuch aslatest. - Merges into other base branches use the base branch name directly.
- Tag pushes use the git tag directly, such as
v1.2.3.
PR validation vs. manual push
When a PR is opened or updated, the server runs the build-container check. That check builds the image but does not push it to a registry.
If you want a pushed image for a PR, use the comment command:
/build-and-push-container
The welcome message generated by the server documents this command like this:
/build-and-push-container - Build and push container image (tagged with PR number)
Supports additional build arguments: /build-and-push-container --build-arg KEY=value
This manual command is separate from the build-container check. It is intended for “build me a test image now” workflows.
What happens on merge
When a PR is merged, the server automatically builds and pushes again.
- Merge to
mainormaster: pushes<container.repository>:<container.tag>. - Merge to another branch: pushes
<container.repository>:<base-branch>.
Merged-PR builds also add --no-cache to the podman build command.
Note:
container.release: trueonly controls tag-based release publishing. It does not disable the automatic image push that happens when a PR is merged.
What happens on tag pushes
If a tag push arrives and container.release: true is enabled, the server builds and pushes a release image tagged with that exact git tag.
Examples:
- Git tag
v1.2.3becomes image tagv1.2.3 - Git tag
2026.03.18becomes image tag2026.03.18
Regular branch pushes do not trigger container publishing.
Warning: Branch names and git tags become image tags as-is. Names like
release/1.2orrelease/v1.2.3are valid git refs, but they may not be valid container tags in your registry. Use registry-safe names such asv1.2.3for release tags.
Registry cleanup for PR images
When a PR is closed or merged, the server tries to delete the temporary pr-<number> tag from the registry.
- For
ghcr.io, it uses the GitHub Packages API. - For other registries such as Quay or Docker Hub, it uses
regctl.
Warning: Use a fully qualified image repository such as
ghcr.io/org/imageorquay.io/org/image. If the registry host is missing, the server skips PR-tag cleanup because it cannot tell which registry to talk to.
Draft PRs and manual container pushes
By default, comment commands are blocked on draft PRs. The example config.yaml documents the draft allowlist like this:
# allow-commands-on-draft-prs: # Or allow only specific commands:
# - build-and-push-container
# - retest
If you want /build-and-push-container to work on draft PRs, add it to allow-commands-on-draft-prs.
PyPI Workflow
Enabling pypi gives you two different behaviors:
- A PR-time packaging check named
python-module-install - A tag-time publish flow to PyPI
PR packaging check
On PR events, the server validates that the project can build as a wheel. The actual command in the code is:
cmd = "uvx pip wheel --no-cache-dir -w {worktree_path}/dist {worktree_path}"
This does not publish anything. It is just a fast packaging check that helps catch broken Python builds before release time.
Tag-based PyPI publishing
On a pushed git tag, the server checks out that tag and runs the publish flow. The key part of the implementation is:
rc, out, err = await run_command(
command=f"uv {uv_cmd_dir} build --sdist --out-dir {_dist_dir}", log_prefix=self.log_prefix
)
commands: list[str] = [
f"uvx {uv_cmd_dir} twine check {_dist_dir}/{tar_gz_file}",
f"uvx {uv_cmd_dir} twine upload --username __token__ "
f"--password {pypi_token} "
f"{_dist_dir}/{tar_gz_file} --skip-existing",
]
In plain English, the server does this:
- Checks out the pushed tag.
- Builds a source distribution into a temporary
pypi-distdirectory. - Runs
twine check. - Uploads to PyPI with
twine upload --skip-existing.
Note: The release upload is an sdist only. The PR-time
python-module-installcheck builds a wheel for validation, but the tag publish path uploads the source tarball.Note: The uploaded package version comes from your package metadata in
pyproject.toml, not from the git tag name. The server checks out the tag and builds whatever version is defined in the project at that commit.
If any part of the PyPI publish flow fails, the server creates a GitHub issue with a sanitized error summary instead of failing silently.
Tip: The upload uses
--skip-existing, which makes repeated delivery of the same tag much easier to tolerate if the artifact is already on PyPI.
Tag-Based Releases
The server only needs a pushed git tag. It does not require a specific release tool.
That said, this repository’s own release flow is a good example of how to generate those tags. The relevant part of .release-it.json is:
{
"git": {
"commitMessage": "Release ${version}",
"tag": true,
"tagAnnotation": "Release ${version}",
"push": true,
"pushArgs": ["--follow-tags"],
"changelog": "uv run scripts/generate_changelog.py ${from} ${to}"
},
"github": {
"release": true,
"releaseName": "Release ${version}"
},
"plugins": {
"@release-it/bumper": {
"in": "pyproject.toml",
"out": { "file": "pyproject.toml", "path": "project.version" }
}
},
"hooks": {
"after:bump": "uv sync"
}
}
That setup does four useful things:
- Bumps
project.versioninpyproject.toml - Runs
uv syncafter the bump - Commits and pushes a release tag
- Creates a GitHub release with generated changelog content
For github-webhook-server, the important detail is simple: once that tag is pushed to GitHub, the webhook server sees the push event and runs the PyPI and container release handlers.
Tip: You do not have to use
release-it. Any workflow that pushes a tag to GitHub will trigger the same server-side release behavior, as long as the repository is configured to receivepushevents.
Slack Notifications
Slack is optional. If slack-webhook-url is not configured, the server skips notification delivery.
The messages are sent through a standard Slack incoming webhook with a simple JSON payload. The message text looks like this:
<repository> Version <tag> published to PYPI.
<owner/repo> New container for <image:tag> published.
<owner/repo> Failed to build and push <image:tag>.
What currently sends Slack notifications:
- Successful PyPI publishes
- Successful container pushes
- Container push failures after a successful build step
What does not depend on Slack:
- PR validation checks
- Tag detection itself
- PyPI upload logic
- Container build logic
Slack is best treated as a notification layer, not the source of truth. Your real release state still lives in GitHub, your registry, and PyPI.
Practical Checklist
- Enable
push,pull_request, andissue_commentevents for the repository. - Configure
containerif you want image builds and registry pushes. - Configure
pypi.tokenif you want tag-based PyPI publishing. - Use a fully qualified image repository like
ghcr.io/org/image. - Use registry-safe git tags such as
v1.2.3. - Add
slack-webhook-urlif you want release notifications. - If you want manual container pushes on draft PRs, allow
build-and-push-containerinallow-commands-on-draft-prs.