Skip to content

Print Versions

Print Versions #415

Workflow file for this run

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"