Print Versions #415
Workflow file for this run
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: Print Versions | |
| on: | |
| workflow_dispatch: | |
| workflow_run: | |
| types: [requested, in_progress, completed] | |
| workflows: | |
| - CI | |
| - Build and deploy ASP.Net Core app to Azure Web App - jobflow-api-staging | |
| - Build and deploy ASP.Net Core app to Azure Web App - jobflow-api | |
| concurrency: | |
| group: ${{ github.workflow }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| actions: read | |
| pull-requests: read | |
| issues: read | |
| jobs: | |
| print-version: | |
| name: Print App Versions | |
| runs-on: ubuntu-latest | |
| environment: Version Table | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Build deployment table | |
| id: build-table | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const toCentralUs = (iso) => { | |
| if (!iso) return 'n/a'; | |
| return new Intl.DateTimeFormat('en-US', { | |
| timeZone: 'America/Chicago', | |
| year: 'numeric', | |
| month: '2-digit', | |
| day: '2-digit', | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| second: '2-digit', | |
| hour12: false | |
| }).format(new Date(iso)).replace(',', ''); | |
| }; | |
| // Find open deploy-review issue for linking | |
| let deployReviewUrl = null; | |
| try { | |
| const reviewIssues = await github.rest.issues.listForRepo({ | |
| owner, repo, | |
| labels: 'deploy-review', | |
| state: 'open', | |
| per_page: 1 | |
| }); | |
| if (reviewIssues.data.length > 0) { | |
| deployReviewUrl = reviewIssues.data[0].html_url; | |
| } | |
| } catch (e) { | |
| // No deploy-review issues found | |
| } | |
| const formatEnvCell = (run) => { | |
| if (!run) return 'n/a'; | |
| const when = run.conclusion === 'success' | |
| ? (run.updated_at || run.run_started_at || run.created_at) | |
| : (run.run_started_at || run.created_at || run.updated_at); | |
| let icon = ''; | |
| let state = ''; | |
| if (run.conclusion === 'success') { | |
| icon = '🟢'; | |
| state = 'Deployed'; | |
| } else if (run.status === 'waiting') { | |
| icon = '🟡'; | |
| state = 'Approve Deploy'; | |
| } else if (run.conclusion === 'cancelled') { | |
| icon = '⚪'; | |
| state = 'Cancelled'; | |
| } else if (run.conclusion === 'failure') { | |
| icon = '🔴'; | |
| state = 'Failed'; | |
| } else if (['in_progress', 'queued', 'pending', 'requested'].includes(run.status)) { | |
| icon = '🔵'; | |
| state = 'In Progress'; | |
| } else { | |
| icon = '⚪'; | |
| state = run.conclusion || run.status || 'Unknown'; | |
| } | |
| let url = run.html_url; | |
| if (run.status === 'waiting' && deployReviewUrl) { | |
| url = deployReviewUrl; | |
| } | |
| return `${icon}<br/>[${state}](${url})<br/>${toCentralUs(when)}`; | |
| }; | |
| const extractTicket = (text) => { | |
| if (!text) return 'n/a'; | |
| const match = text.match(/\b(AB#\d+)\b/i); | |
| return match ? match[1].toUpperCase() : 'n/a'; | |
| }; | |
| const workflows = await github.paginate(github.rest.actions.listRepoWorkflows, { | |
| owner, | |
| repo, | |
| per_page: 100 | |
| }); | |
| const findWorkflow = (name) => workflows.find((w) => w.path && w.path.endsWith(name)); | |
| const stagingWf = findWorkflow('staging_jobflow-api.yml'); | |
| const prodWf = findWorkflow('master_jobflow-api.yml'); | |
| const recentRunsFor = async (workflowId) => { | |
| if (!workflowId) return []; | |
| const response = await github.rest.actions.listWorkflowRuns({ | |
| owner, | |
| repo, | |
| workflow_id: workflowId, | |
| per_page: 50 | |
| }); | |
| return response.data.workflow_runs || []; | |
| }; | |
| const stagingRuns = await recentRunsFor(stagingWf && stagingWf.id); | |
| const prodRuns = await recentRunsFor(prodWf && prodWf.id); | |
| // Build a map of prod runs by SHA for quick lookup | |
| const prodRunBySha = {}; | |
| for (const run of prodRuns) { | |
| if (!prodRunBySha[run.head_sha]) { | |
| prodRunBySha[run.head_sha] = run; | |
| } | |
| } | |
| const formatProdCell = (prodRun, stagingRun) => { | |
| const stagingDeployed = stagingRun && stagingRun.conclusion === 'success'; | |
| const prodDeployed = prodRun && prodRun.conclusion === 'success'; | |
| const prodWaiting = prodRun && prodRun.status === 'waiting'; | |
| if (stagingDeployed && !prodDeployed && !prodWaiting) { | |
| const prodWfUrl = prodWf | |
| ? `https://github.com/${owner}/${repo}/actions/workflows/${prodWf.path.replace('.github/workflows/', '')}` | |
| : `https://github.com/${owner}/${repo}/actions`; | |
| const url = deployReviewUrl || (prodRun ? prodRun.html_url : prodWfUrl); | |
| const when = prodRun | |
| ? toCentralUs(prodRun.run_started_at || prodRun.created_at || prodRun.updated_at) | |
| : ''; | |
| const timeLine = when ? `<br/>${when}` : ''; | |
| return `\u{1F7E1}<br/>[Approve Deploy](${url})${timeLine}`; | |
| } | |
| return formatEnvCell(prodRun); | |
| }; | |
| // Dedupe staging runs by SHA, keep latest per SHA | |
| const seenShas = new Set(); | |
| const uniqueStagingRuns = []; | |
| for (const run of stagingRuns) { | |
| if (!seenShas.has(run.head_sha)) { | |
| seenShas.add(run.head_sha); | |
| uniqueStagingRuns.push(run); | |
| } | |
| } | |
| const MAX_ROWS = 10; | |
| const rows = []; | |
| for (const staging of uniqueStagingRuns.slice(0, MAX_ROWS)) { | |
| const sha = staging.head_sha; | |
| const prod = prodRunBySha[sha] || null; | |
| let prCell = 'n/a'; | |
| let ticketCell = 'n/a'; | |
| try { | |
| const prs = await github.rest.repos.listPullRequestsAssociatedWithCommit({ | |
| owner, repo, | |
| commit_sha: sha | |
| }); | |
| if (prs.data.length > 0) { | |
| const pr = prs.data[0]; | |
| prCell = `[#${pr.number}](${pr.html_url})<br/>${pr.title}`; | |
| ticketCell = extractTicket(pr.title); | |
| } | |
| } catch (e) { /* skip */ } | |
| const buildTime = toCentralUs(staging.run_started_at || staging.created_at); | |
| const versionCell = `[${sha.slice(0, 8)}](https://github.com/${owner}/${repo}/commit/${sha})`; | |
| rows.push(`| ${versionCell} | ${prCell} | ${ticketCell} | ${buildTime} | ${formatEnvCell(staging)} | ${formatProdCell(prod, staging)} |`); | |
| } | |
| const table = [ | |
| '# Version Table', | |
| '', | |
| '_All times are in Central US Time_', | |
| '', | |
| '<details open>', | |
| '<summary>api</summary>', | |
| '', | |
| '### api', | |
| '', | |
| '| Version | PR | Ticket | Build Time | staging | production |', | |
| '| --- | --- | --- | --- | --- | --- |', | |
| ...rows, | |
| '', | |
| '</details>' | |
| ].join('\n'); | |
| core.setOutput('table', table); | |
| - name: Print Info | |
| run: printf '%s\n' "${{ steps.build-table.outputs.table }}" >> "$GITHUB_STEP_SUMMARY" |