This repo contains RPM and Debian packaging metadata for building Percona software packages via a self-hosted OpenSUSE Build Service (OBS) instance. It does not contain upstream source code — only packaging files. Sources are fetched at build time by OBS services declared in obs/_service.
osc— the OBS CLI client (Python library, also used programmatically)percona-obs— the management script in this repo (seerequirements.txt)root/— all packaging content lives here, mirroring the OBS project/package hierarchy
root/
├── project.yaml # OBS project config for the root project
├── common/ # shared packages used across products
│ └── deps/
│ ├── build/ # build-time deps (Go toolchain, OBS source services)
│ │ └── <package>/
│ │ └── obs/_aggregate # aggregates from an external OBS project
│ └── runtime/ # runtime deps shared across products
│ └── <package>/ # e.g. percona-telemetry-agent
│ ├── debian/
│ ├── rpm/
│ └── obs/_service
└── <product>/ # e.g. ppg/
├── releases/ # release pointer files (see root/README.md)
│ └── <name>/release.yaml
├── staging/ # full package set, tag builds, QA/release candidate
│ └── <major-version>/ # e.g. 17/
│ ├── project.yaml # OBS project config for this subproject
│ ├── <package>/ # source packages
│ │ ├── debian/ # Debian packaging (control, rules, changelog, …)
│ │ ├── rpm/ # RPM packaging (*.spec, patches, service files)
│ │ ├── package.yaml # optional OBS package config (title, description)
│ │ └── obs/
│ │ ├── _service # OBS build service config
│ │ ├── _aggregate # aggregates binaries from another OBS project
│ │ └── _multibuild # multi-flavor builds (PostgreSQL extensions only)
│ └── <another-package>/
│ └── ...
└── devel/ # manually curated dev-branch subset (see below)
└── <major-version>/
└── <package>/ # Class A (full copy) or Class B (obs/_link only)
A directory is treated as a package if it contains an obs/ subdirectory or a package.yaml file. Everything else is treated as a project (subproject grouping).
devel/<V>/ holds a manually curated subset of staging/<V>/, built from development
branches so day-to-day branch work can be built and tested without disturbing staging.
Membership is manual — adding a package there also requires adding its direct dependents,
since an omitted dependent would otherwise link against staging binaries. A Class A
devel package is a full copy of the staging package (own rpm/, debian/, obs/) with
obs/_service retargeted from a release tag to a development branch, editable independently
of staging. A Class B devel package contains only an obs/_link pointing at the staging
package (e.g. <link project="${OBS_ROOTPRJ}:ppg:staging:18" package="…"/>), rebuilt in the
devel context so it links against devel binaries. See root/README.md for full detail.
- Single static package name (no version placeholder)
obs/_servicefetches: packaging (debian + rpm subdirs) + upstream source +go_modules(manual)debian/rulesextracts version from.obsinfofile at build time- RPM
Release: 1%{?dist}
- Uses
@BUILD_FLAVOR@placeholder throughout (replaced by PG major version at build time) obs/_multibuildlists PG versions to build for:<flavor>17</flavor>debian/pgversionsspecifies min PG version (e.g.,9.3+)- RPM spec defines
%define pg_version @BUILD_FLAVOR@%{nil}and uses%{pgrel}inName: - Built with PGXS:
USE_PGXS=1 make
obs/_service structure (all packages follow this pattern):
- First
obs_scmservice: fetchdebian/subdir from this repo — use<param name="subdir">${DEBIAN_PACKAGE_DIRECTORY}</param> - Second
obs_scmservice: fetchrpm/subdir from this repo — use<param name="subdir">${RPM_PACKAGE_DIRECTORY}</param> - Third
obs_scmservice: fetch upstream source from its canonical repo - Buildtime services:
tar,recompress(gz),set_version go_modules(manual mode) — only for Go projects (telemetry-agent, etcd)
debian/debian.dsc must list all tarballs in Debtransform-Files-Tar:
Debtransform-Files-Tar: debian.tar.gz vendor.tar.gz rpm.tar.gz
Maintainer (use consistently):
Percona Development Team <info@percona.com>(Debian)Percona LLC(RPM)
Epoch: 1 is set on PostgreSQL-related packages to allow version management.
Each project directory may contain a project.yaml that defines its OBS project metadata.
name: # optional — overrides the OBS project name (empty = use derived name)
title: My Project Title
description: "Human-readable description."
repositories:
- name: RockyLinux_9 # OBS repository name
paths:
- project: openSUSE.org:RockyLinux:9 # upstream OBS project providing the build environment
repository: standard
- subproject: builddep # relative reference: resolves to <rootprj>:builddep
repository: RockyLinux_9
archs: [x86_64]
project-config: | # raw OBS project config string
%if "%_repository" == "RockyLinux_9"
ExpandFlags: module:llvm-toolset-rhel9
%endifname— absent or empty means the OBS project name is derived from the directory path relative toroot/joined with--rootprjusing colons (e.g.home:Admin:ppg:staging:17). Set it explicitly only when the OBS project name must differ from the directory path.repositories[].paths— list of path entries providing the base build environment. Each entry uses eitherproject:(absolute OBS project name) orsubproject:(resolved as<rootprj>:<subproject>) plusrepository:.project-config— passed verbatim to the OBS project config API; used for RPM macros, module expansion flags, etc.titleanddescriptionare informational only and never inherited by child projects.
repositories and project-config are inherited from ancestor project.yaml files when absent or empty in a project's own file. The nearest ancestor that defines the field wins. title, description, and name are never inherited.
This means:
- The root
project.yamlacts as the default config for all subprojects. - A subproject only needs its own
project.yamlif it requires a different build environment. - An empty or missing
project.yamlin a subdirectory is valid — it will fully inherit from its parent.
When percona-obs pushes project metadata to OBS, it automatically injects one <path> entry per ancestor OBS project into every repository of every non-root subproject. This is done by build_project_meta() in percona-obs, using the _ancestor_projects() helper.
Ancestor paths are injected closest-first (immediate parent before grandparent), followed by the upstream path from project.yaml. This gives every subproject direct visibility into packages built in all ancestor projects, without relying on OBS transitive resolution.
For example, the home:Admin:ppg:staging:17 project gets this generated for each repository:
<repository name="RockyLinux_9">
<path project="home:Admin:ppg:staging" repository="RockyLinux_9"/> <!-- auto-injected: immediate parent -->
<path project="home:Admin:ppg" repository="RockyLinux_9"/> <!-- auto-injected: grandparent -->
<path project="home:Admin" repository="RockyLinux_9"/> <!-- auto-injected: great-grandparent (rootprj) -->
<path project="openSUSE.org:RockyLinux:9" repository="standard"/> <!-- from project.yaml -->
<arch>x86_64</arch>
</repository>The root project (matching --rootprj) never gets ancestor paths injected. Only non-root subprojects are affected.
Each package directory may contain a package.yaml with OBS package metadata:
title: My Package Title
description: "Human-readable description."These fields map directly to the OBS package <title> and <description> XML elements.
percona-obs is the management script for syncing local YAML configuration and packaging files to an OBS instance.
Global options:
# Using explicit flags (always works):
percona-obs -A <url> -R <rootprj> [--verbose] <command> ...
# Using a named profile (recommended for day-to-day use):
percona-obs -P <name> [--verbose] <command> ...
# -A / --apiurl OBS API URL (e.g. http://my-obs.local:8000)
# -R / --rootprj OBS root project (e.g. home:Admin)
# -P / --profile Load apiurl, rootprj, and env from .profile/<name>.yaml
# -e KEY:VALUE Define or override an env variable (repeatable; VALUE may be empty)
# --verbose Print debug-level log messages (API calls, unchanged items)-R / --rootprj is always required — either directly or via a profile. Explicit -A / -R / -e flags override the corresponding profile values when both are given.
OBS credentials are read from ~/.config/osc/oscrc (created by osc's first-run wizard).
Profiles store per-environment OBS connection settings in .profile/<name>.yaml (git-ignored). Create one file per environment; use -P <name> to activate it.
File format (.profile/<name>.yaml):
apiurl: http://192.168.1.103:3000 # OBS API URL
rootprj: home:Admin:percona # OBS root project
env: # optional: variables for ${VAR} substitution
- name: REMOTE_OBS_ORG_INTERCONNECT
value: 'openSUSE.org:' # values containing colons must be quotedExample — create a dev profile and use it:
./percona-obs -A http://192.168.1.103:3000 -R home:Admin:percona \
-e REMOTE_OBS_ORG_INTERCONNECT:'openSUSE.org:' \
profile create dev
./percona-obs -P dev sync ppg:staging:17 etcd --dry-runTo add or update an env variable in an existing profile, use -P (to load the current state) plus -e:
./percona-obs -P dev -e ANOTHER_VAR:value profile create devIf the named profile file does not exist, percona-obs exits with an error listing the profiles that are available in .profile/.
${VAR} tokens in the following files under root/ are substituted with values from the active profile's env section (or -e flags) before the content is used or uploaded to OBS:
project.yamlandpackage.yaml— project/package metadataobs/_service,obs/_aggregate,obs/_link— OBS source files
This lets a single source tree target different OBS environments. For example, a local OBS instance interconnected to build.opensuse.org needs an openSUSE.org: prefix on external project references, while the public OBS does not:
# root/project.yaml
repositories:
- name: RockyLinux_9
paths:
- project: ${REMOTE_OBS_ORG_INTERCONNECT}RockyLinux:9
repository: standard# .profile/dev.yaml (local OBS with interconnect)
env:
- name: REMOTE_OBS_ORG_INTERCONNECT
value: 'openSUSE.org:'
# .profile/prod.yaml (public build.opensuse.org — no prefix needed)
env:
- name: REMOTE_OBS_ORG_INTERCONNECT
value: ''Use project verify -P <profile> to validate that all ${VAR} tokens in the tree are defined in the given profile.
percona-obs prints one line per resource, always — including unchanged ones. Each line has a two-character prefix, color-coded when stdout is a TTY (set NO_COLOR=1 to disable):
| Prefix | Color | Meaning |
|---|---|---|
+ |
green | Resource created on OBS (did not exist before) |
~ |
yellow | Resource updated on OBS (existed, content changed) |
= |
dim | Resource unchanged (OBS already matches desired state) |
- |
red | Resource deleted from OBS (orphan cleanup or sync delete) |
@ |
cyan | Package aggregated from a branch source (--branch-from) |
! |
yellow | Uncertain — OBS-only file skipped because services were not run (dry-run only) |
> |
cyan | Action taken: local service run or OBS service triggered |
✔ |
bold green | Command completed successfully |
· |
dim | Debug message (only shown with --verbose) |
In dry-run mode the same +/~/=/-/! symbols are used — the (dry run) note on the final ✔ line indicates nothing was written.
sync compares the desired state against what OBS currently holds before making any write call:
- Project / package meta — the managed fields (title, description, repositories) are compared as XML; OBS-managed fields (ACL entries, person/group/lock) are ignored.
- Project config — the raw string is compared after stripping leading/trailing whitespace.
obs/files — each file's MD5 is compared to the MD5 returned by the OBS source directory listing. Only changed files are uploaded. Files present on OBS but absent locally are deleted. All uploads and deletions are committed as a single OBS source revision.
Every resource is always printed with its status (+/~/=/-). The = line is printed even when nothing changed. Use --force to bypass comparison and always write.
sync push [--force] [--dry-run] [--no-services] [--no-cache] [--non-recursive] [--project-only] [--branch-from PROFILE] [--skip-unchanged] [--report-json PATH] [-m MSG] [project] [package]
Syncs local packaging files to OBS. For each target package, all ancestor projects (from root down) are created/updated first, then the package meta is applied, then source files are synced as a single OBS source revision. Services are run locally to produce the upstream source tarball and packaging artifacts; the _service file is not uploaded to OBS.
| Call form | Effect |
|---|---|
sync push |
Sync all packages under root/ |
sync push <project> |
Sync all packages under the project (recursively) |
sync push <top-level-package> |
Sync a single package directly under root/ |
sync push <project> <package> |
Sync a single package under the project |
Options:
--force— bypass OBS conflict checks; always write meta and files regardless of diff.--dry-run— run local services and report what would be uploaded to OBS without writing. All OBS writes are skipped but the+/~/=/-output reflects what would change.--no-services— skip local service execution; uploadobs/as-is.--no-cache— bypass both cache levels; always run obs_scm and manual services from scratch.--non-recursive— only sync packages directly under the specified project; do not descend into sub-projects.--project-only— only sync project configuration (meta and build config); skip all package syncing.--branch-from PROFILE— for each package unchanged since the given profile's last sync, upload only an_aggregatefile that reuses pre-built binaries from that profile's OBS project instead of uploading sources. The branch profile may target a different OBS instance. After the initial changed/unchanged classification, a second phase queries OBS_builddepinfoand automatically promotes any additional packages whose build dependencies or dependents were promoted (bidirectional fixed-point propagation). The aggregate message format isbranch: <profile> (<source_project>/<package>).--skip-unchanged— plain pushes only (rejected with--branch-from): skip packages whose OBS revision comment records a clean sync from a git SHA with no changes since (package-directory commits, uncommitted edits, rendered values of the macros the package references). One API call per skipped package, or zero when the.cache/sync_state/manifest is warm. Packages whose_servicehas an upstream obs_scm tracking a moving ref (branch or no revision) are never skipped;--forcedisables skipping entirely. See "Reducing OBS API traffic" indocs/PERCONA_OBS_TOOL.md.--report-json PATH— write a JSON sync report (rebuild_projects,promoted,skipped,head_sha) consumed by the CI poll script viaOBS_SYNC_REPORTto scope build monitoring to the projects the sync actually touched.-m MSG/--message MSG— commit message recorded in the OBS source revision. When omitted, a message is generated automatically:sync: <branch>@<short-sha> (<remote_url> or <hostname>)
When --branch-from <profile> is given, each package is individually evaluated: either an _aggregate is uploaded (reusing pre-built binaries from the branch profile's OBS project) or sources are uploaded normally. The decision is made by _resolve_branch_decision.
The corresponding branch OBS project is derived by substituting the current rootprj prefix with the branch profile's rootprj. For example, if the current project is home:Admin:percona-test:ppg:staging:17 and the branch rootprj is home:Admin:percona, the branch project is home:Admin:percona:ppg:staging:17.
- Fetch the latest source revision comment from the branch OBS project for this package (
GET /source/<branch_project>/<package>/_history). - Match it against the sync message pattern
sync: <branch>@<sha> (<detail>). - If the message matches and the detail does not start with
"local changes on"(i.e. was synced from a pushed branch):- Call
git logto check whether any commits touching<package_path>exist since<sha>. - Check for uncommitted edits inside
<package_path>. - Check whether any macro the package references (
%!{NAME}tokens in its obs/, debian/, rpm/ files) renders to a different value now than at<sha>(_macros_changed_since). The comparison is by rendered value, not by whichmacros.yamlfiles were touched: moving a macro between ancestor files with the same value, or bumping a macro this package never uses, is not a change. - Nothing changed → aggregate (package unchanged). Otherwise → fall back to the content check.
- Call
The content check is used when the revision comment cannot be trusted:
- No comment on the branch (new project, never synced)
- Comment doesn't match the
sync:format (e.g. manual commit, older format) - Sync message says
"local changes on <hostname>"(HEAD was not pushed to any remote) - The SHA is valid but something feeding the upload changed since it (see above)
If the local service run needed to build the comparison fails (e.g. a transient obs_scm clone error), it is retried once; a second failure aborts the whole sync with the service's error. A failed run says nothing about whether the content matches, so it is never reported as "content differs" — that would promote an unchanged package.
Content check (_content_matches_branch) performs two sub-checks:
Sub-check 1 — File MD5 comparison
Fetch the expanded file list from OBS (GET /source/<branch_project>/<package>?expand=1). The expand=1 parameter is required to see service-generated files (e.g. _service:obs_scm:*.obsinfo, .obscpio) that OBS stores server-side. For every file in the local obs/ directory, compare its MD5 against the OBS-returned MD5. If any file differs or is missing from OBS, the check fails (→ upload sources).
Sub-check 2 — Upstream obs_scm commit hash
If a _service file exists, extract the upstream obs_scm service — the one that fetches the actual software source. Packaging obs_scm services (whose subdir param matches root/.+/(debian|rpm)$ or equals ${DEBIAN_PACKAGE_DIRECTORY} / ${RPM_PACKAGE_DIRECTORY}) are excluded. If exactly one upstream obs_scm remains:
- Resolve the remote HEAD SHA using
git ls-remote --(30 s timeout), tryingrefs/heads/<revision>, thenrefs/tags/<revision>^{}(annotated tag), thenrefs/tags/<revision>. - If resolution fails, treat the package as changed (conservative: cannot verify).
- Find the obsinfo file on OBS by looking for a name that starts with
<filename_prefix>or_service:obs_scm:<filename_prefix>and ends with.obsinfo. OBS stores server-side service outputs with a_service:<name>:prefix; both forms are checked. - Fetch the obsinfo content with
?expand=1and parse thecommit:line. - If
obs_commit != remote_head_sha→ changed (upstream has moved). If equal → unchanged.
If zero or more than one upstream obs_scm services are found, sub-check 2 is skipped and the MD5 match alone is sufficient.
After Phase 1 classifies every package as "aggregate", "skip_branch", or
"promote", Phase 2 enforces build dependency correctness by promoting any package
whose build dependencies or dependents have been promoted.
Why this is necessary: if package A (e.g. golang-1.25) has local changes and
is promoted, packages that build-depend on A (e.g. percona-telemetry-agent, etcd)
must also be promoted — otherwise they would link against the old branch binaries and
not the new A. Conversely, packages that A depends on are also promoted so A builds
against locally-controlled sources rather than the branch copy.
How it works:
- Determine which OBS projects to query for
_builddepinfo:- With
--branch-from: query the branch OBS (branch_apiurl) for all branch projects derived from every package in scope (not just the ones with"aggregate"decisions), including e.g.<branch_rootprj>:builddep. - Without
--branch-from(plain push over a previously branched env): query the target OBS (apiurl) for the union of target projects and any source projects recorded frombranch:revision comments (branch_project_for.values()).
- With
- Call
_fetch_combined_depinfo(dep_apiurl, dep_projects, local_pkg_names)to build:providers_by_project: per-project map of binary package name → source OBS package (multibuild:flavorsuffixes are stripped from source names at construction time); each consumer's dep is resolved own-project-first, then via the queried repo's<path>chain.fwd_deps[A]: set of local packages that A build-depends on.
- Run fixed-point bidirectional propagation until stable:
- Forward: if B is in
fwd_deps[A]and B is promoted → promote A. - Backward: if A is promoted and B is in
fwd_deps[A]→ promote B.
- Forward: if B is in
- All packages whose decision was changed to
"promote"by Phase 2 log a message indicating which dep triggered them.
After dependency propagation, sync push --branch-from checks whether each in-scope project's
desired metadata (repositories, build flags, project-config) differs from what is currently on
OBS — and promotes all packages in that project if a difference is detected.
Why this is necessary: a PR may change only root/project.yaml (e.g. adding a new architecture
like aarch64). No package files change, so Phase 1 assigns "aggregate" to everything and Phase 2
propagates nothing. Phase 2.5 detects the config change and upgrades every package in the affected
projects to "promote", causing them to be built from source on the new architecture.
How it works:
-
For every project in the PR namespace, call
check_project_config_changed(apiurl, pr_project, ...). This builds the locally-desired project meta XML and compares it against the live OBS meta. -
The function returns
(changed: bool, is_new: bool).is_new=False, changed=True→ project already existed on OBS and its config changed → add toconfig_changed_projectsand promote all packages.is_new=True(PR project does not yet exist) → fallback comparison:- Derive the corresponding production project name (the branch source project under
branch_rootprj). - Call
check_project_config_changed(branch_apiurl, prod_project, ...)to compare the local desired config against what the production project currently has on the branch OBS. - If
not is_new and changedin that comparison (production exists and its config differs from local) → return(changed=True, is_new=False)so the project entersconfig_changed_projects.
- Derive the corresponding production project name (the branch source project under
- Otherwise → no config-triggered promotion for this project.
-
Projects in
config_changed_projectsforce all their packages to"promote", and those projects are included inactive_projectsso they are created on the PR OBS.
Why the fallback is safe: for a re-sync of an existing PR project is_new=False, so the
fallback is never reached. For a new PR where no inherited config changed, the fallback compares
local vs production and finds them equal → no spurious promotions. Only when an arch (or other
config field) was genuinely added does the fallback produce a promotion trigger.
When running sync push without --branch-from (i.e. a full source sync), but the package on OBS already holds a branch: aggregate from a previous --branch-from run, uploading sources would overwrite the aggregate unnecessarily. To detect this:
- Fetch the latest revision comment.
- If it matches
branch: <profile> (<source_project>/<package>), extract<source_project>. - Run the content check (
_content_matches_branch) against that source project. - If the content matches → print
= files ...and skip the upload. If not → proceed with the normal source upload.
For packages with an obs/_multibuild file, the _aggregate XML must list every flavored OBS package name separately. _multibuild_packages(obs_dir, base_name) reads the <flavor> elements and checks the buildemptyflavor attribute (default: true). When buildemptyflavor is absent or "true", the bare package name is included in addition to <base_name>:<flavor> entries. The _aggregate output format is:
<aggregatelist>
<aggregate project="<branch_project>">
<package>percona-pg-telemetry:17</package>
<!-- <package>percona-pg-telemetry</package> only if buildemptyflavor != false -->
</aggregate>
</aggregatelist>The revision message recorded for the aggregate commit is branch: <profile> (<branch_project>/<package>).
Deletes OBS projects (and their sub-projects) or a single package created by sync push.
| Call form | Effect |
|---|---|
sync delete |
Delete the full project tree under rootprj (deepest sub-projects first) |
sync delete <project> |
Delete a project and all its sub-projects |
sync delete <project> <package> |
Delete a single package |
Options:
--yes/-y— skip the confirmation prompt.--recursive— delete projects that still contain packages (passes OBSrecursiveflag). Without this flag, a project with packages will fail with a hint to add--recursive.--dry-run— show what would be deleted without making any changes.
Projects that do not exist on OBS are silently skipped. Projects are always deleted with force=True to bypass inter-project repository dependency checks when removing a whole tree.
No workflow invokes this command anymore — obs-pr-cleanup.yml deletes every closed
PR's OBS project directly without releasing its binaries first. sync release-pr remains
available as a manual/recovery tool only.
Copies binaries from a PR OBS project to the corresponding production project using osc release,
then updates the production project configs to match the local desired state. Historically used to
merge a non-release PR (one that does not create a version tag) once its OBS builds pass.
How it works:
-
Walk the PR project tree (under
rootprj) and callosc releasefor every package in every PR sub-project that exists on OBS.osc releasecopies binary packages from the PR project into the production project (identified by the<releasetarget>in each repository's OBS meta). -
After releasing, discover which production projects were targeted by reading the
<releasetarget project="...">elements from each PR project's OBS meta. -
For each discovered production project, call
_apply_project_config()to sync the locally desired project meta to the production OBS — this propagates any config changes (e.g. a newly added architecture) that were part of the PR but are not yet in the production project. Theenv_varsdict for this step is seeded from the active profile env overrides (so${REMOTE_OBS_ORG_INTERCONNECT}and similar vars are available) plusOBS_ROOTPRJ=<production_rootprj>.
Important: sync release-pr only releases binaries; it does not delete the PR project. Run
sync delete --yes --recursive afterward to clean up.
Options:
--yes/-y— skip the confirmation prompt.
Promotes branch packages (created by a prior --branch-from sync) back to full source syncs. For each targeted package whose latest OBS revision comment matches the branch: pattern, the _aggregate is replaced with the local obs/ source files (running any mode="manual" services as needed). Packages that already hold real sources are skipped (= output).
| Call form | Effect |
|---|---|
sync promote |
Promote all branch packages under rootprj |
sync promote <project> |
Promote all branch packages under the project |
sync promote <project> <package> |
Promote a single package |
Detection: reads the latest OBS revision comment via _fetch_obs_package_latest_comment; if it matches _BRANCH_MSG_RE (^branch: \S+ \((.+)/[^/]+\)$), the package is a branch and will be promoted. Packages without an obs/ directory are silently skipped.
Options:
--dry-run— show what would be promoted without writing to OBS. Services are not run in dry-run mode.--no-services— uploadobs/files as-is without running manual services.--no-cache— disable the service artifact cache.-m/--message— OBS revision commit message (defaults to the standard sync message).
If a package's obs/_service contains any service with mode="manual", sync automatically runs all non-buildtime services locally before uploading. This is required for packages like Go services that use go_modules (mode=manual) to vendor dependencies.
Execution order and file handling:
- All services with
modenot in{buildtime, serveronly, disabled}are run in XML declaration order. - Each service binary is invoked from
/usr/lib/obs/service/<name>with its<param>values and--outdir. - Service outputs are merged into a shared work directory so later services can consume earlier outputs (e.g.
go_modulesconsumingobs_scmtarballs). - Only files produced by
mode="manual"services are committed to OBS. Files produced by no-mode services (e.g. obs_scm source tarballs) are used locally but not uploaded — OBS regenerates those on its server.
If a service binary is missing from /usr/lib/obs/service/, a warning is logged and the service is skipped. A non-zero service exit code aborts the entire sync run.
To avoid re-running expensive operations (git clones, Go/Rust dependency vendoring, tarball downloads) on every sync, percona-obs maintains a four-level on-disk cache at .cache/ in the project root (git-ignored via .gitignore).
Level 1 — obs_scm output cache (.cache/obs_scm/{params_hash}/{head_sha}/)
Before invoking each obs_scm service binary, percona-obs:
- Computes
params_hashas the SHA256 of all sortedname=valueparam pairs from the service XML element. Any change to the service config (URL, revision, extract pattern, etc.) produces a different key. - Calls
git ls-remote(30 s timeout) to resolve the remote revision to a commit SHA (head_sha), trying in order:refs/heads/<revision>,refs/tags/<revision>^{}(annotated tag, peeled to commit),refs/tags/<revision>. - Checks
.cache/obs_scm/{params_hash}/{head_sha}/. On a hit, all cached files are restored to the work directory and obs_scm is skipped entirely. On a miss, obs_scm runs normally and its output files (.obsinfo,.obscpio,.dsc, etc.) are stored atomically to.cache/obs_scm/{params_hash}/{head_sha}/.
If git ls-remote fails or times out, obs_scm always runs and its output is not stored.
Level 2 — manual service output cache (.cache/services/{upstream_commit}/)
After Phase 1 completes, percona-obs identifies the upstream source obs_scm service — the one that fetches the actual software being packaged — by filtering out every obs_scm whose subdir param matches root/.+/(debian|rpm)$ or equals ${DEBIAN_PACKAGE_DIRECTORY} / ${RPM_PACKAGE_DIRECTORY} (those fetch packaging files from this repo). Exactly one service must remain; zero or two or more trigger a warning and the cache is skipped.
The obsinfo file produced by that upstream obs_scm is named {filename}.obsinfo (where filename is the service's filename param, e.g. etcd.obsinfo). Its commit: field — the HEAD commit of the upstream repo at fetch time — is used as the cache key.
- Cache hit:
.cache/services/{upstream_commit}/exists and contains files → those files (vendor tarballs, etc.) are copied to the work directory, allmode="manual"services are skipped, and the function returns immediately. - Cache miss: all
mode="manual"services run in XML-declaration order, then their output files are stored atomically to.cache/services/{upstream_commit}/.
Level 3 — download_url output cache (.cache/download_url/{params_hash}/)
Before invoking each download_url service binary, percona-obs computes params_hash as the SHA256 of all sorted name=value param pairs from the service XML element (after macro/env substitution). Since the URL fully determines the downloaded content for the versioned artifacts these services fetch, no remote check is performed. On a hit, the cached files are restored to the work directory and the download is skipped. On a miss, download_url runs normally and its output files are stored atomically to .cache/download_url/{params_hash}/.
Level 4 — cargo_vendor output cache (.cache/cargo_vendor/{params_hash}/{source_id}/)
cargo_vendor is declared mode="buildtime" but is not a fast local transform — it downloads the full crate dependency tree from crates.io. Its output (vendor.tar.gz) is cached, keyed on params_hash (SHA256 of the service params) plus source_id, which identifies the exact source being vendored: the upstream obs_scm commit hash when the package has an obs_scm service, otherwise the SHA256 of the resolved src archive(s) themselves (for sources fetched via download_url). Any upstream commit or source change therefore produces a new key and invalidates the cache; on store, entries for older revisions of the same service are pruned (vendor tarballs are large). If no source_id can be determined, cargo_vendor runs uncached.
Atomic writes: all levels write to a temporary directory inside the cache directory (ensuring same filesystem), then rename it into place, preventing partial or corrupt cache entries.
--no-cache: pass to sync to bypass all cache levels unconditionally for that run.
When targeting a specific package (sync <project> <package>), the ancestor project chain is only walked if the target project does not yet exist on OBS (fast path avoids redundant GET calls otherwise).
Project names use colon notation matching the directory hierarchy (e.g. ppg:staging:17).
Triggers an OBS service run (runservice) for one or more packages, causing OBS to re-fetch sources and rebuild.
| Call form | Effect |
|---|---|
build trigger |
Trigger services for all packages under root/ |
build trigger <project> |
Trigger services for all packages under the project |
build trigger <top-level-package> |
Trigger service for a single top-level package |
build trigger <project> <package> |
Trigger service for a single package under the project |
Prints a color-coded tree of live build statuses fetched from OBS. For each repository where a package has succeeded, the built version (e.g. 3.5.26-6.1) is shown after the status symbol, parsed from the binary package filename.
| Call form | Effect |
|---|---|
build status |
Status for all packages under root/ |
build status <project> |
Status for all packages under the project (tree rooted there) |
build status <top-level-package> |
Status for a single top-level package |
build status <project> <package> |
Status for a single package |
Status symbols (color output disabled with NO_COLOR=1):
| Symbol | Color | OBS status codes |
|---|---|---|
✔ |
green | succeeded |
✗ |
red | failed / unresolvable / broken |
● |
cyan | building / dispatching |
◌ |
yellow | scheduled / blocked |
– |
dim | excluded / disabled |
? |
dim | unknown or any unrecognised code |
For multibuild packages, when all flavors of a repository share the same status the flavor tags are shown inline (e.g. [:17]). When flavors differ, each expands to its own sub-line under the repository.
When multiple architectures are configured for the same repository, the highest-priority (most actionable) status is kept per flavor; arch details are not shown.
Queries OBS _builddepinfo for all packages in scope and prints a build dependency
tree. Packages are grouped by root packages (packages that no other local package
depends on). Each root package is a tree root; its direct and transitive build
dependencies are indented beneath it with box-drawing characters.
| Call form | Effect |
|---|---|
build dependency |
Dependency tree for all packages under root/ |
build dependency <project> |
Restrict to packages under the given project |
Output format: each line is <pkg> (<obs_project>). Root packages (tree roots) are
printed in bold. Packages with no local dependencies and nothing depending on them are
listed after all trees as isolated packages. Cycles are detected and printed as
(cycle) leaf nodes.
Implementation (cmd_build_dependency in cmd_build.py):
- Scan all packages under scope with
find_packages. - Collect all OBS project names from those packages.
- Call
_fetch_combined_depinfo(apiurl, dep_projects, local_pkg_names)to buildfwd_deps(source package → set of local packages it depends on), resolving each binary to its provider project-aware (own project, then repo path chain). - Identify root packages: any package not present in any
fwd_depsvalue set. - Print trees with
_print_dep_tree(), then isolated packages (no deps, not depended on by anything).
Validates local project configuration without connecting to OBS.
The optional project argument (colon notation, e.g. ppg:staging:17) restricts validation to that subtree. If omitted, the entire root/ tree is validated.
Check 1 — subproject references: every subproject: entry in all project.yaml files within the scope must resolve to an existing directory under root/.
Check 2 — env variable coverage: every ${VAR} token found in project.yaml, package.yaml, and obs/_service / obs/_aggregate / obs/_link files within the scope must be defined in the active env.
Env resolution for the check (same precedence as all other commands):
- Profile env (
-P <profile>) provides the base values. -e KEY:VALUEflags override or supplement individual variables.- With no profile and no
-eflags, any${VAR}token found is an error with a hint to supply a profile.
# Validate the entire tree against the dev profile
./percona-obs -P dev project verify
# Validate only the ppg:staging:17 subproject
./percona-obs -P dev project verify ppg:staging:17
# Check with an inline override (no profile file needed)
./percona-obs -e REMOTE_OBS_ORG_INTERCONNECT:'openSUSE.org:' project verifyExit code is 0 on success, 1 if any check fails.
Release changelogs live at root/ppg/releases/<major>/CHANGELOG.md and follow Keep a Changelog format.
Every changelog entry that references an upstream version must include a URL to the official upstream release notes or changelog. When adding or fixing entries:
- Always verify the URL exists — search the internet for the correct upstream release notes page. Do not guess or use a generic repo URL.
- Follow the established URL pattern per package (see table below). Adapt the pattern to the new version rather than copying a raw git URL or a template artifact.
- Template artifacts (
%!{VAR}) are build-time errors left by the changelog generator — always replace them with the resolved URL. - If no established pattern exists for a package, search for its official release notes page and document the pattern in the table below.
| Package | URL pattern | Example |
|---|---|---|
percona-postgresql |
https://www.postgresql.org/docs/release/<version>/ |
…/release/17.10/ |
etcd |
https://github.com/etcd-io/etcd/releases/tag/v<version> |
…/tag/v3.5.30 |
percona-haproxy |
https://www.haproxy.org/download/<major.minor>/src/CHANGELOG |
…/2.8/src/CHANGELOG |
percona-patroni |
https://github.com/zalando/patroni/releases/tag/v<version> |
…/tag/v4.1.3 |
percona-pg_gather |
https://github.com/jobinau/pg_gather/releases/tag/v<version> |
…/tag/v33 |
percona-pg_tde |
https://github.com/percona/pg_tde/releases/tag/<version> |
…/tag/2.2.0 |
percona-pgbouncer |
https://github.com/pgbouncer/pgbouncer/releases/tag/pgbouncer_<version_with_underscores> |
…/pgbouncer_1_25_2 |
percona-pgpool-II |
https://www.pgpool.net/docs/<major.minor>/en/html/release-<version-with-dashes>.html |
…/release-4-7-1.html |
percona-postgis |
https://github.com/postgis/postgis/blob/<version>/NEWS |
…/blob/3.5.6/NEWS |
percona-postgresql-common |
https://salsa.debian.org/postgresql/postgresql-common/-/tags/debian%2F<version> |
…/debian%2F290 |
percona-telemetry-agent |
https://github.com/percona/telemetry-agent/releases/tag/v<version> |
…/tag/v1.0.13 |
- Copy
ppg/staging/17/percona-pg-telemetry/as a template - Replace all
percona-pg-telemetryreferences with the new package name - Update
obs/_multibuildflavors for the target PG versions - Update
obs/_serviceupstream URL to point to the new package's GitHub repo - Update
rpm/*.spec— preserve@BUILD_FLAVOR@inName:and%define pg_version - Update
debian/control— keep@BUILD_FLAVOR@inPackage:and version-specificDepends:
- Copy
percona-telemetry-agent/as a template - Update
obs/_serviceupstream URL; keepgo_modulesservice in manual mode debian/rulesversion extraction pattern reads/usr/src/packages/SOURCES/*.obsinfo- Ensure
vendor.tar.gzis listed indebian/debian.dsc'sDebtransform-Files-Tar
When given an OBS package URL and a target location within root/, follow these steps. The user may request either a full source import (copy all files from OBS) or an aggregate import (create an _aggregate link so the local OBS pulls built packages from the source project). Use the mode explicitly requested; default to full source import if not specified.
- OBS package URL — the web UI URL, e.g.
http://192.168.1.103:3000/package/show/home:Admin/obs-service-tar_scm - Target location — directory relative to
root/where the package should land (e.g.root/for a top-level package,root/ppg/staging/17/for a subproject package) - Import mode —
full(copy source files) oraggregate(create_aggregatelink)
The web UI URL and API URL are not always the same host:
| Web UI host | API host to use |
|---|---|
build.opensuse.org |
api.opensuse.org |
| Any other host | same host as the web UI |
The path format /package/show/<project>/<package> always identifies the OBS project and package name regardless of which host is used.
osc -A <apiurl> api /source/<obs_project>/<package_name>/_metaExtract <title> and <description>. Treat a description containing only whitespace as empty.
mkdir -p root/<target>/<package_name>/obsList files:
osc -A <apiurl> api /source/<obs_project>/<package_name>Download each <entry name="...">:
osc -A <apiurl> api /source/<obs_project>/<package_name>/<filename>Place every file directly in obs/. Do not split into debian/ or rpm/ — that is a separate step if desired.
Create obs/_aggregate pointing to the source project. When the source is on a remote OBS instance, prefix the project name with the instance identifier:
| Source OBS instance | Project name in _aggregate |
|---|---|
build.opensuse.org / api.opensuse.org |
openSUSE.org:<obs_project> (e.g. openSUSE.org:openSUSE:Tools) |
Local OBS (192.168.1.103) |
Use the project name as-is (e.g. home:Admin) |
<aggregatelist>
<aggregate project="<mapped_project>">
<package><package_name></package>
</aggregate>
</aggregatelist>title: "..." # quote if the value contains a colon
description: |
<description from _meta, reflowed to ~80 chars per line>Omit package.yaml entirely if both <title> and <description> are empty.
Always quote the title value with double quotes if it contains a colon (:) — YAML treats an unquoted colon as a mapping separator and will fail to parse.
- Use the OBS package name unchanged as the local directory name.
- Do not run
blackorpyright— no Python code is modified. - After creating the files, verify with
find root/<package_name> -type f | sort.
Source code reference: /home/rdias/Work/open-build-service/ — key files: src/api/app/models/branch_package.rb, src/api/app/controllers/source_package_command_controller.rb, src/backend/BSSrcServer/Link.pm, src/backend/BSSched/BuildJob/Aggregate.pm.
| Mechanism | Creates _link? |
Independent? | Follows devel chain? | Use case |
|---|---|---|---|---|
_link file |
yes (manually) | no | no | overlay/patch tracking |
cmd=branch |
yes (auto) | no | yes | developer workflow |
cmd=fork |
no (scmsync) | yes (git) | yes | SCM-based development |
cmd=copy |
no | yes | no | release, snapshot |
cmd=linktobranch |
transforms | partially | no | patch a linked package |
_aggregate |
no | n/a | no | binary reuse across projects |
A _link XML file inside a package makes it inherit sources from another package. The backend (BSSrcServer/Link.pm) resolves the link chain at build time — it fetches the origin's files, applies any local overlays, and presents the merged filelist to the build system. Links can chain. rev/srcmd5 can pin a specific revision.
<link project="BaseProject" package="mypackage" rev="abc123"/>POST /source/<project>/<package>?cmd=branch&target_project=<tgt>
The main "developer branch" operation (BranchPackage in branch_package.rb):
- Creates a branch project (e.g.
home:user:branches:BaseProject) - Creates a package in it with a
_linkpointing back to the source - Optionally follows the devel project chain (
devel:pointer on the package) - Optionally follows the update project chain (
OBS:UpdateProjectattribute) - Copies repositories from the source project
Resolution order:
1) BaseProject ← 2) UpdateProject ← 3) DevelProject/Package
X) BranchProject ← branch targets here
Key parameters: maintenance=1, newinstance=1 (copy instead of link), ignoredevel=1, missingok=1, dryrun=1.
POST /source/<project>/<package>?cmd=fork&scmsync=<url>
Variant of branch for scmsync (Git-managed) packages. Creates a new package with its own scmsync URL pointing to a forked repo. Same BranchPackage code path but skips all source link operations.
POST /source/<project>/<package>?cmd=copy&oproject=<src>&opackage=<src_pkg>
A complete, independent copy of source files — no _link. The new package is fully independent of the origin. Used for releases, snapshots, and starting a new independent package from an existing one. Key options: keeplink=1, expand=1, repairlink=1, withvrev=1.
POST /source/<project>/<package>?cmd=linktobranch
Converts an existing _link package into a proper branch (expands the link, stores real files, keeps the link with a baserev). Useful when you need to make actual changes to a linked package.
A special package type (_aggregate XML file) that pulls built binaries (not sources) from another project's repository into the current one. Handled by BSSched/BuildJob/Aggregate.pm. No source link involved — the binaries are made available as if they were built locally.
import osc.core
osc.core.branch_pkg(apiurl, src_project, src_package, ...) # cmd=branch
osc.core.copy_pac(src_apiurl, src_project, src_package, ...) # cmd=copy
osc.core.link_to_branch(apiurl, project, package) # cmd=linktobranch# Check out a package from OBS
osc co <project> <package>
# Sync local files into the OBS checkout, then commit
cp -r obs/* <checkout>/
osc add <new-files>
osc ci -m "update _service"
# Trigger a remote rebuild
osc rebuild <project> <package>
# Follow build log
osc buildlog <project> <package> <repo> <arch>When investigating a bug or unexpected behaviour in percona-obs, use the following iterative process.
Always start with a fully-verbose, non-destructive run:
./percona-obs --verbose -P <profile> sync push [args] --dry-run--verboseenablesDEBUG-level log messages (prefixed·), showing every OBS API call, unchanged-item decisions, dep-promotion steps, and cache hits/misses.--dry-runruns local services and reports what would change on OBS without writing anything.
For build dependency, which has no dry-run flag, just run it directly against the test profile:
./percona-obs --verbose -P <profile> build dependencyKey things to look for in verbose output:
| Verbose line | What it tells you |
|---|---|
planning: checking sync decisions |
Phase 1 started |
branch decision: git-log <pkg> (…) |
Git-based unchanged check for a package |
branch decision: content check <pkg> (…) |
MD5/obsinfo fallback check for a package |
branch decision: aggregate <pkg> (content matches) |
Package was classified as aggregate |
content check: service run failed, retrying once <pkg> |
(warning) a local service failed during the content check; a second failure aborts the sync instead of promoting |
planning: checking build dependencies (N project(s)) |
Phase 2 started; N = number of projects queried |
dep-promote: builddepinfo covers N local packages |
How many packages the dep query returned data for |
dep-promote: <pkg> promoted by dep on <other> |
A package was cascade-promoted by Phase 2 |
_resolve_provider: ambiguous providers for <binary> |
A dep edge was dropped: multiple projects build the binary and none is in the consumer's repo path chain |
fetching revision history: <project>/<pkg> |
OBS revision comment lookup (build dependency / Phase 1 plain-push) |
If dep-promote: builddepinfo covers 0 local packages appears, the dep query returned nothing — likely querying the wrong OBS instance or querying projects with no build results yet.
Dep edges attribute each binary to a provider by searching the consumer's own
project first, then the <path> projects of the queried repository (fetched
from the project _meta), then falling back to the unique provider across all
queried projects. Same-named binaries in sibling tiers (devel vs staging of
the same PG major) are therefore never conflated.
When verbose output is insufficient, add temporary debug logging to the relevant function. The logger instance is available in every module via from .common import logger. Example:
logger.debug(f"dep-promote: dep_projects={dep_projects!r}, dep_apiurl={dep_apiurl!r}")
logger.debug(f"branch decision raw comment: {prior_comment!r}")Run again with --verbose to see the new output. Remove the temporary lines once the root cause is found.
The revision comment on a package is the key data used by Phase 1 and build dependency to detect aggregates. Inspect it with:
osc -A <apiurl> api /source/<project>/<package>/_history | tail -20The comment format must match one of these patterns (see _SYNC_MSG_RE / _BRANCH_MSG_RE):
sync: <branch>@<sha> (<detail>)— normal source syncsync: <branch>@<sha> (local changes on <hostname>)— dirty syncbranch: <profile> (<source_project>/<package>)— aggregate from--branch-from
If the comment doesn't match either pattern, percona-obs treats the package as changed (promotes it).
When Phase 2 or build dependency is not finding expected deps, verify what OBS actually returns:
osc -A <apiurl> api /build/<project>/_builddepinfoKey things to check:
- Is the project listed at all? (It won't be if all packages are aggregates —
_builddepinfoonly covers packages with real build results.) - Are the package names in
<package name="…">matching what you expect? Multibuild packages appear aspkg:flavor— the code strips the:flavorsuffix. - Do the
<pkgdep>entries resolve to binary names that appear as<subpkg>in another package's entry?
When branching is involved, always confirm which OBS instance is being queried:
sync push --branch-from <profile>: Phase 2 always queriesbranch_apiurl(from the branch profile). Verify that the branch profile'sapiurlfield is set correctly with./percona-obs profile list.sync push(plain, over a previously branched env): Phase 2 loads the profile named in eachbranch:revision comment to get itsapiurl. If the profile has been renamed or deleted, it falls back to the targetapiurlwith a debug log.build dependency: checks every package's revision comment individually; routes each package's source project to the correctapiurlvia the same profile lookup.
| Symptom | Root cause | Fix applied |
|---|---|---|
| Phase 2 reports "0 local packages" | Querying target OBS for projects that have only aggregates (no build results) | Query the branch OBS (branch_apiurl) instead of the target OBS |
Dep propagation not triggering for pkg:flavor multibuild |
OBS _builddepinfo names source as pkg:flavor; lookup uses base name |
Strip :flavor suffix in _fetch_combined_depinfo |
| Plain push does not cascade-promote dependents | branch_project_for not populated for "promote" decisions |
Always record branch_project_for[key] when a branch: comment is detected, regardless of decision |
build dependency missing deps for aggregate packages |
Querying target OBS (aggregates have no builddepinfo there) | Check revision comment per package; route aggregate packages to their source OBS |
build dependency wrong for mixed projects (some promoted, some aggregate) |
Single-representative heuristic picked the promoted package | Check ALL packages (not just a representative) to cover mixed projects |
_validate_project_path_refs hangs on external/interconnect OBS |
Querying live OBS for non-local project references | Build local project name whitelist from find_projects(REPO_ROOT, …); skip any project: entry not in that set |
| Purpose | Exemplar |
|---|---|
| Go standalone package | percona-telemetry-agent/ |
| PG extension multi-version | ppg/staging/17/percona-pg-telemetry/ |
| Large PG server package | ppg/staging/17/percona-postgresql17/ |
| Third-party infrastructure service | ppg/staging/17/etcd/ |
| OBS aggregate (mirrors another OBS project) | obs-service-tar_scm/ |
| Root project config | root/project.yaml |
| Management script | percona-obs (commands: sync push, sync delete, sync promote, build trigger, build status, build dependency, profile create, profile list, project verify) |
Four workflows automate the OBS sync lifecycle. All of them use a shared composite action for setup.
Reusable setup steps called by every workflow:
- Install Python 3.11 via
actions/setup-python. - Create
venv/andpip install -r requirements.txt. - Write
~/.config/osc/oscrcfrom theobs-apiurl,obs-user, andobs-passwordinputs soosc(and thereforepercona-obs) can authenticate against OBS.
Trigger: push to main where at least one file under root/** changed.
What it does — two jobs. The sync job serializes on its own concurrency group and is never cancelled mid-upload; the poll job runs in a cancel-superseding group (newest poll wins — version lists and release tags are diffed from the last successful run's head SHA, so the surviving poll covers superseded runs' ranges).
sync job:
- Checks out the repo with full history (
fetch-depth: 0) — required becausesync pushreadsgit logto detect per-package changes since the last OBS sync SHA stored in OBS revision comments. - Runs
obs-setupand restores the.cache/actions/cache entries (including thesync_statemanifest used by--skip-unchanged). - Creates a
percona-obsprofile namedmainpointing atOBS_ROOTPRJwith env varsPERCONA_OBS_PACKAGING_BRANCH=main,PERCONA_OBS_PACKAGING_REPO=<repo-url>, andREMOTE_OBS_ORG_INTERCONNECT=(empty — no interconnect in the self-hosted setup). - Runs
percona-obs -P main sync push --no-scm-validate --skip-unchanged --report-json /tmp/sync-report.jsonto create/update OBS projects and packages, delete any OBS packages whose local directories were removed, and hand the sync report to the poll job as an artifact.
poll job:
5. Runs .github/scripts/poll_obs_builds.py to poll build status until all packages reach a terminal state (succeeded/failed/unresolvable/disabled). OBS_SYNC_REPORT points at the sync report so monitoring is scoped to the projects the sync actually touched (failing open to full-tree polling on a missing/corrupt/stale report); the poll interval backs off ×1.5 while states are unchanged (OBS_POLL_INTERVAL, default 30 s, up to OBS_POLL_MAX_INTERVAL, default 300 s). The script exits non-zero when any build failed/broke/was unresolvable, which fails the job's GitHub Actions check run — that check run is the merge-gating signal, no separate commit status is posted.
Required repository config: OBS_APIURL, OBS_WEB_URL, OBS_ROOTPRJ, OBS_USER (vars); OBS_PASSWORD (secret).
Permissions: contents: write (for badge publishing).
Trigger: pull_request against main (types: opened, synchronize, reopened, labeled) where at least one file under root/** changed. The sync/build (and QA) jobs only run when a trigger label — obs-sync, qa-packages, or qa-containers — is present on the PR. For labeled events, a job-level if on resolve skips the entire run unless the label just added is a trigger label; for the other event types resolve runs, checks the PR's labels, and if no trigger label is present the run gates out after resolve with the rest of the DAG skipped.
What it does:
- Full-history checkout (same reason as above).
- Runs
obs-setup. - Creates two
percona-obsprofiles:main— points atOBS_ROOTPRJ; used only as the--branch-fromsource so the tool can read each package's last-sync SHA from the main OBS project.pr-<N>— points atOBS_PR_ROOTPRJ:pr-<N>(the PR-specific OBS root project). ItsPERCONA_OBS_PACKAGING_BRANCHis set torefs/pull/<N>/head(a GitHub pseudo-ref resolvable viagit ls-remotefor all PRs including forks) andPERCONA_OBS_PACKAGING_REPOis set togithub.event.pull_request.head.repo.clone_url(the fork's repo URL when the PR comes from a fork).
- Runs
percona-obs -P pr-<N> sync push --branch-from main:- For each package, compares
git log <main-last-sync-sha>..HEAD -- <package-path>. If no commits → uploads an_aggregatefile pointing at the corresponding main OBS package (binary reuse, no build needed). If changed → uploads full sources to the PR project.
- For each package, compares
- Posts (or updates) a PR comment with the OBS project URL and a table showing how many packages were built from source vs. aggregated from main. The comment is identified by an HTML marker
<!-- obs-pr-check -->so it is updated in place on subsequent pushes to the PR. If the sync step failed, the comment is updated with an error notice instead.
Required repository config: OBS_APIURL, OBS_WEB_URL, OBS_ROOTPRJ, OBS_PR_ROOTPRJ, OBS_USER (vars); OBS_PASSWORD (secret).
Permissions: contents: read, pull-requests: write.
OBS_PR_ROOTPRJ is the base prefix for PR-specific projects, e.g. home:Admin:percona:pr. The full PR project becomes home:Admin:percona:pr:pr-42. It is intentionally separate from OBS_ROOTPRJ so PR projects live in a distinct namespace and are never confused with the main project tree.
Trigger: pull_request against main (type: closed) where at least one file under root/** changed.
What it does: Detects whether every changed root/** file in the PR sits under a
root/*/releases/ directory (a release-only PR) and, if so and the PR was merged, tags
and dispatches the release before the shared cleanup step runs.
Release PR path (merged, and every changed root/** file is under root/*/releases/):
- For each
release.yamlchanged by the PR, reads the last entry of itsreleases:list from the merge commit blob (not the PR head) and creates that git tag (e.g.ppg/17.9-1) pointing atmerge_commit_sha, usingGITHUB_TOKEN(contents: write). A tag-creation error other than "already exists" fails the job. - Dispatches
obs-release.ymlfor that tag viagh workflow run obs-release.yml --ref main -f tag=<tag>(actions: write).workflow_dispatchcalls made withGITHUB_TOKENare exempt from GitHub's workflow-to-workflow trigger suppression, so this fires reliably without a PAT — and a pushed tag would not have triggered anything on its own.
Every PR (release or not, merged or closed unmerged):
- Runs
percona-obs -P pr-<N> sync delete --yes --recursive --from-obsto delete the PR's OBS project and all its sub-projects.sync release-pris not run first — no workflow promotes a non-release PR's binaries to production anymore; production gets those binaries only through the normalsync-main.ymlsync ofmainplus, for releases, the release pipeline above. - Deletes the PR's Actions caches (created by
obs-pr-check.ymlon the PR merge ref).
OBS 404 responses are handled gracefully in sync delete so the cleanup is safe even if the PR
check never ran. --recursive ensures projects are deleted even if a build was still in progress.
Required repository config: OBS_APIURL, OBS_PR_ROOTPRJ, OBS_USER (vars); OBS_PASSWORD (secret).
Permissions: contents: write (tag creation), actions: write (dispatch + cache deletion).
Trigger: scheduled daily at 02:00 UTC, or manually via workflow_dispatch.
What it does: Lists all open PRs that have had no activity for more than 7 days and runs percona-obs sync delete --yes --recursive against each PR's OBS project (OBS_PR_ROOTPRJ:pr-<N>). This prevents inactive PR projects from accumulating indefinitely on OBS. The delete is a no-op if the project never existed (e.g. the PR never touched root/).
Required repository config: OBS_APIURL, OBS_PR_ROOTPRJ, OBS_USER (vars); OBS_PASSWORD (secret).
${VAR} tokens in obs/_service, obs/_aggregate, and obs/_link files are substituted by apply_env_substitution() before the file is uploaded to OBS. The following variables are available:
| Variable | Source | Purpose |
|---|---|---|
PERCONA_OBS_PACKAGING_BRANCH |
Profile env | Git ref OBS checks out from the packaging repo (e.g. main, refs/pull/42/head) |
PERCONA_OBS_PACKAGING_REPO |
Profile env | HTTPS clone URL of the packaging repo OBS fetches from. Set to the fork's URL for PR profiles so OBS fetches packaging files from the correct repo when building promoted packages. |
REMOTE_OBS_ORG_INTERCONNECT |
Profile env | Prefix for external OBS instance project references (e.g. openSUSE.org:). Empty string when no interconnect is used. |
OBS_ROOTPRJ |
Auto-injected | The root OBS project name (--rootprj). Use this in _aggregate files to reference sibling subprojects without hardcoding the org prefix (e.g. ${OBS_ROOTPRJ}:common:deps:runtime). |
DEBIAN_PACKAGE_DIRECTORY |
Auto-injected per package | Path to the package's debian/ subdir relative to the repo root (e.g. root/ppg/staging/17/percona-haproxy/debian). Use as the subdir param in the first packaging obs_scm service. |
RPM_PACKAGE_DIRECTORY |
Auto-injected per package | Path to the package's rpm/ subdir relative to the repo root (e.g. root/ppg/staging/17/percona-haproxy/rpm). Use as the subdir param in the second packaging obs_scm service. |
PERCONA_OBS_PACKAGING_BRANCH, PERCONA_OBS_PACKAGING_REPO, and REMOTE_OBS_ORG_INTERCONNECT are declared in each percona-obs profile via -e KEY:VALUE at profile-creation time. OBS_ROOTPRJ, DEBIAN_PACKAGE_DIRECTORY, and RPM_PACKAGE_DIRECTORY are injected automatically and do not need to be declared manually.