fix(ci): narrow workflow scope and reuse setup steps (#2708)

* fix(ci): narrow workflow scope and reuse setup steps

* fix(ci): narrow workflow scope and reuse setup steps

Repair Nix fixed-output hashes for the filtered daemon and web source trees.

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* fix(ci): narrow workflow scope and reuse setup steps

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* fix(ci): narrow workflow scope and reuse setup steps

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* fix(ci): repair daemon and nix checks

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)
This commit is contained in:
Marc Chan 2026-05-22 18:58:53 +08:00 committed by GitHub
parent 1b908a8481
commit a5b47c5f76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 275 additions and 415 deletions

View file

@ -0,0 +1,34 @@
name: Setup Playwright
description: Restore Playwright browser cache and install browsers
inputs:
package-json-path:
description: Path to package.json containing @playwright/test or playwright devDependency
required: true
install-command:
description: Command used to install browsers
required: true
runs:
using: composite
steps:
- name: Resolve Playwright version
id: playwright-version
shell: bash
run: |
version=$(node -p "const pkg = require('./${{ inputs.package-json-path }}'); const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }; (deps['@playwright/test'] || deps.playwright || '').replace(/[^0-9.]/g,'')")
if [ -z "$version" ]; then
echo "Could not resolve Playwright version from ${{ inputs.package-json-path }}" >&2
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browser binaries
uses: actions/cache@v5.0.5
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright browsers
shell: bash
run: ${{ inputs.install-command }}

View file

@ -0,0 +1,41 @@
name: Setup workspace
description: Restore pnpm cache and install dependencies
inputs:
node-version:
description: Node.js version to install
required: false
default: '24'
pnpm-version:
description: pnpm version to install
required: false
default: '10.33.2'
runs:
using: composite
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.8
with:
version: ${{ inputs.pnpm-version }}
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
node-version: ${{ inputs.node-version }}
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Cache pnpm store
uses: actions/cache@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
shell: bash
run: pnpm install --frozen-lockfile

View file

@ -73,7 +73,7 @@ jobs:
if [[ "$file" == "tools/pack/"* || "$file" == "apps/packaged/"* || "$file" == "apps/desktop/"* || "$file" == "packages/host/"* || "$file" == "packages/platform/"* || "$file" == "packages/sidecar/"* || "$file" == "packages/sidecar-proto/"* ]]; then
tools_pack_tests_required=true
fi
if [[ "$file" == "package.json" || "$file" == "apps/"*/"package.json" || "$file" == "packages/"*/"package.json" || "$file" == "tools/"*/"package.json" || "$file" == "e2e/package.json" || "$file" == "pnpm-lock.yaml" || "$file" == "pnpm-workspace.yaml" || "$file" == ".github/workflows/ci.yml" ]]; then
if [[ "$file" == "package.json" || "$file" == "apps/daemon/package.json" || "$file" == "apps/web/package.json" || "$file" == "apps/desktop/package.json" || "$file" == "apps/packaged/package.json" || "$file" == "packages/"*/"package.json" || "$file" == "tools/"*/"package.json" || "$file" == "e2e/package.json" || "$file" == "pnpm-lock.yaml" || "$file" == "pnpm-workspace.yaml" || "$file" == ".github/workflows/ci.yml" ]]; then
daemon_tests_required=true
web_tests_required=true
tools_dev_tests_required=true
@ -106,6 +106,15 @@ jobs:
|| [ "$tools_pack_tests_required" = "true" ]; then
workspace_validation_required=true
fi
elif [ "${{ github.event_name }}" = "push" ]; then
daemon_tests_required=true
web_tests_required=true
tools_dev_tests_required=true
tools_pack_tests_required=true
# Main already runs .github/workflows/nix-check.yml, so keep this
# workflow's push path focused on the non-Nix workspace signal.
nix_validation_required=false
workspace_validation_required=true
else
daemon_tests_required=true
web_tests_required=true
@ -155,30 +164,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
node-version: 24
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup workspace
uses: ./.github/actions/setup-workspace
# `scripts/postinstall.mjs` only prebuilds package/tool entrypoints that
# are needed immediately after install for linked bins and shared
@ -212,8 +199,8 @@ jobs:
- name: Check i18n structure
run: pnpm i18n:check
core_tests:
name: Core package tests
workspace_unit_tests:
name: Workspace unit tests
needs: [change_scopes]
if: ${{ needs.change_scopes.outputs.workspace_validation_required == 'true' }}
runs-on: ubuntu-latest
@ -223,77 +210,16 @@ jobs:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup workspace
uses: ./.github/actions/setup-workspace
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
node-version: 24
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Core package tests
- name: Workspace unit tests
run: |
pnpm --filter @open-design/contracts test
pnpm --filter @open-design/host test
pnpm --filter @open-design/platform test
pnpm --filter @open-design/sidecar test
pnpm --filter @open-design/sidecar-proto test
tools_workspace_tests:
name: Tools workspace tests
needs: [change_scopes]
if: ${{ needs.change_scopes.outputs.tools_dev_tests_required == 'true' || needs.change_scopes.outputs.tools_pack_tests_required == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
node-version: 24
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Tools workspace smoke tests
run: |
if [ "${{ needs.change_scopes.outputs.tools_dev_tests_required }}" = "true" ]; then
pnpm --filter @open-design/tools-dev test
fi
@ -302,50 +228,24 @@ jobs:
fi
daemon_workspace_tests:
name: Daemon workspace tests (${{ matrix.shard }}/2)
name: Daemon workspace tests
needs: [change_scopes]
if: ${{ needs.change_scopes.outputs.daemon_tests_required == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard: [1, 2]
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
node-version: 24
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup workspace
uses: ./.github/actions/setup-workspace
- name: Prebuild daemon entrypoint declarations
run: pnpm --filter @open-design/daemon build
- name: Daemon workspace tests
run: pnpm --filter @open-design/daemon exec vitest run -c vitest.config.ts --shard=${{ matrix.shard }}/2
run: pnpm --filter @open-design/daemon test
web_workspace_tests:
name: Web workspace tests
@ -358,30 +258,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
node-version: 24
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup workspace
uses: ./.github/actions/setup-workspace
- name: Prebuild web sidecar declarations
run: pnpm --filter @open-design/web build:sidecar
@ -389,164 +267,26 @@ jobs:
- name: Web workspace tests
run: pnpm --filter @open-design/web test
app_tests:
name: App workspace tests
browser_tests:
name: Browser tests
needs:
- change_scopes
- tools_workspace_tests
- daemon_workspace_tests
- web_workspace_tests
if: ${{ always() && needs.change_scopes.result == 'success' && (needs.change_scopes.outputs.tools_dev_tests_required == 'true' || needs.change_scopes.outputs.tools_pack_tests_required == 'true' || needs.change_scopes.outputs.daemon_tests_required == 'true' || needs.change_scopes.outputs.web_tests_required == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check app workspace test jobs
env:
NEEDS_JSON: ${{ toJSON(needs) }}
TOOLS_REQUIRED: ${{ needs.change_scopes.outputs.tools_dev_tests_required == 'true' || needs.change_scopes.outputs.tools_pack_tests_required == 'true' }}
DAEMON_REQUIRED: ${{ needs.change_scopes.outputs.daemon_tests_required }}
WEB_REQUIRED: ${{ needs.change_scopes.outputs.web_tests_required }}
run: |
set -euo pipefail
echo "$NEEDS_JSON" | jq .
failures=()
if [ "$TOOLS_REQUIRED" = "true" ] && [ "$(echo "$NEEDS_JSON" | jq -r '.tools_workspace_tests.result')" != "success" ]; then
failures+=("tools_workspace_tests=$(echo "$NEEDS_JSON" | jq -r '.tools_workspace_tests.result')")
fi
if [ "$DAEMON_REQUIRED" = "true" ] && [ "$(echo "$NEEDS_JSON" | jq -r '.daemon_workspace_tests.result')" != "success" ]; then
failures+=("daemon_workspace_tests=$(echo "$NEEDS_JSON" | jq -r '.daemon_workspace_tests.result')")
fi
if [ "$WEB_REQUIRED" = "true" ] && [ "$(echo "$NEEDS_JSON" | jq -r '.web_workspace_tests.result')" != "success" ]; then
failures+=("web_workspace_tests=$(echo "$NEEDS_JSON" | jq -r '.web_workspace_tests.result')")
fi
if [ "${#failures[@]}" -gt 0 ]; then
printf 'App workspace validation failed:\n'
printf '%s\n' "${failures[@]}"
exit 1
fi
e2e_vitest:
name: E2E vitest
needs: [change_scopes]
if: ${{ needs.change_scopes.outputs.workspace_validation_required == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
node-version: 24
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Some Vitest-based smoke tests drive the browser directly through
# @playwright/test. Restore browser binaries without saving from CI runs;
# the key follows the @playwright/test version so browser revisions
# update with package bumps.
- name: Resolve Playwright version
id: playwright-version
run: |
version=$(node -p "require('./e2e/package.json').devDependencies['@playwright/test'].replace(/[^0-9.]/g,'')")
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Restore Playwright browser cache
uses: actions/cache/restore@v5.0.5
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright browsers
run: pnpm -C e2e exec playwright install --with-deps chromium
- name: E2E vitest
run: pnpm --filter @open-design/e2e test
ui_e2e_critical:
name: Playwright critical (${{ matrix.group }})
needs: [change_scopes]
if: ${{ needs.change_scopes.outputs.workspace_validation_required == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- group: core
grep_flag: --grep-invert
grep_pattern: home starters|home hero
- group: starters
grep_flag: --grep
grep_pattern: home starters|home hero
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.8
- name: Setup workspace
uses: ./.github/actions/setup-workspace
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
node-version: 24
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Restore Playwright browser binaries without saving from CI runs. The
# key follows the @playwright/test version so browser revisions update
# with package bumps.
- name: Resolve Playwright version
id: playwright-version
run: |
version=$(node -p "require('./e2e/package.json').devDependencies['@playwright/test'].replace(/[^0-9.]/g,'')")
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Restore Playwright browser cache
uses: actions/cache/restore@v5.0.5
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright browsers
run: pnpm -C e2e exec playwright install --with-deps chromium
package-json-path: e2e/package.json
install-command: pnpm -C e2e exec playwright install --with-deps chromium
- name: Prebuild workspace type declarations
run: |
@ -554,10 +294,13 @@ jobs:
pnpm --filter @open-design/desktop build
pnpm --filter @open-design/web build:sidecar
- name: E2E vitest
run: pnpm --filter @open-design/e2e test
- name: Playwright critical
run: |
pnpm -C e2e exec tsx scripts/playwright.ts clean
pnpm -C e2e exec playwright test -c playwright.config.ts ${{ matrix.grep_flag }} '${{ matrix.grep_pattern }}' ui/critical-smoke.test.ts ui/entry-chrome-flows.test.ts
pnpm -C e2e exec playwright test -c playwright.config.ts ui/critical-smoke.test.ts ui/entry-chrome-flows.test.ts
build_workspaces:
name: Build workspaces
@ -570,30 +313,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
node-version: 24
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup workspace
uses: ./.github/actions/setup-workspace
- name: Prebuild workspace type declarations
run: |
@ -615,10 +336,10 @@ jobs:
- change_scopes
- preflight
- nix_validation
- core_tests
- app_tests
- e2e_vitest
- ui_e2e_critical
- workspace_unit_tests
- daemon_workspace_tests
- web_workspace_tests
- browser_tests
- build_workspaces
if: ${{ always() }}
runs-on: ubuntu-latest

View file

@ -61,37 +61,17 @@ jobs:
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v5
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup workspace
uses: ./.github/actions/setup-workspace
# Cache the Playwright browser binaries between runs. The cache key
# is pinned to the playwright version we depend on (kept in
# apps/landing-page/package.json) so a bump invalidates correctly.
- name: Resolve Playwright version
id: playwright-version
run: |
version=$(node -p "require('./apps/landing-page/package.json').devDependencies.playwright.replace(/[^0-9.]/g,'')")
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
uses: actions/cache@v4
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright Chromium
run: pnpm --filter @open-design/landing-page exec playwright install --with-deps chromium
package-json-path: apps/landing-page/package.json
install-command: pnpm --filter @open-design/landing-page exec playwright install --with-deps chromium
- name: Typecheck landing page
run: pnpm --filter @open-design/landing-page typecheck

View file

@ -21,9 +21,53 @@
dream2nix,
home-manager,
}: let
filterProjectSource = includePaths:
nixpkgs.lib.cleanSourceWith {
src = self;
filter = path: type: let
root = toString self;
pathStr = toString path;
rel = nixpkgs.lib.removePrefix (root + "/") pathStr;
matches = includePath:
rel == includePath
|| nixpkgs.lib.hasPrefix (includePath + "/") rel
|| (type == "directory" && nixpkgs.lib.hasPrefix (rel + "/") includePath);
in
rel == ""
|| builtins.any matches includePaths;
};
perSystem = flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {inherit system;};
nodejs = pkgs.nodejs_24;
daemonSrc = filterProjectSource [
"package.json"
"pnpm-lock.yaml"
"pnpm-workspace.yaml"
"tsconfig.json"
"scripts"
"assets"
"plugins"
"skills"
"design-systems"
"design-templates"
"craft"
"prompt-templates"
"apps/daemon"
"packages"
];
webSrc = filterProjectSource [
"package.json"
"pnpm-lock.yaml"
"pnpm-workspace.yaml"
"tsconfig.json"
"apps/web"
"packages/contracts"
"packages/host"
"packages/platform"
"packages/sidecar"
"packages/sidecar-proto"
];
# nixpkgs ships pnpm 10.33.0; the repo's package.json declares
# `engines.pnpm: ">=10.33.2 <11"` and pnpm refuses to install
@ -47,11 +91,11 @@
daemon = pkgs.callPackage ./nix/package-daemon.nix {
inherit dream2nix nixpkgs system nodejs pnpm_10;
src = self;
src = daemonSrc;
};
web = pkgs.callPackage ./nix/package-web.nix {
inherit dream2nix nixpkgs system nodejs pnpm_10;
src = self;
src = webSrc;
};
in {
packages = {

View file

@ -39,7 +39,7 @@ let
pname = "open-design-daemon";
version = (lib.importJSON ../package.json).version;
pnpmDepsHash = (import ./pnpm-deps.nix).hash;
pnpmDepsHash = (import ./pnpm-deps.nix).daemonHash;
in
stdenv.mkDerivation (finalAttrs: {
inherit pname version src;
@ -153,6 +153,20 @@ in
# just apps/daemon.
cp -r . $out/lib/open-design/
# Root devDependencies expose tool workspaces via pnpm symlinks, but the
# daemon derivation intentionally filters tools/ out of src because they
# are not needed at runtime. Prune the dangling symlinks from the copied
# node_modules tree so Nix fixup does not fail on broken links.
rm -f \
$out/lib/open-design/node_modules/@open-design/tools-dev \
$out/lib/open-design/node_modules/@open-design/tools-pack \
$out/lib/open-design/node_modules/@open-design/tools-pr \
$out/lib/open-design/node_modules/@open-design/tools-serve \
$out/lib/open-design/node_modules/.bin/tools-dev \
$out/lib/open-design/node_modules/.bin/tools-pack \
$out/lib/open-design/node_modules/.bin/tools-pr \
$out/lib/open-design/node_modules/.bin/tools-serve
chmod +x $out/lib/open-design/apps/daemon/dist/cli.js
makeWrapper ${nodejs}/bin/node $out/bin/od \

View file

@ -26,7 +26,7 @@ let
pname = "open-design-web";
version = (lib.importJSON ../package.json).version;
pnpmDepsHash = (import ./pnpm-deps.nix).hash;
pnpmDepsHash = (import ./pnpm-deps.nix).webHash;
in
stdenv.mkDerivation (finalAttrs: {
inherit pname version src;

View file

@ -1,9 +1,13 @@
{
# Vendored pnpm store for the workspace packages built by the flake.
# Vendored pnpm store hashes for the workspace packages built by the flake.
#
# Refresh this hash whenever pnpm-lock.yaml changes:
# The daemon and web derivations now build from different filtered source
# trees, so each fetchPnpmDeps invocation needs its own fixed-output hash.
# Refresh a hash whenever pnpm-lock.yaml or that derivation's source filter
# changes:
# 1. Temporarily set the consuming `hash = lib.fakeHash;`
# 2. Run the relevant nix build/flake check
# 3. Copy the expected hash printed by Nix into `hash` below
hash = "sha256-l87ATTkJYpX7OHHxmA/CxvJHdaaN/9RPi6AYI4DRn/I=";
# 3. Copy the expected hash printed by Nix into the matching field below
daemonHash = "sha256-X64KXAuSKE9ARcKHkgilAj7Nlej/MyAF/tiKsX4AEgg=";
webHash = "sha256-74loUCL+WcaZO4AAMnSpNeBhDz1Y9TMgFRPbyaOfPAk=";
}

View file

@ -12,7 +12,7 @@
"description": "Official Open Design plugin registry seed. The generated public catalog is served from open-design.ai/marketplace.",
"version": "0.1.0",
"generatedFrom": "plugins/_official",
"bundledPreinstallCount": 400
"bundledPreinstallCount": 401
},
"plugins": [
{

View file

@ -4,79 +4,101 @@ import process from "node:process";
import { spawnSync } from "node:child_process";
const repoRoot = path.resolve(import.meta.dirname, "..");
const consumerPath = path.join(repoRoot, "nix/package-web.nix");
const sharedHashPath = path.join(repoRoot, "nix/pnpm-deps.nix");
const consumerHashLine = " hash = pnpmDepsHash;";
const fakeHashLine = " hash = lib.fakeHash;";
const nixCommand = ["build", ".#web", "--print-build-logs"];
const maxNixOutputBufferBytes = 32 * 1024 * 1024;
const consumers = [
{
hashKey: "daemonHash",
consumerPath: path.join(repoRoot, "nix/package-daemon.nix"),
nixCommand: ["build", ".#daemon", "--print-build-logs"],
},
{
hashKey: "webHash",
consumerPath: path.join(repoRoot, "nix/package-web.nix"),
nixCommand: ["build", ".#web", "--print-build-logs"],
},
] as const;
function extractExpectedHash(output: string): string | null {
const matches = [...output.matchAll(/got:\s*(sha256-[A-Za-z0-9+/=]+)/g)];
return matches.at(-1)?.[1] ?? null;
}
async function main(): Promise<void> {
const originalConsumer = await readFile(consumerPath, "utf8");
if (!originalConsumer.includes(consumerHashLine)) {
throw new Error(
`Expected to find \`${consumerHashLine.trim()}\` in ${path.relative(repoRoot, consumerPath)}`,
);
const updates: string[] = [];
for (const consumer of consumers) {
const originalConsumer = await readFile(consumer.consumerPath, "utf8");
if (!originalConsumer.includes(consumerHashLine)) {
throw new Error(
`Expected to find \`${consumerHashLine.trim()}\` in ${path.relative(repoRoot, consumer.consumerPath)}`,
);
}
const fakeHashConsumer = originalConsumer.replace(consumerHashLine, fakeHashLine);
await writeFile(consumer.consumerPath, fakeHashConsumer, "utf8");
try {
const result = spawnSync("nix", consumer.nixCommand, {
cwd: repoRoot,
encoding: "utf8",
maxBuffer: maxNixOutputBufferBytes,
stdio: ["inherit", "pipe", "pipe"],
});
if (result.error) {
throw new Error(`Failed to execute nix: ${result.error.message}`);
}
if (result.status === 0) {
throw new Error(
`nix ${consumer.nixCommand.join(" ")} unexpectedly succeeded after replacing the fixed-output hash with lib.fakeHash.`,
);
}
const combinedOutput = `${result.stdout}${result.stderr}`;
const nextHash = extractExpectedHash(combinedOutput);
if (!nextHash) {
throw new Error(
"nix build failed without reporting a fixed-output hash mismatch (`got: sha256-...`). " +
`Refusing to update ${path.relative(repoRoot, sharedHashPath)}.\n\n${combinedOutput}`,
);
}
const originalSharedHash = await readFile(sharedHashPath, "utf8");
const hashPattern = new RegExp(`${consumer.hashKey} = \"sha256-[A-Za-z0-9+/=]+\";`);
if (!hashPattern.test(originalSharedHash)) {
throw new Error(
`Expected to find \`${consumer.hashKey} = \"sha256-...\";\` in ${path.relative(repoRoot, sharedHashPath)}`,
);
}
const updatedSharedHash = originalSharedHash.replace(
hashPattern,
`${consumer.hashKey} = "${nextHash}";`,
);
if (updatedSharedHash === originalSharedHash) {
updates.push(`${consumer.hashKey} already pins ${nextHash}`);
continue;
}
await writeFile(sharedHashPath, updatedSharedHash, "utf8");
updates.push(`${consumer.hashKey} -> ${nextHash}`);
} finally {
await writeFile(consumer.consumerPath, originalConsumer, "utf8");
}
}
const fakeHashConsumer = originalConsumer.replace(consumerHashLine, fakeHashLine);
await writeFile(consumerPath, fakeHashConsumer, "utf8");
try {
const result = spawnSync("nix", nixCommand, {
cwd: repoRoot,
encoding: "utf8",
maxBuffer: maxNixOutputBufferBytes,
stdio: ["inherit", "pipe", "pipe"],
});
if (result.error) {
throw new Error(`Failed to execute nix: ${result.error.message}`);
}
if (result.status === 0) {
throw new Error(
"nix build unexpectedly succeeded after replacing the fixed-output hash with lib.fakeHash.",
);
}
const combinedOutput = `${result.stdout}${result.stderr}`;
const nextHash = extractExpectedHash(combinedOutput);
if (!nextHash) {
throw new Error(
"nix build failed without reporting a fixed-output hash mismatch (`got: sha256-...`). " +
`Refusing to update ${path.relative(repoRoot, sharedHashPath)}.\n\n${combinedOutput}`,
);
}
const originalSharedHash = await readFile(sharedHashPath, "utf8");
const updatedSharedHash = originalSharedHash.replace(
/hash = "sha256-[A-Za-z0-9+/=]+";/,
`hash = "${nextHash}";`,
);
if (updatedSharedHash === originalSharedHash) {
process.stdout.write(
`${path.relative(repoRoot, sharedHashPath)} already pins ${nextHash}; no update needed.\n`,
);
return;
}
await writeFile(sharedHashPath, updatedSharedHash, "utf8");
process.stdout.write(
`Updated ${path.relative(repoRoot, sharedHashPath)} to ${nextHash}.\n` +
`Re-run \`nix flake check --print-build-logs --keep-going\` to confirm.\n`,
);
} finally {
await writeFile(consumerPath, originalConsumer, "utf8");
}
process.stdout.write(
`Updated ${path.relative(repoRoot, sharedHashPath)} (${updates.join(", ")}).\n` +
`Re-run \`nix flake check --print-build-logs --keep-going\` to confirm.\n`,
);
}
await main();