Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/actions/build-iso/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ inputs:
one place (the test workflow builds installer + appliance side by side).
required: false
default: ""
system:
description: >-
Runner architecture (e.g. "x86_64-linux"). When set on a full build, record
each built ISO's exact byte size to iso-sizes-<target>-<system>.tsv in the
dist dir (one "system\tfile\tbytes" row per ISO) so the iso-table action
can render sizes + download links. Empty skips size recording.
required: false
default: ""

outputs:
dist:
Expand All @@ -73,6 +81,7 @@ runs:
TARGET: ${{ inputs.target }}
FULL: ${{ inputs.full }}
DIST: ${{ inputs.dist }}
SYSTEM: ${{ inputs.system }}
# Read by the Makefile under --impure for the image's pretty version
# name / boot-screen label. Set through env (not inlined into the script)
# so an arbitrary PR title can't break the shell; empty on non-PR events.
Expand All @@ -89,6 +98,18 @@ runs:
if [ "$FULL" = "true" ]; then
make "$TARGET/iso"
cp -L "out/$TARGET-iso/iso"/* "$dist/"
# Record this kind's ISO size(s) for the iso-table renderer. Named per
# target+system so parallel builds sharing a dist (or merged from
# separate jobs) never clash. Only the ISO(s) just built are stated,
# not everything already staged in dist.
if [ -n "$SYSTEM" ]; then
meta="$dist/iso-sizes-$TARGET-$SYSTEM.tsv"
: >"$meta"
for iso in "out/$TARGET-iso/iso"/*.iso; do
[ -e "$iso" ] || continue
printf '%s\t%s\t%s\n' "$SYSTEM" "$(basename "$iso")" "$(stat -c %s "$iso")" >>"$meta"
done
fi
else
make "$TARGET/drv"
fi
Expand Down
150 changes: 150 additions & 0 deletions .github/actions/iso-table/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Render the ISO build table (kind × arch: exact size + download link) shared by
# the test and release workflows.
#
# The build-iso action writes one `iso-sizes-<target>-<system>.tsv` per built
# ISO; each workflow uploads those as `iso-meta-*` artifacts and downloads them
# back into a single dir (merge-multiple) that this action reads. The table
# links each kind/arch to its ISO artifact's browser download URL, resolved from
# the current run's artifacts — the very artifacts the workflow just uploaded.
# (When box moves ISOs to S3 these links become S3 URLs; only this action and
# the upload steps change.)
#
# The rendered markdown is exposed as the `table` output so callers place it
# wherever they need: the test workflow sets `sticky-comment: true` to upsert a
# single PR comment; the release workflow feeds `table` into the release body.
name: Render ISO table
description: >-
Render the coder/box ISO build table (size + download link per kind × arch)
from per-arch size metadata, and optionally upsert it as a sticky PR comment.

inputs:
meta-dir:
description: >-
Directory holding the per-arch `iso-sizes-<target>-<system>.tsv` rows
(downloaded from the `iso-meta-*` artifacts, merged into one dir).
required: false
default: meta
expiry-days:
description: >-
Artifact retention in days, shown in the table footer so readers know how
long the download links stay live.
required: false
default: "1"
sticky-comment:
description: >-
"true" upserts the table as a sticky pull-request comment (matched by a
hidden marker so every rebuild updates the same comment). "false" only
renders the `table` output.
required: false
default: "false"
github-token:
description: Token used to list run artifacts and (when sticky) upsert the PR comment.
required: false
default: ${{ github.token }}

outputs:
table:
description: The rendered markdown table (without the sticky-comment marker).
value: ${{ steps.render.outputs.table }}

runs:
using: composite
steps:
- name: Render ISO table
id: render
uses: actions/github-script@v7
env:
META_DIR: ${{ inputs.meta-dir }}
EXPIRY_DAYS: ${{ inputs.expiry-days }}
STICKY_COMMENT: ${{ inputs.sticky-comment }}
with:
github-token: ${{ inputs.github-token }}
script: |
const fs = require('fs');
const path = require('path');

const metaDir = process.env.META_DIR || 'meta';
const expiryDays = process.env.EXPIRY_DAYS || '1';
const sticky = process.env.STICKY_COMMENT === 'true';

// 1. Collect exact ISO sizes from the per-arch TSV rows.
const rows = [];
const files = fs.existsSync(metaDir) ? fs.readdirSync(metaDir) : [];
for (const f of files) {
if (!f.startsWith('iso-sizes-') || !f.endsWith('.tsv')) continue;
const text = fs.readFileSync(path.join(metaDir, f), 'utf8');
for (const line of text.split('\n')) {
if (!line.trim()) continue;
const [system, file, bytes] = line.split('\t');
rows.push({ system, file, bytes: Number(bytes) });
}
}

// 2. Map each ISO artifact name -> its browser download URL.
const { owner, repo } = context.repo;
const runId = context.runId;
const arts = await github.paginate(
github.rest.actions.listWorkflowRunArtifacts,
{ owner, repo, run_id: runId, per_page: 100 },
);
const urlFor = (name) => {
const a = arts.find((x) => x.name === name);
return a
? `https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${a.id}`
: null;
};

// Small presentation helpers.
const fmtSize = (b) => {
if (!Number.isFinite(b) || b <= 0) return '—';
const u = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0, n = b;
while (n >= 1024 && i < u.length - 1) { n /= 1024; i += 1; }
return `${n.toFixed(i >= 2 ? 2 : 0)} ${u[i]}`;
};
const prettyArch = (s) => s.replace(/-linux$/, '');
const kindOf = (file) => file.includes('-installer-')
? { slug: 'installer', label: 'Installer' }
: { slug: 'appliance', label: 'Appliance' };

// 3. Build the table, sorted by kind (installer before appliance) then
// arch for a stable layout.
const kindRank = (file) => (file.includes('-installer-') ? 0 : 1);
rows.sort((a, b) =>
kindRank(a.file) - kindRank(b.file) || a.system.localeCompare(b.system));
let body = '## 📀 ISO build artifacts\n\n';
if (rows.length === 0) {
body += '_No ISO artifacts were produced in this run._\n';
} else {
body += '| Kind | Arch | Size | Download |\n|:--|:--|--:|:--:|\n';
for (const r of rows) {
const k = kindOf(r.file);
const url = urlFor(`coder-box-${k.slug}-${r.system}`);
const dl = url ? `[⬇️ \`${r.file}\`](${url})` : '—';
body += `| ${k.label} | ${prettyArch(r.system)} | ${fmtSize(r.bytes)} | ${dl} |\n`;
}
}
const sha = (context.payload.pull_request?.head?.sha || context.sha).slice(0, 7);
const expiry = `artifacts expire in ~${expiryDays} day${expiryDays === '1' ? '' : 's'}`;
body += `\n<sub>↻ Updated for \`${sha}\` · `
+ `[run #${context.runNumber}](https://github.com/${owner}/${repo}/actions/runs/${runId}) · `
+ `${expiry} · sign in to GitHub to download.</sub>\n`;

core.setOutput('table', body);

// 4. When requested (and on a PR), upsert the sticky comment matched by
// this hidden marker instead of posting a new one each rebuild.
if (sticky && context.payload.pull_request) {
const MARKER = '<!-- iso-build-table -->';
const commentBody = `${body}\n${MARKER}`;
const issue_number = context.payload.pull_request.number;
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number, per_page: 100,
});
const existing = comments.find((c) => c.body && c.body.includes(MARKER));
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body: commentBody });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number, body: commentBody });
}
}
58 changes: 44 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
# /nix/store cached across runs (nix-community/cache-nix-action). Bare
# `make <target>` resolves to
# the runner's native currentSystem; the resulting ISO + its .sha256 sidecar are
# dereferenced into a host mktemp dir. The release job gathers all ISOs and
# attaches them (with sha256 checksums) to the release.
# dereferenced into a host mktemp dir. Each ISO is published as a GitHub Actions
# artifact (not a release asset: assets are capped at 2 GiB and the ISOs exceed
# it) and the release body carries a table linking each kind/arch to its
# artifact. This is a stopgap until the ISOs move to S3, at which point only the
# upload steps and the iso-table action's links change.

name: Build and release ISO

Expand Down Expand Up @@ -93,19 +96,31 @@ jobs:
uses: ./.github/actions/build-iso
with:
target: ${{ matrix.target }}
system: ${{ matrix.system }}

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

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

release:
Expand All @@ -114,7 +129,15 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: write
# listWorkflowRunArtifacts (download links) reads the run's artifacts.
actions: read
steps:
# Checkout so the local iso-table composite action is available.
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.ref }}

- name: Determine release tag
id: tag
run: |
Expand All @@ -126,23 +149,30 @@ jobs:
echo "tag=$tag" >>"$GITHUB_OUTPUT"
echo "Releasing tag: $tag"

- name: Download built ISOs
# Only the tiny size metadata is needed here; the multi-GB ISOs stay as
# artifacts and are linked from the release body, never re-downloaded.
- name: Download ISO size metadata
uses: actions/download-artifact@v7
with:
path: dist
path: meta
pattern: iso-meta-*
merge-multiple: true

- name: List release assets
run: ls -lhR dist/
# Same renderer the test workflow uses for its sticky PR comment; here its
# output becomes the release body. 90-day expiry note matches the
# artifacts' retention.
- name: Render ISO table
id: table
uses: ./.github/actions/iso-table
with:
meta-dir: meta
expiry-days: "90"

- name: Create / update GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: ${{ steps.tag.outputs.tag }}
body: ${{ steps.table.outputs.table }}
generate_release_notes: true
prerelease: ${{ github.event.inputs.prerelease || false }}
files: |
dist/*.iso
dist/*.iso.sha256
fail_on_unmatched_files: true
Loading
Loading