Skip to content

refactor(harness): one runner release listing shape and layout-derived package checks - #74

Open
namikmesic wants to merge 3 commits into
mainfrom
fm/puck-59-release-listing-layout
Open

namikmesic wants to merge 3 commits into
mainfrom
fm/puck-59-release-listing-layout

Conversation

@namikmesic

Copy link
Copy Markdown
Owner

Intent

Deliver puck issue #59, "runner release: one listing shape and one layout table" (#59). Prefactor for the verified-release module. The four record shapes for a release asset (the shared format, the server's download listing, the runner self-update's listing and the app's listing) become one shape in the shared layer with one lenient reader. The runner self-update's shipped-file list and the install script the renderer copies derive from the package layout table instead of restating it. Acceptance criteria: one release asset record type and one listing reader exist in the shared layer; the server, the runner and the app import them; the self-update's shipped-file list and the renderer's install script are generated from the package layout table; a test asserts they match it; release listing tests on the server, the runner and the app pass against the one shape. Closes #59.

What Changed

  • src/harness/runner-releases.ts now holds the only release asset types: ListedRunnerAsset, which is the manifest's RunnerReleaseAsset plus version and url, and RunnerReleaseListing. It also adds readRunnerReleaseListing, the one lenient reader for listings. These replace four separate copies: RunnerAsset/RunnerReleases/readReleases in server-api.ts, ReleaseAsset/Releases in puck-runner/api.ts, and RunnerAsset in server/downloads.ts. The server's /v1/runner/releases route now builds a RunnerReleaseListing, and the app (main/server/api.ts, bridge.ts, renderer runners view) and the runner (ServerApi.releases, which now parses through the reader rather than casting) both read it with that reader. The reader is stricter than the one it replaces: it drops any asset whose os/arch isn't a known RunnerOs/RunnerArch.
  • SHIPPED, the list of files the runner self-update swaps in, now comes from the top level of RUNNER_PACKAGE_ENTRIES instead of a hand-kept list. The renderer's copy-paste install script builds its "required" and "executable" file checks from the same table. As a result the script also requires README.md, LICENSE and bin/node.LICENSE.
  • Tests check these against the layout table. SHIPPED must match the layout's top level, and a test swaps in a full package. The install script's required and executable lists must come from the table, with one "missing file" case per regular file. The shared reader has its own coverage, and the existing server, runner and app listing tests now run against the one shape.

Closes #59.

🤖 Generated with Claude Code

Risk Assessment

✅ Low: A type-and-reader consolidation prefactor: the one shared ListedRunnerAsset/RunnerReleaseListing shape and readRunnerReleaseListing are imported by the server, the runner and the app, the reader's lenient semantics match the old app-side reader (os/arch now narrowed to values the server's filename regex already guarantees), SHIPPED and the install script's required/executable lists derive from RUNNER_PACKAGE_ENTRIES and are proven by behavioral tests (real swapIn and sandboxed script execution), and no acceptance criterion is contradicted.

Testing

I stood up the real Puck server built from this branch, serving freshly packaged runner tarballs. I drove the release listing through the server's HTTP route, the runner's ServerApi and the app's shared reader, and confirmed all three agree on one shape. I ran the renderer-generated install script live against the served package and installed a working 0.1.0 runner. Against a tampered package missing README.md, the same script failed with "Runner package is missing README.md." and left nothing behind; the old hand-written list did not include that file. I also performed a real runner self-update whose swapped files are exactly the layout table's top level, with the registration kept and the previous files preserved. The targeted release-listing and layout test files pass. On this heavily loaded host (load average about 20), runners-view hits shell-spawn timeouts in tests this change did not touch. Those tests fail identically at the base commit, and all 99 pass with a longer timeout. No screenshot was taken: the app's Add-runner dialog needs a signed-in Puck/GitHub session, which isn't available here. The command that dialog copies was executed live instead.

  • Live validation: ✅ go - 5 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Operator queries a development server's GET /v1/runner/releases and gets one listing shape (latest, minVersion, assets of os/arch/version/file/url/sha256/size) whose sha256s match the packaged tarball… ✅ pass live listing-good.http and listing.txt: 200 JSON with latest 0.1.0, minVersion 0.0.5, two assets with exactly those keys; listed sha256 equals each packaged .sha256 file
The runner (ServerApi.releases) and the app (readRunnerReleaseListing, as src/main/server/api.ts uses it) read the live listing through the one shared reader and get identical results ✅ pass live listing.txt: 'app readRunnerReleaseListing(body) equals runner side: true'
User runs the install script the renderer copies against the live server; the script checks every regular file and executable in RUNNER_PACKAGE_ENTRIES and publishes a working runner ✅ pass live install.txt: generated for-required list equals the layout's 9 regular files and the for-executable list its 4 executables; exit 0; installed runner reports {"version":"0.1.0","trustMode":"development…
Adversarial: the server serves a package missing README.md (a layout file the old hand-written check list skipped); the generated install script refuses it and publishes nothing ✅ pass live install-missing.txt: exit 1, stderr ends 'Runner package is missing README.md.', directory empty afterwards
An installed 0.0.9 runner finds 0.1.0 on the live server and self-updates; exactly the layout's top-level entries (SHIPPED) are swapped in, the registration stays, the previous files are kept, and the… ✅ pass live self-update.txt: SHIPPED [config.sh,run.sh,svc.sh,VERSION,README.md,LICENSE,bin]; VERSION 0.1.0; bin/node.LICENSE, README.md and LICENSE replaced; .runner kept; _update/previous holds all 7; new runne…
The Settings > Runners Add-runner dialog in the Electron app shows the layout-derived install commands for the server's package ⏸️ untested no Opening the dialog requires a signed-in Puck session through the server's GitHub App web flow. No GitHub App credentials or signed-in session are available in this isolated run. To drive it, provide a…
Evidence: Live server release listing (HTTP response)
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 578
Cache-Control: no-store
X-Content-Type-Options: nosniff
Date: Sun, 04 Oct 2026 21:34:20 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"latest":"0.1.0","minVersion":"0.0.5","assets":[{"os":"linux","arch":"x64","version":"0.1.0","file":"puck-runner-linux-x64-0.1.0.tar.gz","url":"http://localhost:18089/runner/0.1.0/puck-runner-linux-x64-0.1.0.tar.gz","sha256":"7345cde91a06bab720c67f1e9a9df73ae28fd94d98487ff7b5666c4db78125de","size":43766084},{"os":"macos","arch":"arm64","version":"0.1.0","file":"puck-runner-macos-arm64-0.1.0.tar.gz","url":"http://localhost:18089/runner/0.1.0/puck-runner-macos-arm64-0.1.0.tar.gz","sha256":"0456afc22cfa2ccf9c4740f6ab631b771d2f5fb7d81b71a240e14d9b6599eb5e","size":36985760}]}
Evidence: Runner and app read the live listing identically
runner ServerApi.releases(): {
  "latest": "0.1.0",
  "minVersion": "0.0.5",
  "assets": [
    {
      "os": "linux",
      "arch": "x64",
      "version": "0.1.0",
      "file": "puck-runner-linux-x64-0.1.0.tar.gz",
      "url": "http://localhost:18089/runner/0.1.0/puck-runner-linux-x64-0.1.0.tar.gz",
      "sha256": "7345cde91a06bab720c67f1e9a9df73ae28fd94d98487ff7b5666c4db78125de",
      "size": 43766084
    },
    {
      "os": "macos",
      "arch": "arm64",
      "version": "0.1.0",
      "file": "puck-runner-macos-arm64-0.1.0.tar.gz",
      "url": "http://localhost:18089/runner/0.1.0/puck-runner-macos-arm64-0.1.0.tar.gz",
      "sha256": "0456afc22cfa2ccf9c4740f6ab631b771d2f5fb7d81b71a240e14d9b6599eb5e",
      "size": 36985760
    }
  ]
}
app readRunnerReleaseListing(body) equals runner side: true
  linux-x64: listed sha256 matches packaged .sha256 file: true; keys=os,arch,version,file,url,sha256,size
  macos-arm64: listed sha256 matches packaged .sha256 file: true; keys=os,arch,version,file,url,sha256,size
Evidence: Generated install script (layout-derived checks) installs a working runner
generated layout checks:
  for required in 'config.sh' 'run.sh' 'svc.sh' 'VERSION' 'README.md' 'LICENSE' 'bin/node' 'bin/node.LICENSE' 'bin/puck-runner.cjs'; do
  for executable in 'config.sh' 'run.sh' 'svc.sh' 'bin/node'; do
layout regular files: config.sh run.sh svc.sh VERSION README.md LICENSE bin/node bin/node.LICENSE bin/puck-runner.cjs
install script exit=0 stderr="% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r  5 35.2M    5 2164k    0     0  3270k      0  0:00:11 --:--:--  0:00:11 3268k\r 15 35.2M   15 5464k    0     0  2773k      0  0:00:13  0:00:01  0:00:12 2773k\r 21 35.2M   21 7664k    0     0  2382k      0  0:00:15  0:00:03  0:00:12 2383k\r 24 35.2M   24 8764k    0     0  2303k      0  0:00:15  0:00:03  0:00:12 2303k\r 30 35.2M   30 10.7M    0     0  2313k      0  0:00:15  0:00:04  0:00:11 2313k\r 36 35.2M   36 12.8M    0     0  2295k      0  0:00:15  0:00:05  0:00:10 2168k\r 42 35.2M   42 15.0M    0     0  2074k      0  0:00:17  0:00:07  0:00:10 1820k\r 45 35.2M   45 16.0M    0     0  2134k      0  0:00:16  0:00:07  0:00:09 1957k\r 48 35.2M   48 17.1M    0     0  2006k      0  0:00:17  0:00:08  0:00:09 1778k\r 51 35.2M   51 18.2M    0     0  1906k      0  0:00:18  0:00:09  0:00:09 1525k\r 57 35.2M   57 20.3M    0     0  1909k      0  0:00:18  0:00:10  0:00:08 1484k\r 63 35.2M   63 22.5M    0     0  1948k      0  0:00:18  0:00:11  0:00:07 1738k\r 72 35.2M   72 25.7M    0     0  2034k      0  0:00:17  0:00:12  0:00:05 1886k\r 79 35.2M   79 27.8M    0     0  2078k      0  0:00:17  0:00:13  0:00:04 2205k\r 85 35.2M   85 30.0M    0     0  1952k      0  0:00:18  0:00:15  0:00:03 2026k\r100 35.2M  100 35.2M    0     0  2224k      0  0:00:16  0:00:16 --:--:-- 2871k"
installed tree: ./LICENSE ./README.md ./VERSION ./bin/node ./bin/node.LICENSE ./bin/puck-runner.cjs ./config.sh ./puck-runner-macos-arm64-0.1.0.tar.gz ./run.sh ./svc.sh
(node:3406) ExperimentalWarning: SQLite is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
installed runner version: {"version":"0.1.0","trustMode":"development","runnerProtocol":1}
leftover staging dirs: 0
Evidence: Generated install script refuses a package missing README.md
served package: puck-runner-macos-arm64-0.1.1.tar.gz sha256 a306e3bee1540f9edf115c348b10d1b025a738542a867a0a0f2f5fc9cc2bdeca
install script exit=1 stderr="% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r  5 35.4M    5 2164k    0     0  1655k      0  0:00:21  0:00:01  0:00:20 1655k\r  9 35.4M    9 3264k    0     0  1609k      0  0:00:22  0:00:02  0:00:20 1609k\r 27 35.4M   27 9984k    0     0  3322k      0  0:00:10  0:00:03  0:00:07 3322k\r 33 35.4M   33 11.8M    0     0  3063k      0  0:00:11  0:00:03  0:00:08 3062k\r 39 35.4M   39 14.0M    0     0  2945k      0  0:00:12  0:00:04  0:00:08 2945k\r 45 35.4M   45 16.1M    0     0  2604k      0  0:00:13  0:00:06  0:00:07 2849k\r 48 35.4M   48 17.2M    0     0  2487k      0  0:00:14  0:00:07  0:00:07 2838k\r 51 35.4M   51 18.3M    0     0  2340k      0  0:00:15  0:00:08  0:00:07 1752k\r 54 35.4M   54 19.4M    0     0  2234k      0  0:00:16  0:00:08  0:00:08 1564k\r 69 35.4M   69 24.5M    0     0  2436k      0  0:00:14  0:00:10  0:00:04 1979k\r 93 35.4M   93 33.0M    0     0  2845k      0  0:00:12  0:00:11  0:00:01 3123k\r 99 35.4M   99 35.1M    0     0  2500k      0  0:00:14  0:00:14 --:--:-- 2512k\r100 35.4M  100 35.4M    0     0  2514k      0  0:00:14  0:00:14 --:--:-- 2734k\nRunner package is missing README.md."
directory after the failed install: []
Evidence: Real self-update swaps the layout's top-level files
SHIPPED (derived from layout): ["config.sh","run.sh","svc.sh","VERSION","README.md","LICENSE","bin"]
findUpdate -> {"os":"macos","arch":"arm64","version":"0.1.0","file":"puck-runner-macos-arm64-0.1.0.tar.gz","url":"http://localhost:18089/runner/0.1.0/puck-runner-macos-arm64-0.1.0.tar.gz","sha256":"0456afc22cfa2ccf9c4740f6ab631b771d2f5fb7d81b71a240e14d9b6599eb5e","size":36985760}
  [runner log] update.download {"version":"0.1.0","file":"puck-runner-macos-arm64-0.1.0.tar.gz"}
  [runner log] update.installed {"from":"0.0.9","to":"0.1.0"}
runner dir after update: [".runner","LICENSE","README.md","VERSION","_update","bin","config.sh","run.sh","svc.sh"]
VERSION now: 0.1.0
new bin/node.LICENSE swapped in: true
README.md and LICENSE swapped in: true
registration kept: {"registration":"keep me"}
previous kept: ["LICENSE","README.md","VERSION","bin","config.sh","run.sh","svc.sh"]
(node:20604) ExperimentalWarning: SQLite is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
new runner reports: 0.1.0
findUpdate at 0.1.0 -> null
Evidence: Live driver script
// Live driver: reads the running Puck server's release listing the way the
// runner and the app do, runs the install script the renderer copies
// against the served package, and performs a real runner self-update.
import { execFileSync, spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';

const W = '~/.no-mistakes/worktrees/81e07ded9ba4/01M44CKPQCZ2BNCSTWTHRBW362/src';
const { readRunnerReleaseListing, RUNNER_PACKAGE_ENTRIES } = await import(`${W}/harness/runner-releases.ts`);
const { ServerApi } = await import(`${W}/puck-runner/api.ts`);
const { findUpdate, applyUpdate, SHIPPED } = await import(`${W}/puck-runner/update.ts`);
const { runnerPaths } = await import(`${W}/puck-runner/files.ts`);
const { assetFor, commandsFor } = await import(`${W}/renderer/settings/runners.ts`);

const GOOD = 'http://localhost:18089';
const BAD = 'http://localhost:18090';
const scenario = process.argv[2];
const log = { info: (m: string, f?: object) => console.log(`  [runner log] ${m} ${JSON.stringify(f ?? {})}`), warn: () => {}, error: () => {} };

function runInstall(serverUrl: string, asset: any, cwd: string) {
  const { download } = commandsFor(asset, { serverUrl, token: 'tok-not-used' });
  const r = spawnSync('sh', ['-c', download[0]], { cwd, encoding: 'utf8' });
  return r;
}

if (scenario === 'listing') {
  const runnerSide = await new ServerApi(GOOD).releases();
  const appSide = readRunnerReleaseListing(await (await fetch(`${GOOD}/v1/runner/releases`)).json());
  console.log('runner ServerApi.releases():', JSON.stringify(runnerSide, null, 2));
  console.log('app readRunnerReleaseListing(body) equals runner side:', JSON.stringify(appSide) === JSON.stringify(runnerSide));
  for (const a of runnerSide.assets) {
    const sumLine = fs.readFileSync(`~/.no-mistakes/evidence/01M44CKPQCZ2BNCSTWTHRBW362/downloads/0.1.0/${a.file}.sha256`, 'utf8');
    console.log(`  ${a.os}-${a.arch}: listed sha256 matches packaged .sha256 file: ${sumLine.startsWith(a.sha256)}; keys=${Object.keys(a).join(',')}`);
  }
}

if (scenario === 'install') {
  const listing = readRunnerReleaseListing(await (await fetch(`${GOOD}/v1/runner/releases`)).json());
  const asset = assetFor(listing.assets, 'macos-arm64');
  const { download } = commandsFor(asset, { serverUrl: GOOD, token: 'tok' });
  const checks = download[0].split('\n').filter((l: string) => /^for (required|executable)/.test(l));
  console.log('generated layout checks:\n  ' + checks.join('\n  '));
  console.log('layout regular files:', RUNNER_PACKAGE_ENTRIES.filter((e: any) => e.type === 'file').map((e: any) => e.name).join(' '));
  const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'puck-install-'));
  const r = runInstall(GOOD, asset, cwd);
  console.log(`install script exit=${r.status} stderr=${JSON.stringify(r.stderr.trim())}`);
  console.log('installed tree:', execFileSync('find', ['.', '-type', 'f'], { cwd: path.join(cwd, 'puck-runner'), encoding: 'utf8' }).trim().split('\n').sort().join(' '));
  console.log('installed runner version:', execFileSync('./puck-runner/bin/node', ['./puck-runner/bin/puck-runner.cjs', 'version', '--json'], { cwd, encoding: 'utf8' }).trim());
  console.log('leftover staging dirs:', fs.readdirSync(cwd).filter((n) => n.startsWith('.puck-runner')).length);
  fs.rmSync(cwd, { recursive: true, force: true });
}

if (scenario === 'install-missing') {
  const listing = readRunnerReleaseListing(await (await fetch(`${BAD}/v1/runner/releases`)).json());
  const asset = assetFor(listing.assets, 'macos-arm64');
  console.log('served package:', asset.file, 'sha256', asset.sha256);
  const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'puck-install-'));
  const r = runInstall(BAD, asset, cwd);
  console.log(`install script exit=${r.status} stderr=${JSON.stringify(r.stderr.trim())}`);
  console.log('directory after the failed install:', JSON.stringify(fs.readdirSync(cwd)));
  fs.rmSync(cwd, { recursive: true, force: true });
}

if (scenario === 'self-update') {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'puck-selfupdate-'));
  const paths = runnerPaths(path.join(root, 'runner'));
  // An installed 0.0.9 runner: every layout entry with "old" contents, plus registration state.
  fs.mkdirSync(paths.root, { recursive: true });
  for (const e of RUNNER_PACKAGE_ENTRIES) {
    if (e.type === 'dir') fs.mkdirSync(path.join(paths.root, e.name), { recursive: true });
    else fs.writeFileSync(path.join(paths.root, e.name), e.name === 'VERSION' ? '0.0.9\n' : `old ${e.name}\n`);
  }
  fs.writeFileSync(paths.config, '{"registration":"keep me"}');
  console.log('SHIPPED (derived from layout):', JSON.stringify(SHIPPED));
  const deps = { api: new ServerApi(GOOD), paths, log, version: '0.0.9', os: 'macos', arch: 'arm64' } as any;
  const asset = await findUpdate(deps);
  console.log('findUpdate ->', JSON.stringify(asset));
  await applyUpdate(deps, asset);
  console.log('runner dir after update:', JSON.stringify(fs.readdirSync(paths.root).sort()));
  console.log('VERSION now:', fs.readFileSync(path.join(paths.root, 'VERSION'), 'utf8').trim());
  console.log('new bin/node.LICENSE swapped in:', !fs.readFileSync(path.join(paths.root, 'bin/node.LICENSE'), 'utf8').startsWith('old'));
  console.log('README.md and LICENSE swapped in:', ['README.md', 'LICENSE'].every((f) => !fs.readFileSync(path.join(paths.root, f), 'utf8').startsWith('old')));
  console.log('registration kept:', fs.readFileSync(paths.config, 'utf8'));
  console.log('previous kept:', JSON.stringify(fs.readdirSync(path.join(paths.update, 'previous')).sort()));
  console.log('new runner reports:', execFileSync(path.join(paths.root, 'bin/node'), [path.join(paths.root, 'bin/puck-runner.cjs'), 'version'], { encoding: 'utf8' }).trim());
  const again = await findUpdate({ ...deps, version: '0.1.0' });
  console.log('findUpdate at 0.1.0 ->', JSON.stringify(again));
  fs.rmSync(root, { recursive: true, force: true });
}
Evidence: Generated layout checks
for required in 'config.sh' 'run.sh' 'svc.sh' 'VERSION' 'README.md' 'LICENSE' 'bin/node' 'bin/node.LICENSE' 'bin/puck-runner.cjs'; do
for executable in 'config.sh' 'run.sh' 'svc.sh' 'bin/node'; do
- Outcome: ⚠️ 1 info across 1 run (40m29s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ src/harness/runner-releases.ts:459 - isRunnerOs and isRunnerArch are a fresh spelling of the supported os/arch set that the module already defines in RUNNER_TARGETS (and that runnerTargetFor at line 105-106 also inlines). The listing reader could derive them, e.g. RUNNER_TARGETS.some((t) => t.os === v) / .some((t) => t.arch === v), so a future target is added in one table. Not a defect: the server's FILE_RE in src/server/downloads.ts:28 emits exactly these values, and the independent os/arch check matches the server's regex semantics. Mechanical dedup only.
⚠️ **Test** - 1 info
  • ℹ️ test/unit/runners-view.test.ts:264 - On this host under heavy load, test/unit/runners-view.test.ts 'installs with https/http transport', 'cleans staging after … failures' and 'checks prerequisites' time out at the default 5s (and the sandbox kills each shell at 10s), because each test spawns many shells. The same tests fail identically at the base commit 0e6add2, so this is a host-speed flake, not a regression. All 99 pass with --testTimeout=120000.
  • Live validation: ✅ go - 5 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Operator queries a development server's GET /v1/runner/releases and gets one listing shape (latest, minVersion, assets of os/arch/version/file/url/sha256/size) whose sha256s match the packaged tarball… ✅ pass live listing-good.http and listing.txt: 200 JSON with latest 0.1.0, minVersion 0.0.5, two assets with exactly those keys; listed sha256 equals each packaged .sha256 file
The runner (ServerApi.releases) and the app (readRunnerReleaseListing, as src/main/server/api.ts uses it) read the live listing through the one shared reader and get identical results ✅ pass live listing.txt: 'app readRunnerReleaseListing(body) equals runner side: true'
User runs the install script the renderer copies against the live server; the script checks every regular file and executable in RUNNER_PACKAGE_ENTRIES and publishes a working runner ✅ pass live install.txt: generated for-required list equals the layout's 9 regular files and the for-executable list its 4 executables; exit 0; installed runner reports {"version":"0.1.0","trustMode":"development…
Adversarial: the server serves a package missing README.md (a layout file the old hand-written check list skipped); the generated install script refuses it and publishes nothing ✅ pass live install-missing.txt: exit 1, stderr ends 'Runner package is missing README.md.', directory empty afterwards
An installed 0.0.9 runner finds 0.1.0 on the live server and self-updates; exactly the layout's top-level entries (SHIPPED) are swapped in, the registration stays, the previous files are kept, and the… ✅ pass live self-update.txt: SHIPPED [config.sh,run.sh,svc.sh,VERSION,README.md,LICENSE,bin]; VERSION 0.1.0; bin/node.LICENSE, README.md and LICENSE replaced; .runner kept; _update/previous holds all 7; new runne…
The Settings > Runners Add-runner dialog in the Electron app shows the layout-derived install commands for the server's package ⏸️ untested no Opening the dialog requires a signed-in Puck session through the server's GitHub App web flow. No GitHub App credentials or signed-in session are available in this isolated run. To drive it, provide a…
  • npm run build:server then two live servers: PUCK_DEVELOPMENT=true PUCK_RUNNER_DOWNLOADS=<evidence>/downloads PUCK_RUNNER_MIN_VERSION=0.0.5 node .webpack/server/puck-server.js (port 18089) and a second one serving a tampered 0.1.1 package (port 18090)
  • node scripts/package-runner.mjs --mode development --targets macos-arm64,linux-x64 --out <evidence>/downloads (real runner tarballs with bundled Node)
  • curl -i http://localhost:18089/v1/runner/releases (server listing shape)
  • jiti drive.ts listing: runner ServerApi.releases() and app readRunnerReleaseListing against the live listing, sha256 cross-checked with the packaged .sha256 files
  • jiti drive.ts install: renderer commandsFor(assetFor(listing,'macos-arm64')) download block executed with sh against the live server, then the installed bin/puck-runner.cjs version --json run
  • jiti drive.ts install-missing: the same generated script against a live-served package missing README.md
  • jiti drive.ts self-update: findUpdate + applyUpdate (real tar, real sha256 check, real smoke test of the new bundled node) from 0.0.9 to the served 0.1.0
  • npx vitest run test/unit/runner-releases.test.ts test/unit/runner-release-download.test.ts test/unit/runner-update.test.ts test/unit/server-downloads.test.ts (pass)
  • npx vitest run test/unit/runners-view.test.ts -t "layout table|never publishes|cannot execute" (30 layout-derived tests pass)
  • npx vitest run test/unit/runners-view.test.ts --testTimeout=120000 (99/99 pass)
  • Base-commit comparison: git checkout 0e6add2 -- src test then the same failing runners-view tests (they time out identically), restored with git checkout HEAD -- src test
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Namik Mesic added 3 commits October 4, 2026 23:20
The server's download listing, the runner self-update's listing and the
app's listing each declared their own asset record. They now share
ListedRunnerAsset and RunnerReleaseListing in src/harness/runner-releases.ts.
A listed asset is the manifest's RunnerReleaseAsset plus the version it
belongs to and its download URL. readRunnerReleaseListing is the one
lenient reader, and the app and the runner both read the listing through
it. The server writes the same type.
…kage layout

SHIPPED repeated the package's top-level entries. It is now derived from
RUNNER_PACKAGE_ENTRIES, in table order. A test checks that it covers
exactly the layout's top level, and that a full package swaps every entry
in and keeps the previous ones.
…he layout

The copied install script listed the files it requires and the ones it
must execute by hand. It now builds both lists from RUNNER_PACKAGE_ENTRIES:
every regular file is required, and the ones with an execute bit must be
executable. README.md, LICENSE and bin/node.LICENSE are now required too,
as the layout already says. The sandbox tests build the fake package from
the table. They check that the script lists exactly the table's files and
executables, and that a package missing any file, or unable to execute any
executable, is never published.

Closes #59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runner release: one listing shape and one layout table

1 participant