OBS Nightly QA #20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |