Skip to content

OBS Nightly QA

OBS Nightly QA #13

name: OBS Nightly QA
# Runs nightly (and on demand via workflow_dispatch) against `main`.
# Discovers QA-enabled OBS subprojects beneath ${{ vars.OBS_ROOTPRJ }},
# triggers Jenkins via `percona-obs qa run` for each matrix combo (each is
# its own GitHub Actions check run on the main HEAD), and finally aggregates
# the results into one shields.io endpoint badge per distribution project
# (qa-badge-ppg-staging-18.json, ...) on the `badges` branch.
on:
schedule:
- cron: '0 23 * * *' # 23:00 UTC nightly
workflow_dispatch: {}
jobs:
# ──────────────────────────────────────────────────────────────────────────
# changes: skip the rest of the workflow when nothing under root/ has
# changed since the last successful nightly QA run. workflow_dispatch
# always bypasses this check so manual runs are never suppressed.
# ──────────────────────────────────────────────────────────────────────────
changes:
name: Check root/ changes since last QA run
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
outputs:
has_root_changes: ${{ steps.check.outputs.has_root_changes }}
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Check for root/ changes since last successful QA run
id: check
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "Triggered manually — skipping change check."
echo "has_root_changes=true" >> "$GITHUB_OUTPUT"
exit 0
fi
LAST_SHA=$(gh run list \
--workflow obs-nightly-qa.yml \
--status success \
--limit 1 \
--json headSha \
--jq '.[0].headSha // empty' 2>/dev/null || true)
if [ -z "$LAST_SHA" ]; then
echo "No previous successful run found — running QA."
echo "has_root_changes=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Last successful QA run: $LAST_SHA"
CHANGES=$(git log --oneline "$LAST_SHA"..HEAD -- root/)
if [ -n "$CHANGES" ]; then
echo "Changes found in root/ since $LAST_SHA — running QA."
echo "has_root_changes=true" >> "$GITHUB_OUTPUT"
else
echo "No changes in root/ since last QA run ($LAST_SHA) — skipping."
echo "has_root_changes=false" >> "$GITHUB_OUTPUT"
fi
# ──────────────────────────────────────────────────────────────────────────
# detect: discover QA-enabled subprojects under OBS_ROOTPRJ and emit the
# GitHub Actions matrix.
# ──────────────────────────────────────────────────────────────────────────
detect:
name: Detect QA matrix
needs: changes
runs-on: ubuntu-latest
container: ghcr.io/${{ github.repository_owner }}/obs-tools:latest
if: needs.changes.outputs.has_root_changes == 'true'
permissions:
packages: read
outputs:
has_matrix: ${{ steps.matrix.outputs.has_matrix }}
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
with:
ref: main
- 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_ROOTPRJ: ${{ vars.OBS_ROOTPRJ }}
JENKINS_URL: ${{ vars.JENKINS_URL }}
JENKINS_USER: ${{ vars.JENKINS_USER }}
run: |
mkdir -p .profile
cat > .profile/ci.yaml <<EOF
apiurl: ${OBS_APIURL}
rootprj: ${OBS_ROOTPRJ}
jenkins:
url: ${JENKINS_URL}
user: ${JENKINS_USER}
env:
- name: REMOTE_OBS_ORG_INTERCONNECT
value: 'openSUSE.org:'
EOF
- name: Discover QA matrix
id: matrix
env:
OBS_APIURL: ${{ vars.OBS_APIURL }}
OBS_PROJECT: ${{ vars.OBS_ROOTPRJ }}
PYTHONPATH: ${{ github.workspace }}
run: |
venv/bin/python .github/scripts/list_qa_matrix.py > /tmp/qa-matrix.json
{
echo "matrix<<EOF"
cat /tmp/qa-matrix.json
echo "EOF"
} >> "$GITHUB_OUTPUT"
if [ "$(jq 'length' /tmp/qa-matrix.json)" -gt 0 ]; then
echo "has_matrix=true" >> "$GITHUB_OUTPUT"
else
echo "has_matrix=false" >> "$GITHUB_OUTPUT"
fi
# ──────────────────────────────────────────────────────────────────────────
# qa: trigger Jenkins for each matrix combo (in parallel) and upload the
# per-combo report JSON as an artifact for the aggregate job.
# ──────────────────────────────────────────────────────────────────────────
qa:
name: ${{ matrix.status_context }}
needs: detect
runs-on: ubuntu-latest
container: ghcr.io/${{ github.repository_owner }}/obs-tools:latest
if: needs.detect.outputs.has_matrix == 'true'
permissions:
packages: read
strategy:
matrix:
include: ${{ fromJSON(needs.detect.outputs.matrix) }}
fail-fast: false
steps:
- uses: actions/checkout@v4
with:
ref: main
- 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_CONTAINER_REGISTRY_ROOTPRJ}).
- name: Write CI profile
env:
OBS_APIURL: ${{ vars.OBS_APIURL }}
OBS_ROOTPRJ: ${{ vars.OBS_ROOTPRJ }}
JENKINS_URL: ${{ vars.JENKINS_URL }}
JENKINS_USER: ${{ vars.JENKINS_USER }}
run: |
mkdir -p .profile
cat > .profile/ci.yaml <<EOF
apiurl: ${OBS_APIURL}
rootprj: ${OBS_ROOTPRJ}
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 }}
# Upload the per-combo report so the aggregate job can build the badge.
- name: Upload QA report artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: qa-report-${{ strategy.job-index }}
path: /tmp/qa-report.json
if-no-files-found: ignore
# ──────────────────────────────────────────────────────────────────────────
# aggregate: download every per-combo report, compute one shields.io
# endpoint badge per top-level project, push them to the orphan `badges`
# branch (same pattern as sync-main.yml's "Publish OBS build badge").
# ──────────────────────────────────────────────────────────────────────────
aggregate:
name: Aggregate QA badges
needs: [detect, qa]
runs-on: ubuntu-latest
# Disabled: QA status is now surfaced by an external web dashboard, so we
# no longer publish qa-badge-*.json to the badges branch nor rewrite the
# README QA column (the latter pushes directly to main, blocked once main
# is protected). Re-enable (restore the has_matrix guard) if in-repo QA
# badges are needed again.
if: false
permissions:
# Required to push qa-badge-*.json to the badges branch.
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: main
# Full history + GH_PAT so the README QA-badge update step can
# rebase and push back to main alongside the badges-branch push.
fetch-depth: 0
token: ${{ secrets.GH_PAT }}
- uses: actions/download-artifact@v4
with:
path: /tmp/qa-reports
pattern: qa-report-*
- name: Compute QA badges
run: |
mkdir -p /tmp/qa-badges
python3 .github/scripts/aggregate_qa_badges.py /tmp/qa-reports /tmp/qa-badges
ls -la /tmp/qa-badges
- name: Publish QA badges
env:
GH_TOKEN: ${{ github.token }}
run: |
shopt -s nullglob
BADGES=(/tmp/qa-badges/qa-badge-*.json)
if [ ${#BADGES[@]} -eq 0 ]; then
echo "no badges to publish"
exit 0
fi
for f in "${BADGES[@]}"; do
NAME=$(basename "$f")
BADGE=$(base64 -w0 "$f")
FILE_SHA=$(gh api \
"repos/$GITHUB_REPOSITORY/contents/$NAME?ref=badges" \
--jq '.sha' 2>/dev/null || echo "")
ARGS=(-X PUT
-f "message=ci: update QA badge ($NAME)"
-f "content=$BADGE"
-f "branch=badges")
if [ -n "$FILE_SHA" ]; then
ARGS+=(-f "sha=$FILE_SHA")
fi
gh api "repos/$GITHUB_REPOSITORY/contents/$NAME" "${ARGS[@]}"
done
# Rewrite the QA Status column in README.md so each top-level
# distribution row links to its just-published shields.io badge.
# Idempotent and rebases against main, so it converges with concurrent
# sync-main pushes.
- name: Update README QA badges
run: python3 .github/scripts/update_qa_badges.py /tmp/qa-badges