dist: the pack as one file to hand over, and the stderr trap behind t… #12
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
| # ============================================================================ | |
| # .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" |