From d7fbea62f99d978f3d36ebef50ed8cdc1fbd6fed Mon Sep 17 00:00:00 2001 From: Geza Lore Date: Tue, 7 Oct 2025 10:16:46 +0100 Subject: [PATCH] CI: Notify PRs when coverage report is available --- .github/workflows/pages.yml | 28 +++++++++++++++ ci/ci-pages-notify.bash | 46 +++++++++++++++++++++++++ ci/ci-pages.bash | 69 +++++++++++++++++++++++++++---------- 3 files changed, 124 insertions(+), 19 deletions(-) create mode 100755 ci/ci-pages-notify.bash diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 04199602a..cd2ce3bb6 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -37,10 +37,13 @@ jobs: build: name: Build content runs-on: ubuntu-24.04 + outputs: + coverage-pr-run-ids: ${{ steps.build.outputs.coverage-pr-run-ids }} steps: - name: Checkout uses: actions/checkout@v5 - name: Build pages + id: build env: GH_TOKEN: ${{ github.token }} run: | @@ -62,3 +65,28 @@ jobs: steps: - name: Deploy to GitHub Pages uses: actions/deploy-pages@v4 + + notify: + name: Notify + needs: + - build + - deploy + runs-on: ubuntu-24.04 + if: ${{ github.repository == 'verilator/verilator' }} + steps: + - name: Checkout + uses: actions/checkout@v5 + # Use the Verilator CI app to post the comment + - name: Generate access token + id: generate-token + uses: actions/create-github-app-token@v2.1.4 + with: + app-id: ${{ vars.VERILATOR_CI_ID }} + private-key: ${{ secrets.VERILATOR_CI_KEY }} + permission-actions: write + permission-pull-requests: write + - name: Comment on PR + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + COVERAGE_PR_RUN_IDS: ${{ needs.build.outputs.coverage-pr-run-ids }} + run: bash -x ./ci/ci-pages-notify.bash diff --git a/ci/ci-pages-notify.bash b/ci/ci-pages-notify.bash new file mode 100755 index 000000000..3e892f868 --- /dev/null +++ b/ci/ci-pages-notify.bash @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# DESCRIPTION: Verilator: CI script for 'pages.yml', notifies PRs +# +# Copyright 2025 by Geza Lore. This program is free software; you +# can redistribute it and/or modify it under the terms of either the GNU +# Lesser General Public License Version 3 or the Perl Artistic License +# Version 2.0. +# +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +# Notify PRs via comment that their coverage reports are available + +# Get the current repo URL - might differ on a fork +readonly REPO_URL=$(gh repo view --json url --jq .url) + +# Create artifacts root directory +ARTIFACTS_ROOT=artifacts +mkdir -p ${ARTIFACTS_ROOT} + +for RUN_ID in ${COVERAGE_PR_RUN_IDS//,/ }; do + echo "@@@ Processing run ${RUN_ID}" + + # Create workflow artifacts directory + ARTIFACTS_DIR=${ARTIFACTS_ROOT}/${RUN_ID} + mkdir -p ${ARTIFACTS_DIR} + + # Download artifact of this run, if exists + gh run download ${RUN_ID} --name coverage-pr-notification --dir ${ARTIFACTS_DIR} || true + ls -lsha ${ARTIFACTS_DIR} + + # Move on if no notification is required + if [ ! -f ${ARTIFACTS_DIR}/pr-number.txt ]; then + echo "No notification found" + continue + fi + echo "Posting notification found" + + cat ${ARTIFACTS_DIR}/body.txt + gh pr comment $(cat ${ARTIFACTS_DIR}/pr-number.txt) --body-file ${ARTIFACTS_DIR}/body.txt + + # Get the artifact ID + ARTIFACT_ID=$(gh api "repos/{owner}/{repo}/actions/runs/${RUN_ID}/artifacts" --jq '.artifacts[] | select(.name == "coverage-pr-notification") | .id') + + # Delete it, so we only notify once + gh api --method DELETE "repos/{owner}/{repo}/actions/artifacts/${ARTIFACT_ID}" +done diff --git a/ci/ci-pages.bash b/ci/ci-pages.bash index ec2eec318..c61a09c9c 100755 --- a/ci/ci-pages.bash +++ b/ci/ci-pages.bash @@ -24,9 +24,13 @@ mkdir -p ${PAGES_ROOT} # Get the current repo URL - might differ on a fork readonly REPO_URL=$(gh repo view --json url --jq .url) +# Set GITHUB_OUTPUT when run locally for testing +if [[ -z "$GITHUB_OUTPUT" ]]; then + GITHUB_OUTPUT=github-output.txt +fi + # Populates ${PAGES_ROOT}/coverage-reports compile_coverage_reports() { - # We will process all runs up to and including this date. This is chosen to be # slightly less than the artifact retention period for simplicity. local OLDEST=$(date --date="28 days ago" --iso-8601=date) @@ -50,8 +54,11 @@ compile_coverage_reports() { mkdir -p ${COVERAGE_ROOT} # Create index page contents fragment - local CONTENTSS=contents.tmp - echo > ${CONTENTSS} + local CONTENTS=contents.tmp + echo > ${CONTENTS} + + # Run IDs of PR jobs processed + local PR_RUN_IDS="" # Iterate over all unique event types that triggered the workflows for EVENT in $(jq -r 'map(.event) | sort | unique | .[]' completedRuns.json); do @@ -61,19 +68,28 @@ compile_coverage_reports() { EMIT_SECTION_HEADER=1 # For each worfklow run that was triggered by this event type - for WORKFLOW_ID in $(jq ".[] | select(.event == \"${EVENT}\") |.databaseId" completedRuns.json); do - echo "@@@ Processing run ${WORKFLOW_ID}" + for RUN_ID in $(jq ".[] | select(.event == \"${EVENT}\") |.databaseId" completedRuns.json); do + echo "@@@ Processing run ${RUN_ID}" # Extract the info of this run - jq ".[] | select(.databaseId == $WORKFLOW_ID)" completedRuns.json > workflow.json + jq ".[] | select(.databaseId == $RUN_ID)" completedRuns.json > workflow.json jq "." workflow.json + # Record run ID of PR job + if [[ $EVENT == "pull_request" ]]; then + if [[ -z "$PR_RUN_IDS" ]]; then + PR_RUN_IDS="$RUN_ID" + else + PR_RUN_IDS="$PR_RUN_IDS,$RUN_ID" + fi + fi + # Create workflow artifacts directory - local ARTIFACTS_DIR=${ARTIFACTS_ROOT}/${WORKFLOW_ID} + local ARTIFACTS_DIR=${ARTIFACTS_ROOT}/${RUN_ID} mkdir -p ${ARTIFACTS_DIR} # Download artifacts of this run, if exists - gh run download ${WORKFLOW_ID} --name coverage-report --dir ${ARTIFACTS_DIR} || true + gh run download ${RUN_ID} --name coverage-report --dir ${ARTIFACTS_DIR} || true ls -lsha ${ARTIFACTS_DIR} # Move on if no coverage report is available @@ -86,26 +102,34 @@ compile_coverage_reports() { # Emit section header if [[ -n $EMIT_SECTION_HEADER ]]; then unset EMIT_SECTION_HEADER - echo "

Coverage reports for '${EVENT}' runs:

" >> ${CONTENTSS} + if [[ $EVENT == "pull_request" ]]; then + echo "

Patch coverage reports for '${EVENT}' runs:

" >> ${CONTENTS} + else + echo "

Code coverage reports for '${EVENT}' runs:

" >> ${CONTENTS} + fi fi - #Create pages subdirectory - mv ${ARTIFACTS_DIR}/report ${COVERAGE_ROOT}/${WORKFLOW_ID} + # Create pages subdirectory + mv ${ARTIFACTS_DIR}/report ${COVERAGE_ROOT}/${RUN_ID} # Add index page content local WORKFLOW_CREATED=$(jq -r '.createdAt' workflow.json) local WOFKRLOW_NUMBER=$(jq -r '.number' workflow.json) - cat >> ${CONTENTSS} <#${WOFKRLOW_NUMBER} - | GitHub: ${WORKFLOW_ID} + cat >> ${CONTENTS} <#${WOFKRLOW_NUMBER} + | GitHub: ${RUN_ID} | started at: ${WORKFLOW_CREATED} -
CONTENTS_TEMPLATE + if [ -e ${ARTIFACTS_DIR}/pr-number.txt ]; then + local PRNUMBER=$(cat ${ARTIFACTS_DIR}/pr-number.txt) + echo " | Pull request: #${PRNUMBER}" >> ${CONTENTS} + fi + echo "
" >> ${CONTENTS} done # Section break - if [[ -z $EMIT_SECTION_HEADER ]]; then - echo "
" >> ${CONTENTSS} + if [[ -z "$EMIT_SECTION_HEADER" ]]; then + echo "
" >> ${CONTENTS} fi done @@ -118,7 +142,7 @@ CONTENTS_TEMPLATE