From 11433d001737ce9fdee92dff23e83d2d09a7389c Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:44:05 -0400 Subject: [PATCH] ci: extract reusable actions and workflows Extracts screenshot setup and publishing logic into reusable GitHub Actions components. This adds composite actions for macOS screen-recording permissions and Windows tray/notification setup, introduces a reusable `_publish-screenshots.yml` workflow with inputs (artifact prefix, source run, screenshot branch), and updates `ci.yml` and `publish-screenshots.yml` to call these shared pieces. The result reduces duplication and makes screenshot publishing more configurable and easier to maintain. --- .../action.yml | 54 +++ .../action.yml | 36 ++ .github/workflows/_publish-screenshots.yml | 419 ++++++++++++++++++ .github/workflows/ci.yml | 76 +--- .github/workflows/publish-screenshots.yml | 364 +-------------- 5 files changed, 522 insertions(+), 427 deletions(-) create mode 100644 .github/actions/configure-macos-screen-recording/action.yml create mode 100644 .github/actions/configure-windows-tray-screenshots/action.yml create mode 100644 .github/workflows/_publish-screenshots.yml diff --git a/.github/actions/configure-macos-screen-recording/action.yml b/.github/actions/configure-macos-screen-recording/action.yml new file mode 100644 index 0000000..5c2b68b --- /dev/null +++ b/.github/actions/configure-macos-screen-recording/action.yml @@ -0,0 +1,54 @@ +--- +name: Configure macOS screen recording +description: Configure a GitHub-hosted macOS runner for tray screenshots and UI automation. + +runs: + using: composite + steps: + - name: Configure macOS screen recording + shell: bash + run: | + set -euo pipefail + + brew install cliclick + clickTool="$(command -v cliclick)" + + configure_system_tccdb() { + local values=$1 + local dbPath="/Library/Application Support/com.apple.TCC/TCC.db" + local sqlQuery="INSERT OR IGNORE INTO access VALUES($values);" + sudo sqlite3 -cmd ".timeout 30000" "$dbPath" "$sqlQuery" + } + + configure_user_tccdb() { + local values=$1 + local dbPath="$HOME/Library/Application Support/com.apple.TCC/TCC.db" + local sqlQuery="INSERT OR IGNORE INTO access VALUES($values);" + sqlite3 -cmd ".timeout 30000" "$dbPath" "$sqlQuery" + } + + systemValuesArray=( + "'kTCCServiceScreenCapture','/bin/bash',1,2,0,1,NULL,NULL,NULL,'UNUSED',NULL,0,1599831148" + "'kTCCServicePostEvent','/bin/bash',1,2,0,1,NULL,NULL,NULL,'UNUSED',NULL,0,1599831148" + "'kTCCServicePostEvent','$clickTool',1,2,0,1,NULL,NULL,NULL,'UNUSED',NULL,0,1599831148" + ) + for values in "${systemValuesArray[@]}"; do + configure_system_tccdb "$values,NULL,NULL,'UNUSED',${values##*,}" + done + + userValuesArray=( + "'kTCCServiceScreenCapture','/bin/bash',1,2,0,1,NULL,NULL,NULL,'UNUSED',NULL,0,1583997993" + "'kTCCServicePostEvent','/bin/bash',1,2,0,1,NULL,NULL,NULL,'UNUSED',NULL,0,1583997993" + "'kTCCServicePostEvent','$clickTool',1,2,0,1,NULL,NULL,NULL,'UNUSED',NULL,0,1583997993" + ) + for values in "${userValuesArray[@]}"; do + configure_user_tccdb "$values,NULL,NULL,'UNUSED',${values##*,}" + done + + preflightScreenshot="$RUNNER_TEMP/screen-capture-preflight.png" + screencapture -x "$preflightScreenshot" + sleep 1 + "$clickTool" kp:return + sleep 1 + + echo "macOS screen recording configured." diff --git a/.github/actions/configure-windows-tray-screenshots/action.yml b/.github/actions/configure-windows-tray-screenshots/action.yml new file mode 100644 index 0000000..578304a --- /dev/null +++ b/.github/actions/configure-windows-tray-screenshots/action.yml @@ -0,0 +1,36 @@ +--- +name: Configure Windows tray screenshots +description: Configure a GitHub-hosted Windows runner for visible tray icons and notifications. + +runs: + using: composite + steps: + - name: Configure Windows tray screenshots + shell: pwsh + run: | + echo "::group::Enable all tray icons" + $trayIconScript = Join-Path $env:RUNNER_TEMP "Enable-AllTrayIcons.ps1" + Invoke-WebRequest ` + -Uri "https://raw.githubusercontent.com/paulmann/windows-show-all-tray-icons/main/Enable-AllTrayIcons.ps1" ` + -OutFile $trayIconScript + & $trayIconScript -Action Enable -Force + echo "::endgroup::" + + echo "::group::Disable Do Not Disturb" + Add-Type -AssemblyName System.Windows.Forms + Start-Process "ms-settings:notifications" + Start-Sleep -Seconds 2 + [System.Windows.Forms.SendKeys]::SendWait("{TAB}") + [System.Windows.Forms.SendKeys]::SendWait("{TAB}") + [System.Windows.Forms.SendKeys]::SendWait(" ") + echo "::endgroup::" + + echo "::group::Minimize all windows" + $shell = New-Object -ComObject Shell.Application + $shell.MinimizeAll() + echo "::endgroup::" + + echo "::group::Set Date - Hack for Quiet Time" + $newDate = (Get-Date).AddHours(2) + Set-Date -Date $newDate + echo "::endgroup::" diff --git a/.github/workflows/_publish-screenshots.yml b/.github/workflows/_publish-screenshots.yml new file mode 100644 index 0000000..8225706 --- /dev/null +++ b/.github/workflows/_publish-screenshots.yml @@ -0,0 +1,419 @@ +--- +name: _Publish Screenshots + +on: + workflow_call: + inputs: + artifact-name-prefix: + description: Prefix shared by the screenshot artifact names. + required: true + type: string + screenshot-branch: + default: screenshots + description: Branch used to store baseline and pull request screenshots. + required: false + type: string + source-run-id: + description: Workflow run containing the screenshot artifacts. + required: true + type: number + +permissions: + actions: read + contents: write + pull-requests: write + statuses: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Set Commit Status (In Progress) + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + SOURCE_RUN_ID: ${{ inputs.source-run-id }} + with: + script: | + const { + GITHUB_REPOSITORY, + GITHUB_RUN_ID, + GITHUB_SERVER_URL, + SOURCE_RUN_ID, + } = process.env; + const { data: sourceRun } = await github.rest.actions.getWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: Number(SOURCE_RUN_ID), + }); + 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: sourceRun.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: ${{ inputs.source-run-id }} + path: run-screenshots + pattern: ${{ format('{0}*', inputs.artifact-name-prefix) }} + + - name: Prepare Matrix Screenshot Directories + env: + ARTIFACT_NAME_PREFIX: ${{ inputs.artifact-name-prefix }} + run: | + mkdir -p prepared + + shopt -s nullglob + for artifact_dir in run-screenshots/*; do + [ -d "${artifact_dir}" ] || continue + artifact_name="${artifact_dir##*/}" + matrix_name="${artifact_name#"${ARTIFACT_NAME_PREFIX}"}" + if [ "${matrix_name}" = "${artifact_name}" ] || [ -z "${matrix_name}" ]; then + echo "Unexpected screenshot artifact name: ${artifact_name}" + exit 1 + fi + 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 + env: + SOURCE_RUN_ID: ${{ inputs.source-run-id }} + with: + script: | + const { data: run } = await github.rest.actions.getWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: Number(process.env.SOURCE_RUN_ID), + }); + 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 || ''}`, + `head_branch=${headBranch || ''}`, + `head_sha=${run.head_sha || ''}`, + `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.setOutput('head_sha', run.head_sha || ''); + core.setOutput('run_number', String(run.run_number || '')); + core.setOutput('run_url', run.html_url || ''); + + 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 Screenshot Repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + path: screenshots-repo + + - name: Prepare Screenshot Branch + env: + SCREENSHOT_BRANCH: ${{ inputs.screenshot-branch }} + run: | + cd screenshots-repo + + git check-ref-format --branch "${SCREENSHOT_BRANCH}" + if git show-ref --verify --quiet "refs/remotes/origin/${SCREENSHOT_BRANCH}"; then + git checkout -B "${SCREENSHOT_BRANCH}" "origin/${SCREENSHOT_BRANCH}" + else + git checkout --orphan "${SCREENSHOT_BRANCH}" + git rm -rf . + fi + + - 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 + SCREENSHOT_BRANCH: ${{ inputs.screenshot-branch }} + WORKFLOW_HEAD_SHA: ${{ steps.context.outputs.head_sha }} + WORKFLOW_RUN_NUMBER: ${{ steps.context.outputs.run_number }} + WORKFLOW_RUN_URL: ${{ steps.context.outputs.run_url }} + run: | + raw_base="https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/${SCREENSHOT_BRANCH}" + 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 \`%s\` baseline.\n\n" "${PR_NUMBER}" "${SCREENSHOT_BRANCH}" + } > 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="" + 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="" + 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 }} + SCREENSHOT_BRANCH: ${{ inputs.screenshot-branch }} + WORKFLOW_HEAD_SHA: ${{ steps.context.outputs.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 "${SCREENSHOT_BRANCH}" + echo "has_changes=true" >> "${GITHUB_OUTPUT}" + + - name: Post PR Comparison Comment + if: steps.context.outputs.is_pr == 'true' + uses: mshick/add-pr-comment@ec328af66588ab8f77cdeb2c264f14aba45bbf59 # v3.12.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 }} + SOURCE_RUN_ID: ${{ inputs.source-run-id }} + with: + script: | + const success = process.env.JOB_STATUS === 'success'; + const { + GITHUB_REPOSITORY, + GITHUB_RUN_ID, + GITHUB_SERVER_URL, + SOURCE_RUN_ID, + } = process.env; + const { data: sourceRun } = await github.rest.actions.getWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: Number(SOURCE_RUN_ID), + }); + 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: sourceRun.head_sha, + state: success ? 'success' : 'failure', + target_url: targetUrl, + description: success ? 'Screenshots published successfully' : 'Screenshots publishing failed', + context: 'publish-screenshots', + }); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6496b29..eb4bb4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,7 +112,6 @@ jobs: if: runner.os == 'macOS' run: | dependencies=( - "cliclick" "cmake" "doxygen" "graphviz" @@ -126,50 +125,7 @@ jobs: - name: Configure macOS screen recording if: runner.os == 'macOS' - run: | - set -euo pipefail - - clickTool="$(command -v cliclick)" - - configure_system_tccdb() { - local values=$1 - local dbPath="/Library/Application Support/com.apple.TCC/TCC.db" - local sqlQuery="INSERT OR IGNORE INTO access VALUES($values);" - sudo sqlite3 "$dbPath" "$sqlQuery" - } - - configure_user_tccdb() { - local values=$1 - local dbPath="$HOME/Library/Application Support/com.apple.TCC/TCC.db" - local sqlQuery="INSERT OR IGNORE INTO access VALUES($values);" - sqlite3 "$dbPath" "$sqlQuery" - } - - systemValuesArray=( - "'kTCCServiceScreenCapture','/bin/bash',1,2,0,1,NULL,NULL,NULL,'UNUSED',NULL,0,1599831148" - "'kTCCServicePostEvent','/bin/bash',1,2,0,1,NULL,NULL,NULL,'UNUSED',NULL,0,1599831148" - "'kTCCServicePostEvent','$clickTool',1,2,0,1,NULL,NULL,NULL,'UNUSED',NULL,0,1599831148" - ) - for values in "${systemValuesArray[@]}"; do - configure_system_tccdb "$values,NULL,NULL,'UNUSED',${values##*,}" - done - - userValuesArray=( - "'kTCCServiceScreenCapture','/bin/bash',1,2,0,1,NULL,NULL,NULL,'UNUSED',NULL,0,1583997993" - "'kTCCServicePostEvent','/bin/bash',1,2,0,1,NULL,NULL,NULL,'UNUSED',NULL,0,1583997993" - "'kTCCServicePostEvent','$clickTool',1,2,0,1,NULL,NULL,NULL,'UNUSED',NULL,0,1583997993" - ) - for values in "${userValuesArray[@]}"; do - configure_user_tccdb "$values,NULL,NULL,'UNUSED',${values##*,}" - done - - preflightScreenshot="$RUNNER_TEMP/screen-capture-preflight.png" - screencapture -x "$preflightScreenshot" - sleep 1 - "$clickTool" kp:return - sleep 1 - - echo "macOS screen recording configured." + uses: ./.github/actions/configure-macos-screen-recording - name: Setup Dependencies Windows if: runner.os == 'Windows' @@ -235,35 +191,9 @@ jobs: working-directory: build/tests run: ./test_tray --gtest_color=yes --gtest_filter=TrayTest.TestTrayInit - - name: Configure Windows + - name: Configure Windows tray screenshots if: runner.os == 'Windows' - shell: pwsh - run: | - echo "::group::Enable all tray icons" - Invoke-WebRequest ` - -Uri "https://raw.githubusercontent.com/paulmann/windows-show-all-tray-icons/main/Enable-AllTrayIcons.ps1" ` - -OutFile "Enable-AllTrayIcons.ps1" - .\Enable-AllTrayIcons.ps1 -Action Enable -Force # Enable with comprehensive method (resets ALL icon settings) - echo "::endgroup::" - - echo "::group::Disable Do Not Disturb" - Add-Type -AssemblyName System.Windows.Forms - Start-Process "ms-settings:notifications" - Start-Sleep -Seconds 2 - [System.Windows.Forms.SendKeys]::SendWait("{TAB}") - [System.Windows.Forms.SendKeys]::SendWait("{TAB}") - [System.Windows.Forms.SendKeys]::SendWait(" ") - echo "::endgroup::" - - echo "::group::Minimize all windows" - $shell = New-Object -ComObject Shell.Application - $shell.MinimizeAll() - echo "::endgroup::" - - echo "::group::Set Date - Hack for Quiet Time" - $newDate = (Get-Date).AddHours(2) - Set-Date -Date $newDate - echo "::endgroup::" + uses: ./.github/actions/configure-windows-tray-screenshots - name: Run tests id: test diff --git a/.github/workflows/publish-screenshots.yml b/.github/workflows/publish-screenshots.yml index aee11b1..056cf30 100644 --- a/.github/workflows/publish-screenshots.yml +++ b/.github/workflows/publish-screenshots.yml @@ -7,361 +7,17 @@ on: types: - completed -permissions: - actions: read - contents: write - pull-requests: write - statuses: write +permissions: {} 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 || ''}`, - `head_branch=${headBranch || ''}`, - `head_sha=${run.head_sha || ''}`, - `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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - 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="" - 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="" - 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@ec328af66588ab8f77cdeb2c264f14aba45bbf59 # v3.12.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', - }); + permissions: + actions: read + contents: write + pull-requests: write + statuses: write + uses: ./.github/workflows/_publish-screenshots.yml + with: + artifact-name-prefix: tray-screenshots- + source-run-id: ${{ github.event.workflow_run.id }}