Skip to content

pgAdmin 4 for UBI-9: SP1 node_modules tooling + SP2 local-npm-registry + SP3 Python 3.12 stack #189

pgAdmin 4 for UBI-9: SP1 node_modules tooling + SP2 local-npm-registry + SP3 Python 3.12 stack

pgAdmin 4 for UBI-9: SP1 node_modules tooling + SP2 local-npm-registry + SP3 Python 3.12 stack #189

Workflow file for this run

name: OBS PR Check
# For PRs that touch root/, create a PR-specific OBS project and build
# packages there. Unchanged packages are aggregated (binary reuse) from the
# main branch's OBS project; only packages changed in this PR are built from
# source. Posts (or updates) a comment on the PR with the OBS project URL.
#
# Job DAG:
#
# resolve ──> sync ──┬──> build ─────────────────────────┐
# │ │
# └──> detect-qa-matrix ───────────────┴──> qa (matrix)
#
# `resolve` fetches PR context from the GitHub API and gates the DAG on the
# presence of a trigger label so that non-trigger events produce no check runs.
# `detect-qa-matrix` runs in parallel with `build`; the `qa` matrix combos
# show up in the PR's check list as queued check runs as soon as detect
# finishes, so the upcoming QA work is visible while OBS is still building.
#
# For `labeled` events the `resolve` job runs only when the label that was just
# added is a trigger label (obs-sync, qa-packages, qa-containers). Control-only
# labels (e.g. no-dep-cascade) and repo-filter labels (e.g. Ubuntu_26.04) fail
# the gate so no workflow run is created and no spurious check runs appear on
# the PR. Triggering via pull_request (rather than workflow_dispatch) is
# required for the resulting check runs to show in the PR's statusCheckRollup
# and be visible to branch protection.
on:
pull_request:
branches: [main]
paths: ['root/**']
types: [opened, synchronize, reopened, labeled]
# One run per PR at a time. When a push and label additions fire near-
# simultaneously, or when two trigger labels are added together, the earlier
# runs are cancelled and only the last one proceeds (which sees the full label
# set via the API in the resolve job).
concurrency:
group: obs-pr-check-pr-${{ github.event.number }}
cancel-in-progress: true
jobs:
# ──────────────────────────────────────────────────────────────────────────
# resolve: fetch PR context from the GitHub API. Also checks for a trigger
# label so downstream jobs can gate on has_trigger_label instead of repeating
# the label condition.
#
# The job-level `if` skips the entire run for `labeled` events on non-trigger
# labels (control-only labels, repo-filter labels). When the condition is
# false GitHub does not create the workflow run, so no check runs appear on
# the PR for those label additions.
# ──────────────────────────────────────────────────────────────────────────
resolve:
name: Resolve PR context
if: |
github.event.action != 'labeled' ||
github.event.label.name == 'obs-sync' ||
github.event.label.name == 'qa-packages' ||
github.event.label.name == 'qa-containers'
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
pr_number: ${{ steps.ctx.outputs.pr_number }}
head_sha: ${{ steps.ctx.outputs.head_sha }}
clone_url: ${{ steps.ctx.outputs.clone_url }}
labels_json: ${{ steps.ctx.outputs.labels_json }}
has_trigger_label: ${{ steps.ctx.outputs.has_trigger_label }}
steps:
- name: Fetch PR context
id: ctx
env:
GH_TOKEN: ${{ github.token }}
run: |
PR="${{ github.event.number }}"
DATA=$(gh api repos/${{ github.repository }}/pulls/${PR})
HEAD_SHA=$(echo "$DATA" | jq -r '.head.sha')
CLONE_URL=$(echo "$DATA" | jq -r '.head.repo.clone_url')
LABELS=$(echo "$DATA" | jq -c '[.labels[].name]')
HAS_TRIGGER=$(echo "$LABELS" | jq '[.[] | select(. == "obs-sync" or . == "qa-packages" or . == "qa-containers")] | length > 0')
{
echo "pr_number=${PR}"
echo "head_sha=${HEAD_SHA}"
echo "clone_url=${CLONE_URL}"
printf 'labels_json=%s\n' "${LABELS}"
echo "has_trigger_label=${HAS_TRIGGER}"
} >> "$GITHUB_OUTPUT"
# ──────────────────────────────────────────────────────────────────────────
# sync: push the PR contents to OBS and (for release-only PRs) test the
# release flow. Does NOT poll OBS builds — that is done in `build` so it
# can run in parallel with QA matrix detection.
# ──────────────────────────────────────────────────────────────────────────
sync:
name: Sync packages to OBS
needs: [resolve]
runs-on: ubuntu-latest
container: ghcr.io/${{ github.repository_owner }}/obs-tools:latest
# Backstop for stalled source services / OBS API hangs; a healthy sync
# job finishes in well under 30 minutes.
timeout-minutes: 120
if: needs.resolve.outputs.has_trigger_label == 'true'
permissions:
# Required to post/update the PR comment.
pull-requests: write
packages: read
outputs:
is_release_pr: ${{ steps.detect.outputs.is_release_pr }}
sync_outcome: ${{ steps.sync.outcome }}
env:
PR_NUMBER: ${{ needs.resolve.outputs.pr_number }}
OBS_APIURL: ${{ vars.OBS_APIURL }}
OBS_WEB_URL: ${{ vars.OBS_WEB_URL }}
OBS_ROOTPRJ: ${{ vars.OBS_ROOTPRJ }}
OBS_PR_ROOTPRJ: ${{ vars.OBS_PR_ROOTPRJ }}
steps:
- uses: actions/checkout@v4
with:
# Full history is required: --branch-from runs git log <sha>..HEAD
# per package to detect which packages changed since main's last sync.
fetch-depth: 0
# Packages synced before the migration to percona/obs-packaging carry
# OBS sync comments whose SHAs come from the legacy repo's history and
# do not resolve in this clone, which forces the branch decision off
# the cheap git gate into a full content check for every such package.
# Fetch the legacy history so those SHAs resolve; drop this step once
# all packages have been re-synced with percona/obs-packaging SHAs.
- name: Fetch legacy repo history (pre-migration sync SHAs)
run: |
git fetch https://github.com/rjd15372/percona-obs-packaging.git \
'refs/heads/*:refs/remotes/legacy/*' \
|| echo "::warning::legacy repo unreachable; sync falls back to content checks"
# Persists the content-addressed .cache/ tree across runs, split in
# two entries (same layout as sync-main.yml so PR runs can restore the
# main branch's caches via restore-keys):
#
# deps — download_url, cargo_vendor and Phase 2 manual-service
# outputs (crates, go tarballs, vendor tarballs): small and
# stable across commits.
# scm — Phase 1 obs_scm outputs: multi-GB source archives keyed by
# upstream HEAD, churning on every upstream advance.
#
# Splitting keeps the small, highly-reusable deps cache from being
# evicted out of the repo's 10 GB Actions cache quota by churn in the
# much larger scm tree. The caches are keyed by the PR head commit so
# re-running the workflow on the same commit is a full hit and Phase
# 1/2 services are skipped entirely -- otherwise each fresh checkout
# would regenerate vendor tarballs with new file mtimes, producing
# different MD5s and needless re-uploads. Entries are content-
# addressed so a partial hit via restore-keys is always safe: unused
# SHA-keyed subdirs are ignored.
# The deps `path` list MUST stay identical to the one in sync-main.yml:
# actions/cache hashes the path list into the cache *version*, and
# restore-keys only match entries with the same version. A divergence
# silently hides main's deps cache from PR runs, which then re-download
# every download_url source (and re-vendor every crate) on every run.
- name: Restore percona-obs deps cache
uses: actions/cache@v4
with:
path: |
.cache/download_url
.cache/cargo_vendor
.cache/services
.cache/sync_state
key: percona-obs-deps-${{ runner.os }}-${{ needs.resolve.outputs.head_sha }}
restore-keys: |
percona-obs-deps-${{ runner.os }}-
- name: Restore percona-obs scm cache
uses: actions/cache@v4
with:
path: .cache/obs_scm
key: percona-obs-scm-${{ runner.os }}-${{ needs.resolve.outputs.head_sha }}
restore-keys: |
percona-obs-scm-${{ runner.os }}-
- uses: ./.github/actions/obs-setup
with:
obs-apiurl: ${{ vars.OBS_APIURL }}
obs-user: ${{ vars.OBS_USER }}
obs-password: ${{ secrets.OBS_PASSWORD }}
# Two profiles are needed:
#
# main — points at the main branch's OBS project; used as the
# --branch-from source so percona-obs can read each package's
# last-sync SHA and decide aggregate vs. promote.
#
# pr-N — points at the PR-specific OBS project (rootprj:pr-N);
# packages changed in this PR are synced here from source,
# unchanged packages get an _aggregate pointing at main.
#
# PERCONA_OBS_PACKAGING_BRANCH for the PR profile is set to
# refs/pull/<N>/head — a GitHub pseudo-ref accessible via git ls-remote
# for all PRs including forks. OBS's obs_scm service uses this ref to
# check out the PR's packaging files when it builds promoted packages.
- name: Create percona-obs profiles
run: |
PR_ROOTPRJ="${OBS_PR_ROOTPRJ}:pr-${PR_NUMBER}"
venv/bin/python -m percona_obs \
-A "$OBS_APIURL" \
-R "$OBS_ROOTPRJ" \
-e "REMOTE_OBS_ORG_INTERCONNECT:" \
-e "PERCONA_OBS_PACKAGING_BRANCH:main" \
-e "PERCONA_OBS_PACKAGING_REPO:${{ github.server_url }}/${{ github.repository }}.git" \
profile create main
venv/bin/python -m percona_obs \
-A "$OBS_APIURL" \
-R "$PR_ROOTPRJ" \
-e "REMOTE_OBS_ORG_INTERCONNECT:" \
-e "PERCONA_OBS_PACKAGING_BRANCH:refs/pull/${PR_NUMBER}/head" \
-e "PERCONA_OBS_PACKAGING_REPO:${{ needs.resolve.outputs.clone_url }}" \
profile create "pr-${PR_NUMBER}"
# Detect whether this is a release-only PR (all changed files under
# root/ are under root/*/releases/). Release-only PRs skip the package
# sync steps entirely — releases/ is excluded from sync push traversal
# so there is nothing to build.
- name: Detect release-only PR
id: detect
run: |
CHANGED=$(git diff --name-only origin/main...HEAD -- 'root/**')
NON_RELEASE=$(echo "$CHANGED" | grep -v '^root/[^/]*/releases/' || true)
if [ -z "$CHANGED" ] || [ -n "$NON_RELEASE" ]; then
echo "is_release_pr=false" >> "$GITHUB_OUTPUT"
else
echo "is_release_pr=true" >> "$GITHUB_OUTPUT"
fi
# Build --only-repos from any PR labels that are not control labels.
# If repo labels like RockyLinux_9 or Debian_13 are present, sync push
# will restrict OBS project meta to those repositories only.
# Also emits --no-dep-cascade when the no-dep-cascade label is set.
- name: Resolve repo label filter
id: repo-filter
run: |
LABELS='${{ needs.resolve.outputs.labels_json }}'
CONTROL='. != "obs-sync" and . != "qa-packages" and . != "qa-containers" and . != "no-dep-cascade"'
REPO_LABELS=$(echo "$LABELS" | jq -r "[.[] | select(${CONTROL})] | join(\",\")")
if [ -n "$REPO_LABELS" ]; then
echo "only_repos_arg=--only-repos ${REPO_LABELS}" >> "$GITHUB_OUTPUT"
fi
if echo "$LABELS" | jq -e '[.[] | select(. == "no-dep-cascade")] | length > 0' > /dev/null; then
echo "no_dep_cascade_arg=--no-dep-cascade" >> "$GITHUB_OUTPUT"
fi
# Dry run: shows the sync plan (which packages will be promoted vs
# aggregated) before the real sync starts, making progress visible early.
# A failure here aborts the workflow before the real sync runs.
- name: Plan sync (dry run)
if: steps.detect.outputs.is_release_pr != 'true'
run: |
venv/bin/python -u -m percona_obs \
-P "pr-${PR_NUMBER}" sync push \
--dry-run \
--branch-from main \
--no-scm-validate \
${{ steps.repo-filter.outputs.only_repos_arg }} \
${{ steps.repo-filter.outputs.no_dep_cascade_arg }}
# -u disables Python stdout buffering so progress lines appear
# immediately even though output is piped through tee.
# Output is tee'd to a file so the comment step can parse package counts.
- name: Sync PR to OBS
id: sync
if: steps.detect.outputs.is_release_pr != 'true'
run: |
venv/bin/python -u -m percona_obs --verbose \
-P "pr-${PR_NUMBER}" sync push \
--branch-from main \
--no-scm-validate \
${{ steps.repo-filter.outputs.only_repos_arg }} \
${{ steps.repo-filter.outputs.no_dep_cascade_arg }} \
| tee /tmp/sync-output.txt
# Drop cache entries not used by any run in the last 7 days so the
# .cache/ tree saved by actions/cache stops growing monotonically.
# Entry mtimes are bumped on every cache hit, so age == unused-for.
# Runs even when the sync fails: pruning only removes stale entries.
- name: Prune stale cache entries
if: always()
env:
PYTHONPATH: ${{ github.workspace }}
run: venv/bin/python .github/scripts/prune_obs_cache.py
# The full build-status comment is posted by the `build` job (which has
# the poll details). This step only fires for the two terminal-in-sync
# cases: release-only PRs and sync failures. For successful non-release
# PRs it is skipped — `build` will post the real comment.
- name: Post OBS project comment (sync-side)
if: always() && (steps.detect.outputs.is_release_pr == 'true' || steps.sync.outcome != 'success')
env:
GH_TOKEN: ${{ github.token }}
IS_RELEASE_PR: ${{ steps.detect.outputs.is_release_pr }}
SYNC_OUTCOME: ${{ steps.sync.outcome }}
RELEASE_OUTCOME: ''
POLL_OUTCOME: ''
WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
OBS_PR_ROOTPRJ: ${{ vars.OBS_PR_ROOTPRJ }}
run: python .github/scripts/post_pr_comment.py
# Upload the sync output so the `build` job can read it for the comment.
# Only meaningful when the sync step actually ran and succeeded.
- name: Upload sync output
if: steps.sync.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: sync-output
path: /tmp/sync-output.txt
retention-days: 1
# ──────────────────────────────────────────────────────────────────────────
# build: poll OBS for build completion and post the build-status PR
# comment. Runs in parallel with `detect-qa-matrix`.
# ──────────────────────────────────────────────────────────────────────────
build:
name: Wait for OBS builds
needs: [resolve, sync]
if: needs.sync.outputs.is_release_pr != 'true' && needs.sync.outputs.sync_outcome == 'success'
runs-on: ubuntu-latest
container: ghcr.io/${{ github.repository_owner }}/obs-tools:latest
permissions:
pull-requests: write
packages: read
env:
PR_NUMBER: ${{ needs.resolve.outputs.pr_number }}
OBS_APIURL: ${{ vars.OBS_APIURL }}
OBS_WEB_URL: ${{ vars.OBS_WEB_URL }}
OBS_PR_ROOTPRJ: ${{ vars.OBS_PR_ROOTPRJ }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/obs-setup
with:
obs-apiurl: ${{ vars.OBS_APIURL }}
obs-user: ${{ vars.OBS_USER }}
obs-password: ${{ secrets.OBS_PASSWORD }}
- name: Download sync output
uses: actions/download-artifact@v4
with:
name: sync-output
path: /tmp
# Poll OBS until all builds in the PR project reach a terminal state.
# Fails the job (and PR check) if any build fails, is broken, or is
# unresolvable.
- name: Poll OBS build status
id: poll
env:
OBS_APIURL: ${{ vars.OBS_APIURL }}
OBS_ROOTPRJ: ${{ vars.OBS_PR_ROOTPRJ }}:pr-${{ needs.resolve.outputs.pr_number }}
PYTHONPATH: ${{ github.workspace }}
run: venv/bin/python .github/scripts/poll_obs_builds.py
# Always runs so the comment is updated even when poll fails.
- name: Post OBS project comment
if: always()
env:
GH_TOKEN: ${{ github.token }}
IS_RELEASE_PR: 'false'
SYNC_OUTCOME: 'success'
RELEASE_OUTCOME: ''
POLL_OUTCOME: ${{ steps.poll.outcome }}
WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
OBS_PR_ROOTPRJ: ${{ vars.OBS_PR_ROOTPRJ }}
run: python .github/scripts/post_pr_comment.py
# ──────────────────────────────────────────────────────────────────────────
# detect-qa-matrix: discover QA-enabled OBS subprojects. Runs in parallel
# with `build`; once it finishes, the `qa` matrix job's check runs appear in
# the PR's check list as queued, signalling the upcoming QA work.
# ──────────────────────────────────────────────────────────────────────────
detect-qa-matrix:
name: Detect QA matrix
needs: [resolve, sync]
if: |
needs.sync.outputs.is_release_pr != 'true' && needs.sync.outputs.sync_outcome == 'success' &&
(contains(fromJSON(needs.resolve.outputs.labels_json), 'qa-packages') || contains(fromJSON(needs.resolve.outputs.labels_json), 'qa-containers'))
runs-on: ubuntu-latest
container: ghcr.io/${{ github.repository_owner }}/obs-tools:latest
permissions:
packages: read
outputs:
has_matrix: ${{ steps.matrix.outputs.has_matrix }}
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/obs-setup
with:
obs-apiurl: ${{ vars.OBS_APIURL }}
obs-user: ${{ vars.OBS_USER }}
obs-password: ${{ secrets.OBS_PASSWORD }}
- name: Write CI profile
env:
OBS_APIURL: ${{ vars.OBS_APIURL }}
OBS_PR_ROOTPRJ: ${{ vars.OBS_PR_ROOTPRJ }}
PR_NUMBER: ${{ needs.resolve.outputs.pr_number }}
JENKINS_URL: ${{ vars.JENKINS_URL }}
JENKINS_USER: ${{ vars.JENKINS_USER }}
run: |
mkdir -p .profile
cat > .profile/ci.yaml <<EOF
apiurl: ${OBS_APIURL}
rootprj: ${OBS_PR_ROOTPRJ}:pr-${PR_NUMBER}
jenkins:
url: ${JENKINS_URL}
user: ${JENKINS_USER}
env:
- name: REMOTE_OBS_ORG_INTERCONNECT
value: 'openSUSE.org:'
EOF
# Build QA_TYPES from qa-packages / qa-containers labels.
# Passed to list_qa_matrix.py to restrict which subproject types are scanned.
- name: Resolve QA type filter
id: qa-types
run: |
LABELS='${{ needs.resolve.outputs.labels_json }}'
TYPES=""
if echo "$LABELS" | jq -e '[.[] | select(. == "qa-packages")] | length > 0' > /dev/null; then
TYPES="packages"
fi
if echo "$LABELS" | jq -e '[.[] | select(. == "qa-containers")] | length > 0' > /dev/null; then
TYPES="${TYPES:+$TYPES,}containers"
fi
echo "qa_types=${TYPES}" >> "$GITHUB_OUTPUT"
# Iterate every immediate subproject of the PR's OBS root project, call
# `percona-obs qa show <project> --json` for each, and concatenate the
# results into a single matrix array (empty for subprojects without a
# `qa:` block).
- name: Discover QA matrix from OBS PR subprojects
id: matrix
env:
OBS_APIURL: ${{ vars.OBS_APIURL }}
OBS_PROJECT: ${{ vars.OBS_PR_ROOTPRJ }}:pr-${{ needs.resolve.outputs.pr_number }}
PYTHONPATH: ${{ github.workspace }}
QA_TYPES: ${{ steps.qa-types.outputs.qa_types }}
run: |
venv/bin/python .github/scripts/list_qa_matrix.py > /tmp/qa-matrix.json
MATRIX=$(cat /tmp/qa-matrix.json)
echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT"
if [ "$(echo "${MATRIX}" | jq 'length')" -gt 0 ]; then
echo "has_matrix=true" >> "$GITHUB_OUTPUT"
else
echo "has_matrix=false" >> "$GITHUB_OUTPUT"
fi
# ──────────────────────────────────────────────────────────────────────────
# qa: trigger and poll Jenkins, once per matrix combo (in parallel).
# ──────────────────────────────────────────────────────────────────────────
qa:
name: ${{ matrix.status_context }}
needs: [resolve, build, detect-qa-matrix]
if: needs.detect-qa-matrix.outputs.has_matrix == 'true' && needs.build.result == 'success'
runs-on: ubuntu-latest
container: ghcr.io/${{ github.repository_owner }}/obs-tools:latest
permissions:
packages: read
strategy:
matrix:
include: ${{ fromJSON(needs.detect-qa-matrix.outputs.matrix) }}
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/obs-setup
with:
obs-apiurl: ${{ vars.OBS_APIURL }}
obs-user: ${{ vars.OBS_USER }}
obs-password: ${{ secrets.OBS_PASSWORD }}
# Same CI profile as the detect job so `qa run` re-resolves project.yaml
# parameters identically (same OBS_ROOTPRJ → same ${OBS_ROOTPRJ_SLASHES}).
- name: Write CI profile
env:
OBS_APIURL: ${{ vars.OBS_APIURL }}
OBS_PR_ROOTPRJ: ${{ vars.OBS_PR_ROOTPRJ }}
PR_NUMBER: ${{ needs.resolve.outputs.pr_number }}
JENKINS_URL: ${{ vars.JENKINS_URL }}
JENKINS_USER: ${{ vars.JENKINS_USER }}
run: |
mkdir -p .profile
cat > .profile/ci.yaml <<EOF
apiurl: ${OBS_APIURL}
rootprj: ${OBS_PR_ROOTPRJ}:pr-${PR_NUMBER}
jenkins:
url: ${JENKINS_URL}
user: ${JENKINS_USER}
env:
- name: REMOTE_OBS_ORG_INTERCONNECT
value: 'openSUSE.org:'
EOF
- name: Run qa pipeline
env:
JENKINS_API_TOKEN: ${{ secrets.JENKINS_API_TOKEN }}
PYTHONUNBUFFERED: "1"
run: |
venv/bin/python -m percona_obs -P ci qa run "${{ matrix.project }}" \
--pipeline "${{ matrix.pipeline }}" \
--wait \
--report-json /tmp/qa-report.json \
${{ matrix.axis_filters }}