Skip to content

dist: the pack as one file to hand over, and the stderr trap behind t… #12

dist: the pack as one file to hand over, and the stderr trap behind t…

dist: the pack as one file to hand over, and the stderr trap behind t… #12

Workflow file for this run

# ============================================================================
# .github/workflows/distribute.yml - build, verify and publish vn-harness.
#
# WHAT IT BUILDS
# --------------
# A distribution is a FOLDER, not an installer: app/ is a launcher that runs the
# pinned `npx @deepseek-ai/dsh@<pin> web`, and the plugins are installed into the
# harness web profile as LIVE LINKS into packages/. So the artifact is the built
# shell beside the whole pack, plus a zip of the two.
#
# The build, the ship list, the layout and the end-to-end check all live in
# scripts/dist.ps1 (Windows) and scripts/dist.sh (macOS/Linux) - the SAME files
# `distribute.bat` / `./distribute.sh` run on a laptop. This workflow only picks
# the runner, calls them with -Verify, and uploads what they produced, so a local
# run and a CI run cannot drift.
#
# WHAT RUNS ON WHICH EVENT, AND WHY IT IS SPLIT
# ---------------------------------------------
# Standard GitHub-hosted runners are FREE for a public repository, so the runner
# BILL is not the reason for this split - wall-clock time and waste are:
#
# event checks rust tests core legs extra legs -Verify uploads
# push / pull_request yes yes 3 of 6 - linux-x64 no
# workflow_dispatch yes yes 6 of 6 yes all yes
# schedule (weekly) yes yes 6 of 6 yes all yes
# release: published yes yes 6 of 6 yes all yes
#
# A push gets one leg per operating system (win-x64, mac-arm64, linux-x64) and
# uploads nothing: uploading six archives on every push only to expire them is
# the waste this removes. The three remaining legs - mac-x64, and the two ARM64
# ones no other leg can produce - run on the paths that PRODUCE something: a
# manual run, the weekly schedule and a release. The two ARM64 legs are
# REQUIRED, not `continue-on-error`: each has been green on every run since it
# was added, and an artifact that may silently not appear is worse than a leg
# that fails loudly.
#
# WHERE THE MINUTES GO, MEASURED, AND WHAT THAT CHANGED
# ----------------------------------------------------
# The Windows leg sets the wall clock at ~7 minutes, and 275 s of that is the
# `-Verify` install: the installer bootstraps pnpm (9 s) and then fetches the
# harness's OWN dependency closure into a throwaway home (~230 s for ~223 MB,
# npm's and pnpm's stores, with Windows Defender looking at every file), while
# the Rust build plus the assembly is ~95 s. The same verification on Linux is
# a fraction of that. So a push verifies ONCE, on the cheapest leg, instead of
# three times; the package stores are cached (keyed on the pin, so a new harness
# version refills instead of lying); and the legs that produce an artifact - the
# ones where "does this folder actually run" is the question being answered -
# still all verify.
#
# The weekly schedule exists because the runner images move under us: the first
# cut asked for `macos-13`, which had been retired, and no amount of running the
# distributer locally could have caught that - only a runner can. macOS x64 is
# `macos-15-intel`, macOS arm64 is `macos-15`, and the ARM64 Linux image is
# `ubuntu-22.04-arm`; `ubuntu-22.04` is the oldest supported Ubuntu on purpose,
# because a binary built there runs on anything at or above its glibc.
#
# WHAT THE VERIFY STEP PROVES
# ---------------------------
# `-Verify` copies the assembled folder to a temp location, installs it into a
# throwaway DSH_HOME with the distribution's OWN installer, asserts the profile
# now lists every bundle the folder carries, then boots the pinned harness from
# it and waits for the `dsh web:` ready line - token REDACTED in the log, and a
# line that does not name a loopback address refused rather than trusted. It
# never opens a window, which is why a runner with no screen can run it.
#
# AND WHAT IT DELIBERATELY DOES NOT DO
# ------------------------------------
# No .msi / .dmg / .deb / AppImage: app/src-tauri/tauri.conf.json keeps
# `bundle.active: false`, so nothing needs the Tauri CLI, a multi-size icon set
# or a signing identity. macOS therefore ships a bare binary in a zip (no .app
# bundle and no notarization, so Gatekeeper needs a right-click -> Open the
# first time), and Windows ships an unsigned .exe (SmartScreen says "unknown
# publisher"). Both are recorded in docs/DISTRIBUTE.md.
#
# ACTION VERSIONS ARE PINNED, AND TO THE NODE-24 ONES
# ---------------------------------------------------
# The four GitHub-official actions are pinned to a commit SHA (the tag is mutable,
# the commit is not) and each pin is the FIRST release of that action whose
# `runs.using` is `node24` - checkout v5, setup-node v5, upload-artifact v6,
# download-artifact v7. Node 20 actions are force-upgraded by GitHub and warn on
# every job, and the artifact actions' v5 was still node20 by default: v6/v7 are
# the releases that changed it. NOT the latest majors on purpose -
# upload-artifact v7 and download-artifact v8 also move to ESM, add direct
# (unzipped) transfers and turn a digest mismatch into a hard failure, none of
# which this workflow wants.
# ============================================================================
name: distribute
on:
# Manual runs: the way to watch the whole thing work, and to download the
# artifacts, without pushing a tag. `release: true` publishes a GitHub Release
# from the run (the `tag` input names it, or v<pack version> is used).
workflow_dispatch:
inputs:
version:
description: 'Override the pack version used in the artifact names (default: package.json)'
required: false
default: ''
release:
description: 'Publish the artifacts as a GitHub Release when the build is green'
type: boolean
default: false
tag:
description: 'Tag for that release (default: v<pack version>); created from this commit if absent'
required: false
default: ''
# Every push to main that can change what ships: the checks, the Rust tests and
# one build leg per operating system, with no artifact upload. A release is a
# deliberate act (a tag, or the input above), never a side effect of a push.
push:
branches: [main]
paths:
- 'app/**'
- 'packages/**'
- 'scripts/**'
- 'docs/**'
- 'assets/**'
- '.dsh-version.json'
- 'package.json'
- 'README.md'
- 'LICENSE'
- 'SECURITY.md'
- 'install.bat'
- 'install.sh'
- 'uninstall.bat'
- 'uninstall.sh'
- 'run-web.bat'
- 'run-web.sh'
- 'run-desktop.bat'
- 'distribute.bat'
- 'distribute.sh'
- '.gitignore'
- '.github/workflows/distribute.yml'
# Pull requests get the whole push path. No paths: filter here on purpose -
# `paths:` is parsed out of this file by scripts/checks/check-dist-layout.mjs
# and one tuned list beats two that can drift.
pull_request:
# The runner images move; only a runner can notice. Weekly, full matrix,
# publishes nothing.
schedule:
- cron: '17 6 * * 1'
# Publishing a Release builds every matrix target's archive and attaches them
# to it. `gh release create v0.1.1 --generate-notes` is the whole ritual; the
# job below refuses to attach anything when the tag and package.json's version
# disagree, so a release tag can never name a version that was never built.
release:
types: [published]
# Read-only by default; only the release job asks for more, on itself.
permissions:
contents: read
# A superseded push or PR has nothing worth finishing. A release does: cancelling
# one mid-flight would leave a tag with half its assets, so it is never cancelled.
concurrency:
group: distribute-${{ github.ref }}
cancel-in-progress: ${{ github.event_name != 'release' }}
# Colour and backtraces are for the log a failure is read from. The Node version
# and the Rust toolchain are pinned INLINE where they are used (a step's `with:`
# does not get the `env` context reliably), so there are no half-used variables
# here.
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
# ---------------------------------------------------------------------------
# 1. The distribution feature's own invariants, before anything is compiled.
# ---------------------------------------------------------------------------
checks:
name: checks (ship list, bundles, half-to-half parity)
runs-on: ubuntu-22.04
timeout-minutes: 15
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '22'
# This repository is public, and a credential in a commit is on GitHub for
# as long as the repository exists. The check scans exactly what
# `git add -A` would stage and fails on a credential-shaped string, on a
# missing credential ignore rule, and on itself if it stops firing.
- name: Nothing secret reaches a commit
run: node scripts/checks/check-no-secrets.mjs
- name: Distribution layout and payload
run: node scripts/checks/check-dist-layout.mjs
# check-client-bundles.mjs renders every browser half with a REAL React and
# looks for one in $DSH_HOME/profiles/node_modules first, then the npm npx
# caches. A fresh runner has neither and the check THROWS rather than
# skipping, so this installs a React into that first location. Pinned on
# purpose: the pack's bundles are developed against React 18.3.1, and a
# check that renders against a different React is a different check.
- name: The React runtime the client checks render with
run: npm install --prefix "$HOME/.dsh/profiles" --no-save --no-package-lock --no-fund --no-audit react@18.3.1 react-dom@18.3.1
# These two skip loudly where a host cannot run them (a check that cannot
# run says so instead of passing), so they are safe on a bare runner: the
# client half skips the sections that need the harness's own core bundles,
# and the skill check skips every TikZ example when there is no TeX engine.
# check-node-routes.mjs and check-pdf-node.mjs are NOT run here: they want a
# real profile with the harness installed, which this job deliberately does
# not build - run them locally (see scripts/checks/README.md).
- name: Client bundles
run: node scripts/checks/check-client-bundles.mjs
- name: Shipped skill examples
run: node scripts/checks/check-skill-examples.mjs
# The startup window's two halves speak two languages: keystate.rs decides
# and emits a JSON payload, ui/index.html draws the line from it. Nothing
# type-checks across that seam, so this renders the page in both states
# (key loaded / no key) against a DOM stub and asserts the names agree.
- name: The startup window
run: node scripts/checks/check-splash.mjs
# ---------------------------------------------------------------------------
# 2. The shell's own unit tests. They ran NOWHERE before this job: the build
# legs compile the crate with `cargo build --release`, which runs no test,
# and every check above reads the Rust as TEXT. This is the one job that
# executes keystate.rs / readyline.rs / windowstate.rs - including the two
# that feed a key in and assert it never reaches the splash or the console.
# It runs in PARALLEL with the builds (so it costs no wall clock) and the
# release job waits on it, so publishing is gated on it too.
# ---------------------------------------------------------------------------
rust-tests:
name: rust tests (the shell's 56 unit tests)
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
with:
# The toolchain the 0.1.0 release was built with, read out of that
# release's BUILD-INFO.json. Pinned rather than `stable` so two cuts of
# the same commit compile with the same rustc: bump it deliberately,
# not by drift.
toolchain: '1.98.1'
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
workspaces: app/src-tauri
# `cargo test` compiles the whole crate, so this job needs what a Linux
# build needs - the same list the build legs install, for the same reason.
- name: Linux webview dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential file pkg-config \
libwebkit2gtk-4.1-dev libgtk-3-dev libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev librsvg2-dev patchelf
# -locked: the tests must not be the step that quietly moves Cargo.lock.
- name: cargo test
run: cargo test --locked --manifest-path app/src-tauri/Cargo.toml
# ---------------------------------------------------------------------------
# 3a. The core legs: one per operating system, on every event. These are the
# three a push is judged by, and they upload only when the run is one that
# produces something (dispatch / schedule / release).
# ---------------------------------------------------------------------------
build-core:
name: build ${{ matrix.rid }}
needs: checks
runs-on: ${{ matrix.os }}
# 45, not 90: the slowest leg measured 7.5 minutes (Windows, MSVC plus the
# harness dependency closure). A timeout that expires mid-build reads as a
# code failure and is not, so it stays generous - twice the measured worst.
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
# windows-2022 rather than windows-latest (Server 2025): the x64 leg is
# the one users download, so it stays on the image whose WebView2 and
# MSVC story is the most predictable.
- os: windows-2022
rid: win-x64
# macOS arm64 builds natively on the arm64 image; the Intel leg is a
# separate runner (macos-15-intel) because macOS 13 is gone as an image
# and macOS 14 is deprecated - a native build per architecture beats a
# universal binary this project does not attempt.
- os: macos-15
rid: mac-arm64
# The oldest supported Ubuntu: a binary built here runs on anything at
# or above its glibc, which a newer image cannot promise.
#
# `verify: true` makes THIS the leg that proves the distribution boots
# on a push: the installer runs and the pinned harness answers from the
# assembled folder, on the cheapest runner of the three. The Windows
# and macOS legs assemble without it there and verify on every path
# that produces an artifact (dispatch, weekly, release).
- os: ubuntu-22.04
rid: linux-x64
verify: true
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '22'
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
with:
# The toolchain the 0.1.0 release was built with, read out of that
# release's BUILD-INFO.json. Pinned rather than `stable` so two cuts of
# the same commit compile with the same rustc: bump it deliberately,
# not by drift.
toolchain: '1.98.1'
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
workspaces: app/src-tauri
# `-Verify` installs the ENTIRE harness dependency closure (~223 MB) into a
# throwaway home on every run, through two content-addressed stores: npm's
# (`npx` fetches `@deepseek-ai/dsh@<pin>`) and pnpm's (the profile's own
# install). Measured on the Windows leg: 275 s of install, of which ~230 s
# is that closure. Both stores are keyed on the pin, so a new harness
# version misses and refills rather than serving a stale answer.
- name: Prepare the package caches
shell: bash
run: |
mkdir -p "$HOME/.npm"
mkdir -p "$HOME/.local/share/pnpm/store" "$HOME/Library/pnpm/store" "$HOME/AppData/Local/pnpm/store"
- uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: |
~/.npm
~/.local/share/pnpm/store
~/Library/pnpm/store
~/AppData/Local/pnpm/store
key: packages-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.dsh-version.json') }}
restore-keys: |
packages-${{ runner.os }}-${{ runner.arch }}-
# Tauri 2 links against WebKitGTK on Linux. Everything here is needed by
# wry/tao (webkit2gtk-4.1, gtk3, libsoup3, javascriptcoregtk-4.1) or by the
# tauri build script (librsvg, patchelf); `zip` is what scripts/dist.sh
# prefers for the archive, and `lsof` is the fallback it uses to prove a
# verify boot left no server holding its port.
- name: Linux webview dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential file pkg-config \
libwebkit2gtk-4.1-dev libgtk-3-dev libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev librsvg2-dev patchelf \
zip lsof
- name: Build, assemble and verify the distribution (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
VERSION_INPUT: ${{ github.event.inputs.version }}
# `-Verify` is 70% of this leg and is proven once per push on the
# cheapest leg, then on EVERY leg of a run that produces something.
VERIFY: ${{ (github.event_name != 'push' && github.event_name != 'pull_request') || matrix.verify == true }}
run: |
$arguments = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "$env:GITHUB_WORKSPACE/scripts/dist.ps1")
if ($env:VERIFY -eq 'true') { $arguments += '-Verify' }
if ($env:VERSION_INPUT) { $arguments += @('-Version', $env:VERSION_INPUT) }
& pwsh @arguments
exit $LASTEXITCODE
- name: Build, assemble and verify the distribution (macOS / Linux)
if: runner.os != 'Windows'
shell: bash
env:
VERSION_INPUT: ${{ github.event.inputs.version }}
VERIFY: ${{ (github.event_name != 'push' && github.event_name != 'pull_request') || matrix.verify == true }}
run: |
verify_flag=''
if [ "$VERIFY" = 'true' ]; then verify_flag='-Verify'; fi
if [ -n "$VERSION_INPUT" ]; then
sh scripts/dist.sh $verify_flag -Version "$VERSION_INPUT"
else
sh scripts/dist.sh $verify_flag
fi
- name: The release tag must name the version that was built
if: github.event_name == 'release'
shell: bash
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
tag="${RELEASE_TAG#v}"
want=$(node -p "require('./package.json').version")
if [ "$tag" != "$want" ]; then
echo "::error::The release tag is '$RELEASE_TAG' but package.json says '$want'. Bump the version and tag again, or retag the release."
exit 1
fi
# What was built, on the run's own summary page: the pack version, the
# harness pin, the toolchain, the archive and its size. The per-file
# checksums stay in the folder's SHA256SUMS.txt, which is 340 lines.
- name: Summarize the leg
if: always()
shell: bash
run: |
info=$(ls dist/*/BUILD-INFO.json 2>/dev/null | head -n 1 || true)
archive=$(ls dist/*.zip dist/*.tar.gz 2>/dev/null | head -n 1 || true)
onefile=$(ls dist/*.exe dist/*.run 2>/dev/null | head -n 1 || true)
field() { sed -n "s/.*\"$1\": \"\([^\"]*\)\".*/\1/p" "$info" | head -n 1; }
{
echo "### ${{ matrix.rid }}"
echo
if [ -n "$archive" ]; then
printf -- '- archive: `%s` (%s bytes)\n' "$(basename "$archive")" "$(wc -c < "$archive" | tr -d ' ')"
fi
if [ -n "$onefile" ]; then
printf -- '- one file: `%s` (%s bytes)\n' "$(basename "$onefile")" "$(wc -c < "$onefile" | tr -d ' ')"
fi
if [ -n "$info" ]; then
printf -- '- payload: %s files, %s bytes\n' "$(field payloadFiles)" "$(field payloadBytes)"
printf -- '- pack `%s`, harness pin `%s`, commit `%s`, dirty `%s`\n' \
"$(field packVersion)" "$(field dshPin)" "$(field commit)" "$(field dirty)"
printf -- '- toolchain: `%s`, `%s`\n' "$(field rustc)" "$(field node)"
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload the archives
if: github.event_name != 'push' && github.event_name != 'pull_request'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: vn-harness-${{ matrix.rid }}
if-no-files-found: error
retention-days: 14
path: |
dist/*.zip
dist/*.tar.gz
# The single-file build is an artifact like any other, and the leg
# that produces an artifact is the leg that must upload it: without
# these two lines the release would carry the folder and the zip and
# silently omit the one file most people would download.
dist/*.exe
dist/*.run
# ---------------------------------------------------------------------------
# 3b. The extra legs: the ones no core leg can produce - the Intel macOS
# binary, and the two ARM64 ones. They run on the paths that PRODUCE an
# artifact (a manual run, the weekly schedule, a release), never on a push.
# Both ARM64 legs are required now: each has been green on every run since
# it was added, and the `experimental` / continue-on-error staging this
# replaced is what lets an artifact silently not appear. Reverting one to
# optional is a deliberate two-file act - this matrix and the assertion in
# scripts/checks/check-dist-layout.mjs.
# ---------------------------------------------------------------------------
build-extra:
name: build ${{ matrix.rid }} (extra)
needs: checks
if: github.event_name != 'push' && github.event_name != 'pull_request'
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- os: macos-15-intel
rid: mac-x64
# ARM64 Linux: a standard runner since 2025 and free for public
# repositories, on the same old Ubuntu as the x64 leg so the two
# Linux archives agree about glibc.
- os: ubuntu-22.04-arm
rid: linux-arm64
# Windows ARM64 is the newest toolchain of the three, and the one the
# image notice below is about.
- os: windows-11-arm
rid: win-arm64
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '22'
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
with:
# The toolchain the 0.1.0 release was built with, read out of that
# release's BUILD-INFO.json. Pinned rather than `stable` so two cuts of
# the same commit compile with the same rustc: bump it deliberately,
# not by drift.
toolchain: '1.98.1'
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
workspaces: app/src-tauri
# `-Verify` installs the ENTIRE harness dependency closure (~223 MB) into a
# throwaway home on every run, through two content-addressed stores: npm's
# (`npx` fetches `@deepseek-ai/dsh@<pin>`) and pnpm's (the profile's own
# install). Measured on the Windows leg: 275 s of install, of which ~230 s
# is that closure. Both stores are keyed on the pin, so a new harness
# version misses and refills rather than serving a stale answer.
- name: Prepare the package caches
shell: bash
run: |
mkdir -p "$HOME/.npm"
mkdir -p "$HOME/.local/share/pnpm/store" "$HOME/Library/pnpm/store" "$HOME/AppData/Local/pnpm/store"
- uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: |
~/.npm
~/.local/share/pnpm/store
~/Library/pnpm/store
~/AppData/Local/pnpm/store
key: packages-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.dsh-version.json') }}
restore-keys: |
packages-${{ runner.os }}-${{ runner.arch }}-
- name: Linux webview dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential file pkg-config \
libwebkit2gtk-4.1-dev libgtk-3-dev libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev librsvg2-dev patchelf \
zip lsof
- name: Build, assemble and verify the distribution (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
VERSION_INPUT: ${{ github.event.inputs.version }}
# `-Verify` is 70% of this leg and is proven once per push on the
# cheapest leg, then on EVERY leg of a run that produces something.
VERIFY: ${{ (github.event_name != 'push' && github.event_name != 'pull_request') || matrix.verify == true }}
run: |
$arguments = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "$env:GITHUB_WORKSPACE/scripts/dist.ps1")
if ($env:VERIFY -eq 'true') { $arguments += '-Verify' }
if ($env:VERSION_INPUT) { $arguments += @('-Version', $env:VERSION_INPUT) }
& pwsh @arguments
exit $LASTEXITCODE
- name: Build, assemble and verify the distribution (macOS / Linux)
if: runner.os != 'Windows'
shell: bash
env:
VERSION_INPUT: ${{ github.event.inputs.version }}
VERIFY: ${{ (github.event_name != 'push' && github.event_name != 'pull_request') || matrix.verify == true }}
run: |
verify_flag=''
if [ "$VERIFY" = 'true' ]; then verify_flag='-Verify'; fi
if [ -n "$VERSION_INPUT" ]; then
sh scripts/dist.sh $verify_flag -Version "$VERSION_INPUT"
else
sh scripts/dist.sh $verify_flag
fi
- name: The release tag must name the version that was built
if: github.event_name == 'release'
shell: bash
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
tag="${RELEASE_TAG#v}"
want=$(node -p "require('./package.json').version")
if [ "$tag" != "$want" ]; then
echo "::error::The release tag is '$RELEASE_TAG' but package.json says '$want'. Bump the version and tag again, or retag the release."
exit 1
fi
- name: Summarize the leg
if: always()
shell: bash
run: |
info=$(ls dist/*/BUILD-INFO.json 2>/dev/null | head -n 1 || true)
archive=$(ls dist/*.zip dist/*.tar.gz 2>/dev/null | head -n 1 || true)
onefile=$(ls dist/*.exe dist/*.run 2>/dev/null | head -n 1 || true)
field() { sed -n "s/.*\"$1\": \"\([^\"]*\)\".*/\1/p" "$info" | head -n 1; }
{
echo "### ${{ matrix.rid }}"
echo
if [ -n "$archive" ]; then
printf -- '- archive: `%s` (%s bytes)\n' "$(basename "$archive")" "$(wc -c < "$archive" | tr -d ' ')"
fi
if [ -n "$onefile" ]; then
printf -- '- one file: `%s` (%s bytes)\n' "$(basename "$onefile")" "$(wc -c < "$onefile" | tr -d ' ')"
fi
if [ -n "$info" ]; then
printf -- '- payload: %s files, %s bytes\n' "$(field payloadFiles)" "$(field payloadBytes)"
printf -- '- pack `%s`, harness pin `%s`, commit `%s`, dirty `%s`\n' \
"$(field packVersion)" "$(field dshPin)" "$(field commit)" "$(field dirty)"
printf -- '- toolchain: `%s`, `%s`\n' "$(field rustc)" "$(field node)"
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload the archives
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: vn-harness-${{ matrix.rid }}
if-no-files-found: error
retention-days: 14
path: |
dist/*.zip
dist/*.tar.gz
# The single-file build is an artifact like any other, and the leg
# that produces an artifact is the leg that must upload it: without
# these two lines the release would carry the folder and the zip and
# silently omit the one file most people would download.
dist/*.exe
dist/*.run
# ---------------------------------------------------------------------------
# 4. Attach the archives to a GitHub Release (tag or the manual input). It waits
# on the tests as well as the builds, so nothing is published from a tree
# whose own unit tests have not run.
# ---------------------------------------------------------------------------
release:
name: release
needs: [build-core, build-extra, rust-tests]
# `!cancelled() && !failure()` rather than a bare event test: a skipped or
# failed need must not end in "upload whatever survived".
if: |
!cancelled() && !failure() &&
(github.event_name == 'release' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'true'))
runs-on: ubuntu-22.04
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: '22'
- name: Collect the archives
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
path: artifacts
merge-multiple: true
- name: Hash them
run: |
cd artifacts
ls -l
sha256sum vn-harness-* > SHA256SUMS.txt
cat SHA256SUMS.txt
- name: Attach them to the release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
TAG_INPUT: ${{ github.event.inputs.tag }}
run: |
set -eu
tag="${RELEASE_TAG:-$TAG_INPUT}"
if [ -z "$tag" ]; then tag="v$(node -p "require('./package.json').version")"; fi
# A release that does not exist yet is created from this commit; an
# existing one just gets the assets added (or replaced).
gh release view "$tag" >/dev/null 2>&1 || \
gh release create "$tag" --title "$tag" --generate-notes --target "$GITHUB_SHA"
# `artifacts/*`, NOT a list of extensions. Every leg runs the same
# distributer, and dist.sh PREFERS `zip` and writes a `.tar.gz` only
# when zip is absent - and this job installs zip on Linux and macOS
# has it - so `artifacts/*.tar.gz` matched nothing on every cut, and an
# unmatched glob under `set -eu` killed this step: the first release
# built all six targets and then published a release with no assets.
# Whatever was collected is what gets attached, checksums included,
# and a leg that ever does produce a `.tar.gz` is covered by the same
# pattern instead of needing this line edited again.
gh release upload "$tag" artifacts/* --clobber
echo "Attached $(ls artifacts | wc -l) file(s) to $tag."
- name: Summarize the release
if: always()
shell: bash
run: |
{
echo "### Release assets"
echo
echo '| file | bytes |'
echo '|---|---|'
for f in artifacts/*; do
[ -f "$f" ] || continue
printf '| `%s` | %s |\n' "$(basename "$f")" "$(wc -c < "$f" | tr -d ' ')"
done
} >> "$GITHUB_STEP_SUMMARY"