Skip to content

Publish Screenshots #216

Publish Screenshots

Publish Screenshots #216

---
name: Publish Screenshots
on:
workflow_run:
workflows: ["CI"]
types:
- completed
permissions:
actions: read
contents: write
pull-requests: write
statuses: write
jobs:
publish:
if: github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure'
runs-on: ubuntu-latest
steps:
- name: Set Commit Status (In Progress)
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
WORKFLOW_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
with:
script: |
const {
GITHUB_REPOSITORY,
GITHUB_RUN_ID,
GITHUB_SERVER_URL,
WORKFLOW_HEAD_SHA,
} = process.env;
const targetUrl = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`;
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: WORKFLOW_HEAD_SHA,
state: 'pending',
target_url: targetUrl,
description: 'Screenshots publishing in progress',
context: 'publish-screenshots',
});
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
run-id: ${{ github.event.workflow_run.id }}
path: run-screenshots
pattern: tray-screenshots-*
- name: Prepare Matrix Screenshot Directories
run: |
mkdir -p prepared
shopt -s nullglob
for artifact_dir in run-screenshots/tray-screenshots-*; do
[ -d "${artifact_dir}" ] || continue
matrix_name="${artifact_dir##*/tray-screenshots-}"
mkdir -p "prepared/${matrix_name}"
cp -R "${artifact_dir}/." "prepared/${matrix_name}/"
done
if [ -z "$(find prepared -mindepth 1 -print -quit)" ]; then
echo "No screenshots were downloaded from CI artifacts."
exit 1
fi
echo "Prepared screenshot files:"
find prepared -type f | sort
- name: Determine Context
id: context
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const run = context.payload.workflow_run;
const eventName = run.event || '';
const headBranch = run.head_branch || '';
const headRepository = run.head_repository || {};
const headRepositoryName = headRepository.full_name || '';
function normalizePrNumber(value) {
if (value === undefined || value === null || value === '') {
return '';
}
const prNumber = String(value);
if (!/^\d+$/.test(prNumber)) {
throw new Error(`Invalid PR number value: ${prNumber}`);
}
return prNumber;
}
let prNumber = '';
if (eventName === 'pull_request') {
prNumber = normalizePrNumber(run.pull_requests?.[0]?.number);
if (!prNumber) {
const headOwner = headRepository.owner?.login
|| headRepository.owner?.name
|| headRepositoryName.split('/')[0]
|| '';
const head = headOwner && headBranch ? `${headOwner}:${headBranch}` : '';
if (head) {
core.info(`workflow_run.pull_requests is empty; resolving PR from head ${head}.`);
const pullRequests = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head,
sort: 'updated',
direction: 'desc',
per_page: 100,
});
const baseRepository = `${context.repo.owner}/${context.repo.repo}`;
const matchingSha = pullRequests.find((pr) => pr.head?.sha === run.head_sha);
const matchingBase = pullRequests.find((pr) => pr.base?.repo?.full_name === baseRepository);
const pullRequest = matchingSha || matchingBase || pullRequests[0];
prNumber = normalizePrNumber(pullRequest?.number);
}
}
if (!prNumber) {
throw new Error([
'Unable to determine PR number for pull_request workflow_run.',
`head_repository=${headRepositoryName || '<unknown>'}`,
`head_branch=${headBranch || '<unknown>'}`,
`head_sha=${run.head_sha || '<unknown>'}`,
`payload_pull_requests=${run.pull_requests?.length || 0}`,
].join(' '));
}
}
const isPr = eventName === 'pull_request' && prNumber !== '';
const isMasterPush = eventName === 'push' && headBranch === 'master';
core.setOutput('event_name', eventName);
core.setOutput('head_branch', headBranch);
core.setOutput('is_pr', String(isPr));
core.setOutput('pr_number', prNumber);
core.setOutput('is_master_push', String(isMasterPush));
core.info(`event_name=${eventName}`);
core.info(`head_branch=${headBranch}`);
core.info(`is_pr=${isPr}`);
core.info(`pr_number=${prNumber}`);
core.info(`is_master_push=${isMasterPush}`);
- name: Checkout Screenshots Branch
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: screenshots
path: screenshots-repo
- name: Sync Screenshot Content
id: sync
if: steps.context.outputs.is_master_push == 'true' || steps.context.outputs.is_pr == 'true'
env:
PR_NUMBER: ${{ steps.context.outputs.pr_number }}
run: |
target_dir=""
if [ "${{ steps.context.outputs.is_master_push }}" = "true" ]; then
target_dir="screenshots-repo/baseline"
elif [ "${{ steps.context.outputs.is_pr }}" = "true" ]; then
if ! [[ "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then
echo "Invalid PR number: ${PR_NUMBER}"
exit 1
fi
target_dir="screenshots-repo/pull-requests/PR-${PR_NUMBER}"
else
echo "Unsupported workflow context for screenshot sync."
exit 1
fi
mkdir -p "${target_dir}"
# Mirror the prepared set and delete files removed from CI output.
rsync -a --delete prepared/ "${target_dir}/"
echo "target_dir=${target_dir}" >> "${GITHUB_OUTPUT}"
- name: Build PR Screenshot Comparison Comment
if: steps.context.outputs.is_pr == 'true'
env:
BASELINE_ROOT: screenshots-repo/baseline
PR_NUMBER: ${{ steps.context.outputs.pr_number }}
PR_ROOT: prepared
WORKFLOW_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
WORKFLOW_RUN_NUMBER: ${{ github.event.workflow_run.run_number }}
WORKFLOW_RUN_URL: ${{ github.event.workflow_run.html_url }}
run: |
raw_base="https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/screenshots"
baseline_root="${BASELINE_ROOT}"
pr_root="${PR_ROOT}"
cache_buster="${WORKFLOW_HEAD_SHA}"
run_date="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
{
echo "| | |"
echo "| --- | --- |"
printf "| **Last Updated** | %s |\n" "${run_date}"
printf "| **Source Run** | [CI Run #%s](%s) |\n" "${WORKFLOW_RUN_NUMBER}" "${WORKFLOW_RUN_URL}"
printf "| **Commit** | \`%s\` |\n" "${WORKFLOW_HEAD_SHA}"
echo
echo "## Screenshot Comparison"
echo
printf "PR #%s screenshots vs \`screenshots\` baseline.\n\n" "${PR_NUMBER}"
} > pr-comment.md
tmp_baseline="$(mktemp)"
tmp_pr="$(mktemp)"
tmp_matrices="$(mktemp)"
if [ -d "${baseline_root}" ]; then
find "${baseline_root}" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' >> "${tmp_matrices}"
fi
if [ -d "${pr_root}" ]; then
find "${pr_root}" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' >> "${tmp_matrices}"
fi
if [ ! -s "${tmp_matrices}" ]; then
echo "No matrix screenshots were found in baseline or PR artifacts." >> pr-comment.md
rm -f "${tmp_baseline}" "${tmp_pr}" "${tmp_matrices}"
exit 0
fi
while IFS= read -r matrix; do
[ -n "${matrix}" ] || continue
baseline_matrix="${baseline_root}/${matrix}"
pr_matrix="${pr_root}/${matrix}"
: > "${tmp_baseline}"
: > "${tmp_pr}"
if [ -d "${baseline_matrix}" ]; then
(
cd "${baseline_matrix}"
find . -type f | sed 's#^\./##' | LC_ALL=C sort
) > "${tmp_baseline}"
fi
if [ -d "${pr_matrix}" ]; then
(
cd "${pr_matrix}"
find . -type f | sed 's#^\./##' | LC_ALL=C sort
) > "${tmp_pr}"
fi
{
printf "### Matrix: \`%s\`\n\n" "${matrix}"
echo "| Image | Baseline | PR |"
echo "| --- | --- | --- |"
} >> pr-comment.md
tmp_all="$(mktemp)"
cat "${tmp_baseline}" "${tmp_pr}" | LC_ALL=C sort -u > "${tmp_all}"
if [ ! -s "${tmp_all}" ]; then
echo "| _(none)_ | | |" >> pr-comment.md
echo >> pr-comment.md
rm -f "${tmp_all}"
continue
fi
while IFS= read -r rel_file; do
[ -n "${rel_file}" ] || continue
baseline_cell=""
if grep -Fxq "${rel_file}" "${tmp_baseline}"; then
img_src="${raw_base}/baseline/${matrix}/${rel_file}?v=${cache_buster}"
baseline_cell="<img src=\"${img_src}\" width=\"320\" />"
fi
pr_cell=""
if grep -Fxq "${rel_file}" "${tmp_pr}"; then
img_src="${raw_base}/pull-requests/PR-${PR_NUMBER}/${matrix}/${rel_file}?v=${cache_buster}"
pr_cell="<img src=\"${img_src}\" width=\"320\" />"
fi
printf "| \`%s\` | %s | %s |\n" "${rel_file}" "${baseline_cell}" "${pr_cell}" >> pr-comment.md
done < "${tmp_all}"
echo >> pr-comment.md
rm -f "${tmp_all}"
done < <(LC_ALL=C sort -u "${tmp_matrices}")
rm -f "${tmp_baseline}" "${tmp_pr}" "${tmp_matrices}"
echo "Generated pr-comment.md"
- name: Commit and Push Screenshot Changes
id: push
if: steps.context.outputs.is_master_push == 'true' || steps.context.outputs.is_pr == 'true'
env:
PR_NUMBER: ${{ steps.context.outputs.pr_number }}
WORKFLOW_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
cd screenshots-repo
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
commit_message=""
if [ "${{ steps.context.outputs.is_master_push }}" = "true" ]; then
git add -A baseline
commit_message="chore: update screenshots (${WORKFLOW_HEAD_SHA})"
elif [ "${{ steps.context.outputs.is_pr }}" = "true" ]; then
if ! [[ "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then
echo "Invalid PR number: ${PR_NUMBER}"
exit 1
fi
git add -A "pull-requests/PR-${PR_NUMBER}"
commit_message="chore: update PR-${PR_NUMBER} screenshots (${WORKFLOW_HEAD_SHA})"
else
echo "Unsupported workflow context for commit/push."
exit 1
fi
if git diff --cached --quiet; then
echo "has_changes=false" >> "${GITHUB_OUTPUT}"
exit 0
fi
git commit -m "${commit_message}"
git push origin screenshots
echo "has_changes=true" >> "${GITHUB_OUTPUT}"
- name: Post PR Comparison Comment
if: steps.context.outputs.is_pr == 'true'
uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue: ${{ steps.context.outputs.pr_number }}
message-path: pr-comment.md
message-id: screenshot-comparison
- name: Set Commit Status (Complete)
if: always()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
JOB_STATUS: ${{ job.status }}
WORKFLOW_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
with:
script: |
const success = process.env.JOB_STATUS === 'success';
const {
GITHUB_REPOSITORY,
GITHUB_RUN_ID,
GITHUB_SERVER_URL,
WORKFLOW_HEAD_SHA,
} = process.env;
const targetUrl = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`;
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: WORKFLOW_HEAD_SHA,
state: success ? 'success' : 'failure',
target_url: targetUrl,
description: success ? 'Screenshots published successfully' : 'Screenshots publishing failed',
context: 'publish-screenshots',
});