Skip to content

Commit cb6e3bf

Browse files
authored
ci(.github): publish release ISOs as artifacts, share the ISO table
2 parents 3a3bfeb + e3ab0f7 commit cb6e3bf

4 files changed

Lines changed: 236 additions & 116 deletions

File tree

.github/actions/build-iso/action.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ inputs:
5555
one place (the test workflow builds installer + appliance side by side).
5656
required: false
5757
default: ""
58+
system:
59+
description: >-
60+
Runner architecture (e.g. "x86_64-linux"). When set on a full build, record
61+
each built ISO's exact byte size to iso-sizes-<target>-<system>.tsv in the
62+
dist dir (one "system\tfile\tbytes" row per ISO) so the iso-table action
63+
can render sizes + download links. Empty skips size recording.
64+
required: false
65+
default: ""
5866

5967
outputs:
6068
dist:
@@ -73,6 +81,7 @@ runs:
7381
TARGET: ${{ inputs.target }}
7482
FULL: ${{ inputs.full }}
7583
DIST: ${{ inputs.dist }}
84+
SYSTEM: ${{ inputs.system }}
7685
# Read by the Makefile under --impure for the image's pretty version
7786
# name / boot-screen label. Set through env (not inlined into the script)
7887
# so an arbitrary PR title can't break the shell; empty on non-PR events.
@@ -89,6 +98,18 @@ runs:
8998
if [ "$FULL" = "true" ]; then
9099
make "$TARGET/iso"
91100
cp -L "out/$TARGET-iso/iso"/* "$dist/"
101+
# Record this kind's ISO size(s) for the iso-table renderer. Named per
102+
# target+system so parallel builds sharing a dist (or merged from
103+
# separate jobs) never clash. Only the ISO(s) just built are stated,
104+
# not everything already staged in dist.
105+
if [ -n "$SYSTEM" ]; then
106+
meta="$dist/iso-sizes-$TARGET-$SYSTEM.tsv"
107+
: >"$meta"
108+
for iso in "out/$TARGET-iso/iso"/*.iso; do
109+
[ -e "$iso" ] || continue
110+
printf '%s\t%s\t%s\n' "$SYSTEM" "$(basename "$iso")" "$(stat -c %s "$iso")" >>"$meta"
111+
done
112+
fi
92113
else
93114
make "$TARGET/drv"
94115
fi
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Render the ISO build table (kind × arch: exact size + download link) shared by
2+
# the test and release workflows.
3+
#
4+
# The build-iso action writes one `iso-sizes-<target>-<system>.tsv` per built
5+
# ISO; each workflow uploads those as `iso-meta-*` artifacts and downloads them
6+
# back into a single dir (merge-multiple) that this action reads. The table
7+
# links each kind/arch to its ISO artifact's browser download URL, resolved from
8+
# the current run's artifacts — the very artifacts the workflow just uploaded.
9+
# (When box moves ISOs to S3 these links become S3 URLs; only this action and
10+
# the upload steps change.)
11+
#
12+
# The rendered markdown is exposed as the `table` output so callers place it
13+
# wherever they need: the test workflow sets `sticky-comment: true` to upsert a
14+
# single PR comment; the release workflow feeds `table` into the release body.
15+
name: Render ISO table
16+
description: >-
17+
Render the coder/box ISO build table (size + download link per kind × arch)
18+
from per-arch size metadata, and optionally upsert it as a sticky PR comment.
19+
20+
inputs:
21+
meta-dir:
22+
description: >-
23+
Directory holding the per-arch `iso-sizes-<target>-<system>.tsv` rows
24+
(downloaded from the `iso-meta-*` artifacts, merged into one dir).
25+
required: false
26+
default: meta
27+
expiry-days:
28+
description: >-
29+
Artifact retention in days, shown in the table footer so readers know how
30+
long the download links stay live.
31+
required: false
32+
default: "1"
33+
sticky-comment:
34+
description: >-
35+
"true" upserts the table as a sticky pull-request comment (matched by a
36+
hidden marker so every rebuild updates the same comment). "false" only
37+
renders the `table` output.
38+
required: false
39+
default: "false"
40+
github-token:
41+
description: Token used to list run artifacts and (when sticky) upsert the PR comment.
42+
required: false
43+
default: ${{ github.token }}
44+
45+
outputs:
46+
table:
47+
description: The rendered markdown table (without the sticky-comment marker).
48+
value: ${{ steps.render.outputs.table }}
49+
50+
runs:
51+
using: composite
52+
steps:
53+
- name: Render ISO table
54+
id: render
55+
uses: actions/github-script@v7
56+
env:
57+
META_DIR: ${{ inputs.meta-dir }}
58+
EXPIRY_DAYS: ${{ inputs.expiry-days }}
59+
STICKY_COMMENT: ${{ inputs.sticky-comment }}
60+
with:
61+
github-token: ${{ inputs.github-token }}
62+
script: |
63+
const fs = require('fs');
64+
const path = require('path');
65+
66+
const metaDir = process.env.META_DIR || 'meta';
67+
const expiryDays = process.env.EXPIRY_DAYS || '1';
68+
const sticky = process.env.STICKY_COMMENT === 'true';
69+
70+
// 1. Collect exact ISO sizes from the per-arch TSV rows.
71+
const rows = [];
72+
const files = fs.existsSync(metaDir) ? fs.readdirSync(metaDir) : [];
73+
for (const f of files) {
74+
if (!f.startsWith('iso-sizes-') || !f.endsWith('.tsv')) continue;
75+
const text = fs.readFileSync(path.join(metaDir, f), 'utf8');
76+
for (const line of text.split('\n')) {
77+
if (!line.trim()) continue;
78+
const [system, file, bytes] = line.split('\t');
79+
rows.push({ system, file, bytes: Number(bytes) });
80+
}
81+
}
82+
83+
// 2. Map each ISO artifact name -> its browser download URL.
84+
const { owner, repo } = context.repo;
85+
const runId = context.runId;
86+
const arts = await github.paginate(
87+
github.rest.actions.listWorkflowRunArtifacts,
88+
{ owner, repo, run_id: runId, per_page: 100 },
89+
);
90+
const urlFor = (name) => {
91+
const a = arts.find((x) => x.name === name);
92+
return a
93+
? `https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${a.id}`
94+
: null;
95+
};
96+
97+
// Small presentation helpers.
98+
const fmtSize = (b) => {
99+
if (!Number.isFinite(b) || b <= 0) return '—';
100+
const u = ['B', 'KB', 'MB', 'GB', 'TB'];
101+
let i = 0, n = b;
102+
while (n >= 1024 && i < u.length - 1) { n /= 1024; i += 1; }
103+
return `${n.toFixed(i >= 2 ? 2 : 0)} ${u[i]}`;
104+
};
105+
const prettyArch = (s) => s.replace(/-linux$/, '');
106+
const kindOf = (file) => file.includes('-installer-')
107+
? { slug: 'installer', label: 'Installer' }
108+
: { slug: 'appliance', label: 'Appliance' };
109+
110+
// 3. Build the table, sorted by kind (installer before appliance) then
111+
// arch for a stable layout.
112+
const kindRank = (file) => (file.includes('-installer-') ? 0 : 1);
113+
rows.sort((a, b) =>
114+
kindRank(a.file) - kindRank(b.file) || a.system.localeCompare(b.system));
115+
let body = '## 📀 ISO build artifacts\n\n';
116+
if (rows.length === 0) {
117+
body += '_No ISO artifacts were produced in this run._\n';
118+
} else {
119+
body += '| Kind | Arch | Size | Download |\n|:--|:--|--:|:--:|\n';
120+
for (const r of rows) {
121+
const k = kindOf(r.file);
122+
const url = urlFor(`coder-box-${k.slug}-${r.system}`);
123+
const dl = url ? `[⬇️ \`${r.file}\`](${url})` : '—';
124+
body += `| ${k.label} | ${prettyArch(r.system)} | ${fmtSize(r.bytes)} | ${dl} |\n`;
125+
}
126+
}
127+
const sha = (context.payload.pull_request?.head?.sha || context.sha).slice(0, 7);
128+
const expiry = `artifacts expire in ~${expiryDays} day${expiryDays === '1' ? '' : 's'}`;
129+
body += `\n<sub>↻ Updated for \`${sha}\` · `
130+
+ `[run #${context.runNumber}](https://github.com/${owner}/${repo}/actions/runs/${runId}) · `
131+
+ `${expiry} · sign in to GitHub to download.</sub>\n`;
132+
133+
core.setOutput('table', body);
134+
135+
// 4. When requested (and on a PR), upsert the sticky comment matched by
136+
// this hidden marker instead of posting a new one each rebuild.
137+
if (sticky && context.payload.pull_request) {
138+
const MARKER = '<!-- iso-build-table -->';
139+
const commentBody = `${body}\n${MARKER}`;
140+
const issue_number = context.payload.pull_request.number;
141+
const comments = await github.paginate(github.rest.issues.listComments, {
142+
owner, repo, issue_number, per_page: 100,
143+
});
144+
const existing = comments.find((c) => c.body && c.body.includes(MARKER));
145+
if (existing) {
146+
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body: commentBody });
147+
} else {
148+
await github.rest.issues.createComment({ owner, repo, issue_number, body: commentBody });
149+
}
150+
}

.github/workflows/release.yml

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111
# /nix/store cached across runs (nix-community/cache-nix-action). Bare
1212
# `make <target>` resolves to
1313
# the runner's native currentSystem; the resulting ISO + its .sha256 sidecar are
14-
# dereferenced into a host mktemp dir. The release job gathers all ISOs and
15-
# attaches them (with sha256 checksums) to the release.
14+
# dereferenced into a host mktemp dir. Each ISO is published as a GitHub Actions
15+
# artifact (not a release asset: assets are capped at 2 GiB and the ISOs exceed
16+
# it) and the release body carries a table linking each kind/arch to its
17+
# artifact. This is a stopgap until the ISOs move to S3, at which point only the
18+
# upload steps and the iso-table action's links change.
1619

1720
name: Build and release ISO
1821

@@ -93,19 +96,31 @@ jobs:
9396
uses: ./.github/actions/build-iso
9497
with:
9598
target: ${{ matrix.target }}
99+
system: ${{ matrix.system }}
96100

101+
# Publish the ISO + its .sha256 sidecar as one artifact per kind (a single
102+
# upload-artifact step uploads matched files CONCURRENTLY). Named
103+
# coder-box-<target>-<system> so the iso-table renderer can resolve each
104+
# kind/arch to its download URL. Kept for the max public-repo retention.
97105
- name: Upload ISO artifact
98106
uses: actions/upload-artifact@v6
99107
with:
100108
name: coder-box-${{ matrix.target }}-${{ matrix.system }}
101-
path: ${{ steps.build.outputs.dist }}/*.iso
109+
path: |
110+
${{ steps.build.outputs.dist }}/*.iso
111+
${{ steps.build.outputs.dist }}/*.iso.sha256
112+
retention-days: 90
102113
if-no-files-found: error
103114

104-
- name: Upload ISO checksum artifact
115+
# Tiny size metadata (written by build-iso) the release job renders into
116+
# the release-body table. Named per target+system so merge-multiple can't
117+
# clash when both kinds share an arch.
118+
- name: Upload ISO size metadata
105119
uses: actions/upload-artifact@v6
106120
with:
107-
name: coder-box-${{ matrix.target }}-${{ matrix.system }}-sha256
108-
path: ${{ steps.build.outputs.dist }}/*.iso.sha256
121+
name: iso-meta-${{ matrix.target }}-${{ matrix.system }}
122+
path: ${{ steps.build.outputs.dist }}/iso-sizes-*.tsv
123+
retention-days: 90
109124
if-no-files-found: error
110125

111126
release:
@@ -114,7 +129,15 @@ jobs:
114129
runs-on: ubuntu-24.04
115130
permissions:
116131
contents: write
132+
# listWorkflowRunArtifacts (download links) reads the run's artifacts.
133+
actions: read
117134
steps:
135+
# Checkout so the local iso-table composite action is available.
136+
- name: Checkout
137+
uses: actions/checkout@v5
138+
with:
139+
ref: ${{ github.event.inputs.ref }}
140+
118141
- name: Determine release tag
119142
id: tag
120143
run: |
@@ -126,23 +149,30 @@ jobs:
126149
echo "tag=$tag" >>"$GITHUB_OUTPUT"
127150
echo "Releasing tag: $tag"
128151
129-
- name: Download built ISOs
152+
# Only the tiny size metadata is needed here; the multi-GB ISOs stay as
153+
# artifacts and are linked from the release body, never re-downloaded.
154+
- name: Download ISO size metadata
130155
uses: actions/download-artifact@v7
131156
with:
132-
path: dist
157+
path: meta
158+
pattern: iso-meta-*
133159
merge-multiple: true
134160

135-
- name: List release assets
136-
run: ls -lhR dist/
161+
# Same renderer the test workflow uses for its sticky PR comment; here its
162+
# output becomes the release body. 90-day expiry note matches the
163+
# artifacts' retention.
164+
- name: Render ISO table
165+
id: table
166+
uses: ./.github/actions/iso-table
167+
with:
168+
meta-dir: meta
169+
expiry-days: "90"
137170

138171
- name: Create / update GitHub Release
139172
uses: softprops/action-gh-release@v2
140173
with:
141174
tag_name: ${{ steps.tag.outputs.tag }}
142175
name: ${{ steps.tag.outputs.tag }}
176+
body: ${{ steps.table.outputs.table }}
143177
generate_release_notes: true
144178
prerelease: ${{ github.event.inputs.prerelease || false }}
145-
files: |
146-
dist/*.iso
147-
dist/*.iso.sha256
148-
fail_on_unmatched_files: true

0 commit comments

Comments
 (0)