#!/usr/bin/env bash
# DESCRIPTION: Verilator: CI script for 'pages.yml', builds the GitHub Pages
#
# SPDX-FileCopyrightText: 2025 Geza Lore
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
# This scipt build the content of the GitHub Pages for the repository.
# Currently this hosts code coverage reports, and PR RTLMeter detailed reports.
# Developer note: You should be able to run this script in your local checkout
# if you have GitHub CLI (command 'gh') setup, authenticated ('gh auth login'),
# and have set a default repository ('gh repo set-default').
# Create pages root directory. The contents of this directory will be deployed
# and served via GitHubPages
readonly PAGES_ROOT=pages
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
# Run IDs of PR jobs processed
PR_RUN_IDS=""
# 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)
# Gather all coverage workflow runs within the time window
gh run list -w coverage.yml --limit 1000 --created ">=${OLDEST}" --json "databaseId,event,status,conclusion,createdAt,number" > recentRuns.json
echo @@@ Recent runs:
jq "." recentRuns.json
# Select completd runs that were not cancelled or skipped, sort by descending run number
jq 'sort_by(-.number) | map(select(.status == "completed" and (.conclusion == "success" or .conclusion == "failure")))' recentRuns.json > completedRuns.json
echo @@@ Completed with success or failure:
jq "." completedRuns.json
# Create artifacts root directory
local ARTIFACTS_ROOT=artifacts
mkdir -p ${ARTIFACTS_ROOT}
# Create coverage reports root directory
local COVERAGE_ROOT=${PAGES_ROOT}/coverage-reports
mkdir -p ${COVERAGE_ROOT}
# Create index page contents fragment
local CONTENTS=contents.tmp
echo > ${CONTENTS}
# Iterate over all unique event types that triggered the workflows
for EVENT in $(jq -r 'map(.event) | sort | unique | .[]' completedRuns.json); do
echo "@@@ Processing '${EVENT}' runs"
# Emit section header if a report exists with this event type
EMIT_SECTION_HEADER=1
# For each worfklow run that was triggered by this event type
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 == $RUN_ID)" completedRuns.json > workflow.json
jq "." workflow.json
# Record run ID of PR job
if [[ $EVENT == "pull_request" ]]; then
PR_RUN_IDS="$PR_RUN_IDS $RUN_ID"
fi
# Create workflow artifacts directory
local ARTIFACTS_DIR=${ARTIFACTS_ROOT}/${RUN_ID}
mkdir -p ${ARTIFACTS_DIR}
# Download artifacts of this run, if exists
gh run download ${RUN_ID} --name coverage-report --dir ${ARTIFACTS_DIR} || true
ls -lsha ${ARTIFACTS_DIR}
# Move on if no coverage report is available
if [ ! -d ${ARTIFACTS_DIR}/report ]; then
echo "No coverage report found"
continue
fi
echo "Coverage report found"
# Emit section header
if [[ -n $EMIT_SECTION_HEADER ]]; then
unset EMIT_SECTION_HEADER
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}/${RUN_ID}
# Add index page content
local WORKFLOW_CREATED=$(jq -r '.createdAt' workflow.json)
local WOFKRLOW_NUMBER=$(jq -r '.number' workflow.json)
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 "
" >> ${CONTENTS}
fi
done
# Write coverage report index.html
cat > ${COVERAGE_ROOT}/index.html <
Verilator CI coverage reports
$(cat ${CONTENTS})
Assembled $(date --iso-8601=minutes --utc)
INDEX_TEMPLATE
# Report size
du -shc ${COVERAGE_ROOT}/*
}
# Populates ${PAGES_ROOT}/rtlmeter-reports
compile_rtlmeter_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)
# Gather all RTLMeter workflow runs within the time window
gh run list -w rtlmeter.yml --limit 1000 --created ">=${OLDEST}" --json "databaseId,event,status,conclusion,createdAt,number" > recentRuns.json
echo @@@ Recent runs:
jq "." recentRuns.json
# Select completd runs that were not cancelled or skipped, sort by descending run number
jq 'sort_by(-.number) | map(select(.status == "completed" and (.conclusion == "success" or .conclusion == "failure")))' recentRuns.json > completedRuns.json
echo @@@ Completed with success or failure:
jq "." completedRuns.json
# Create artifacts root directory
local ARTIFACTS_ROOT=artifacts
mkdir -p ${ARTIFACTS_ROOT}
# Create rtlmeter reports root directory
local RTLMETER_ROOT=${PAGES_ROOT}/rtlmeter-reports
mkdir -p ${RTLMETER_ROOT}
# Create index page contents fragment
local CONTENTS=contents.tmp
echo > ${CONTENTS}
# Iterate over all unique event types that triggered the workflows
for EVENT in $(jq -r 'map(.event) | sort | unique | .[]' completedRuns.json); do
echo "@@@ Processing '${EVENT}' runs"
# Emit section header if a report exists with this event type
EMIT_SECTION_HEADER=1
# For each worfklow run that was triggered by this event type
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 == $RUN_ID)" completedRuns.json > workflow.json
jq "." workflow.json
# Record run ID of PR job
if [[ $EVENT == "pull_request" ]]; then
PR_RUN_IDS="$PR_RUN_IDS $RUN_ID"
fi
# Create workflow artifacts directory
local ARTIFACTS_DIR=${ARTIFACTS_ROOT}/${RUN_ID}
mkdir -p ${ARTIFACTS_DIR}
# Download artifacts of this run, if exists
gh run download ${RUN_ID} --name rtlmeter-pr-report --dir ${ARTIFACTS_DIR} || true
ls -lsha ${ARTIFACTS_DIR}
# Move on if no RTLMeter report is available
if [ ! -f ${ARTIFACTS_DIR}/report.txt ]; then
echo "No RTLMeter report found"
continue
fi
echo "RTLMeter report found"
# Emit section header
if [[ -n $EMIT_SECTION_HEADER ]]; then
unset EMIT_SECTION_HEADER
echo "RTLMeter reports for '${EVENT}' runs:
" >> ${CONTENTS}
fi
# Extract run metadata
local WORKFLOW_CREATED=$(jq -r '.createdAt' workflow.json)
local WOFKRLOW_NUMBER=$(jq -r '.number' workflow.json)
# Wrap the report into an HTML page. The report content is already HTML
# (produced by ci-rtlmeter-pr-report.bash), so we just embed it in the
# page body.
cat > ${RTLMETER_ROOT}/${RUN_ID}.html <
Verilator RTLMeter report #${WOFKRLOW_NUMBER}
$(cat ${ARTIFACTS_DIR}/report.txt)
REPORT_TEMPLATE
# Add index page content
cat >> ${CONTENTS} <#${WOFKRLOW_NUMBER}
| GitHub: ${RUN_ID}
| started at: ${WORKFLOW_CREATED}
CONTENTS_TEMPLATE
echo "
" >> ${CONTENTS}
done
# Section break
if [[ -z "$EMIT_SECTION_HEADER" ]]; then
echo "
" >> ${CONTENTS}
fi
done
# Write rtlmeter report index.html
cat > ${RTLMETER_ROOT}/index.html <
Verilator CI RTLMeter reports
$(cat ${CONTENTS})
Assembled $(date --iso-8601=minutes --utc)
INDEX_TEMPLATE
# Report size
du -shc ${RTLMETER_ROOT}/*
}
# Compilie coverage reports
compile_coverage_reports;
# Compile RTLMeter reports
compile_rtlmeter_reports;
# You can build any other content here to be put under ${PAGES_ROOT}
# Set output to all affected unique PR run IDs (comma separated, no trailing comma)
IDS=$(echo ${PR_RUN_IDS} | xargs -n1 | sort -u | paste -sd,)
echo "pr-run-ids=${IDS}" >> $GITHUB_OUTPUT