Webhook integrations
On this page
The forge integration configures the forge side of a Copr webhook so pushes
and tags to a repository trigger rebuilds. GitHub and GitLab are supported,
each with one command: coprctl integration github enable and
coprctl integration gitlab enable.
Access requirements
The GitHub integration needs a GitHub personal access token (PAT) scoped to the repositories you manage.
- Scope:
admin:repo_hook(classic PATs), or for fine-grained PATs theWebhooksread/write permission on the target repositories. GitHub's guidance on creating fine-grained tokens is at docs.github.com. - This is the narrowest scope that works and the only one the tool uses. It reads no code and writes no commits.
- A broader token is never needed and is worse to leak; keep the scope tight.
- The token is read at runtime from
GITHUB_TOKENorGH_TOKEN. It is never stored by the tool and never printed.
The GitLab integration needs a GitLab personal access token with the api
scope on the groups and projects you manage.
- The token is read at runtime from
GITLAB_TOKENand is never stored by the tool. - Self-hosted GitLab: set
GITLAB_API_URLto the API root of your instance, including the/api/v4prefix. Without it the tool targets gitlab.com. Note that package scoping still matchesgitlab.comclone URLs, so on a self-hosted instance the package-scoped URL may not be selected; the receiver URL itself is independent of the clone host.
The tool does not yet validate the scope before acting; a missing scope surfaces as an error from the forge API when the hook is created or updated.
Before you begin
- The Copr project exists and is owned by you (or a group you administer).
- A webhook secret is known to the tool. The secret is stored in local state
and is not invented: run
coprctl integration rotate-secretonce if none is cached. - The packages you want to rebuild on push exist in the project.
Enable a webhook
For GitHub:
export GH_TOKEN=<your-token>
coprctl integration rotate-secret OWNER/PROJECT --yes
coprctl integration github enable OWNER/PROJECT \
--repo OWNER/REPO
For GitLab:
export GITLAB_TOKEN=<your-token>
coprctl integration rotate-secret OWNER/PROJECT --yes
coprctl integration gitlab enable OWNER/PROJECT \
--repo GROUP/PROJECT
The default trigger is tag-only: GitHub listens for the create event and
GitLab for the tag push event, both of which fire when a tag is created.
Copr uses the tag name to rebuild the matching package
(PKGNAME-VERSION[-RELEASE]). This means branch pushes do not trigger rebuilds
by default.
The trigger is configurable:
--tag-only=falseopts back in to branch pushes (GitHubpushevent, GitLabpush_eventstoggle).- GitHub additionally accepts
--events push,createto override the default with an explicit event list. GitLab has no event list; its triggers are thepush_eventsandtag_push_eventstoggles, so--eventsis not offered.
The command:
- resolves the Copr webhook URL from the project id and the cached secret,
scoped to the matching SCM package so a bare
v<semver>tag matches that package by name; - enables auto-rebuild on the relevant packages;
- creates the forge hook, or updates an existing one whose destination still carries this project's receiver prefix (forge and project id, so a rotated secret is recognized), and never duplicates hooks;
- sends a delivery check: GitHub reads back the ping delivery status, GitLab triggers a test hook;
- persists the hook id in local state for later reconciliation.
Enabling a webhook implies the tag pushes should rebuild the package, so
auto-rebuild is turned on for the project's SCM packages automatically. Pass
--no-auto-rebuild to opt out and only wire the hook.
Both sides are reported in the result, and the webhook URL is masked unless
--reveal is passed.
Disable a webhook
coprctl integration disable removes the forge hook a previous enable created.
coprctl integration disable OWNER/PROJECT \
--forge github --repo OWNER/REPO --yes
The command verifies before it deletes: it lists the hooks on the target repo and removes the one whose destination equals the expected Copr webhook URL for that forge and project (including the package scope, when the enable selected one). The stored hook id is only a hint, so a stale id never deletes a hook aimed at a different project or forge. If no hook matches, nothing is deleted and the command reports the mismatch.
Disabling does not rotate the Copr webhook secret. The secret is
project-scoped and shared across every hook, so rotation would break other
consumers. Rotate explicitly with integration rotate-secret only when you
intend to break all hooks.
Custom webhooks
Any service can POST to the custom receiver. The route requires a package name, so the URL is always package-scoped:
coprctl integration url OWNER/PROJECT --forge custom --package PKG
The output is the URL to configure in the external service. For GitHub and
GitLab the same command prints the standard forge URL, and --package selects
the package-scoped form:
coprctl integration url OWNER/PROJECT
coprctl integration url OWNER/PROJECT --forge gitlab --package PKG
--forge bitbucket is accepted too; the receiver route is identical in shape,
though there is no enable command for Bitbucket yet.
The receiver choice matters: the custom route is for packages whose source
method is custom (it hands the raw hook payload to the build script). A
package with an SCM source is rebuilt through the forge route, which parses a
forge-shaped payload; the trigger command below sends that shape.
Triggering a rebuild without a forge hook
Not every project wants the forge to decide. A release pipeline may need a
stable-tag-only policy, or a manual rebuild for one package. coprctl integration trigger drives the same receiver a forge hook would, so the
pipeline keeps the hook-free setup and still does not need a Copr API token:
coprctl integration trigger OWNER/PROJECT/PKG --tag v1.2.3
The command resolves the package's SCM source first: an upload-sourced
package, or an SCM source without a clone URL, fails before anything is sent,
with the conversion step as the hint. The receiver URL is package-scoped, so
Copr matches the bare v<semver> tag to the package by name. The secret is
the one rotate-secret cached, or the COPRCTL_WEBHOOK_SECRET environment
variable when the pipeline passes it in; the override is read at trigger time
and never persisted.
--dry-run prints the method, the masked URL, and the payload without
sending anything, which is enough to review the request in code review.
Success is the receiver accepting the request, and nothing more: the GitHub
route answers a bare OK with no build id. The newest build surfaces through
coprctl build list OWNER/PROJECT -n1 or the monitor; a pipeline that wants
the outcome polls there.
In GitHub Actions the trigger fires from the release workflow with the pre-release guard the forge cannot express:
on:
release:
types: [published]
jobs:
copr:
if: ${{ !github.event.release.prerelease }}
steps:
- name: Trigger Copr rebuild
env:
COPRCTL_WEBHOOK_SECRET: ${{ secrets.COPRCTL_WEBHOOK_SECRET }}
run: |
coprctl integration trigger OWNER/PROJECT/PKG --tag "${{ github.ref_name }}"
The secret in the receiver URL is the bearer credential for rebuilds of that
one package. It is never printed by trigger, rotated with integration rotate-secret like any other webhook setup, and the pipeline secret is then
the only value to update.
Managing the secret
The Copr webhook secret is a credential: it is never written into a manifest,
never in export output, and never printed without --reveal.
coprctl integration url OWNER/PROJECTprints the webhook URL with the secret masked.coprctl integration rotate-secret OWNER/PROJECT --yesgenerates a new secret and caches it in local state (mode 0600).
Rotation breaks every existing hook by design. Re-enable the hook after rotating so the forge side points at the new secret.
Template specs and Rust workspaces
Some projects keep a template spec such as packaging/rpm/foo.spec.in
instead of a plain .spec, with placeholders for the version and release.
Copr cannot consume that file directly; the project needs the make_srpm
source method, where the srpm target in .copr/Makefile renders the spec
before rpmbuild sees it.
coprctl detect recognizes this layout: the template appears in specs
with "template": true and method make_srpm, the proposal points spec
at the rendered basename, and a spec.makeSrpmTarget decision (resolved by
init --yes) records the missing Makefile. coprctl init writes both
copr.yaml and the scaffolded .copr/Makefile into the repository and
reports not-needed, exists:<path>, or wrote:<path> in its makefile
output key.
The scaffolded srpm target does three things:
- Derives the version from the pushed tag (
v1.2.3becomes1.2.3, with a0.prereleaserelease for tagged pre-releases). - Renders every
@TOKEN@placeholder found in the template (@RPM_VERSION@style tokens map to computed values; anything else becomes a make variable to pass on the command line) and packs the sources withgit archive. - Runs
cargo vendorand writes the.cargo/config.tomlsource override, so the spec'scargo build --locked --offlinestep needs no network. That step needs a committedCargo.lockand the network Copr provides at srpm time.
Copr invokes the target as make -f <repo>/.copr/Makefile srpm outdir=... spec=... from the cloned checkout, as root in a mock chroot
with network access, and picks the resulting SRPM up from outdir
afterwards (see the Copr user documentation, "Build Source Types"). The
scaffold renders SPEC_TEMPLATE and ignores the passed spec, which the
protocol permits. The target assumes committed state on a tagged checkout:
uncommitted work is excluded with a warning, and a missing tag needs an
explicit VERSION=x.y.z. Keep the chroot BuildRequires
(cargo >= <rust-version>) in step with the workspace rust-version.
Tag matching needs one extra look for these projects: release tags are
usually bare (v1.2.3) while the RPM package has its own name (foo-cli).
integration github enable already selects the package-scoped webhook URL
in that case, so the bare tag resolves to the right package. Branch pushes
stay quiet under the tag-only default.
Related
coprctl doctorreports connectivity and credential presence.- See the architecture page on the agent contract for how this surfaces to agents.