penpot/scripts/sync-pnpm-version
Andrey Antukh d68531b783
⬆️ Update devenv dependencies (#11790)
* ⬆️ Update devenv dependencies

Update Node.js, OpenCode, clj-kondo, Babashka, Pixi, GitHub CLI, uv,
and Serena to their current stable releases.

AI-assisted-by: gpt-5.6-sol

* ⬆️ Update devenv to Java 27

Use Zulu JDK 27 in the development image for compatibility testing.
Update the official checksums for both supported architectures.

AI-assisted-by: gpt-5.6-sol

* 🐳 Replace MinIO with RustFS in devenv

Run RustFS as the development S3 service and wait for its health check.
Install a pinned AWS CLI with checksums and use it to create the bucket
idempotently from each backend entry point.

Keep the old MinIO volume untouched and use a new RustFS volume.

AI-assisted-by: gpt-5.6-sol

* 🐳 Replace MailCatcher with persistent Mailpit

Run Mailpit as the devenv SMTP sink while preserving mailer:1025 and the
localhost:1080 UI.

Store its SQLite inbox in a named volume and wait for the readiness
endpoint before starting runtime containers. Bind the web UI to loopback so
development emails stay local.

AI-assisted-by: gpt-5.6-sol

* ⬆️ Update Node.js to 24.21.0

Align the host NVM version with the Node.js version used by devenv.

AI-assisted-by: gpt-5.6-sol

* ⬆️ Update devenv to PostgreSQL 18.6

Run PostgreSQL 18 with its versioned volume layout and a TCP readiness
check that ignores the temporary initialization server.

Install the matching client, create penpot_nexus, and preserve the old
PostgreSQL 16 volume for rollback or logical migration.

AI-assisted-by: gpt-5.6-sol

* 🐳 Expose RustFS ports in devenv

Publish the RustFS S3 API and management console on localhost port 9000
and 9001.

Keep both bindings on loopback so object storage is not exposed to the local
network.

AI-assisted-by: gpt-5.6-sol

* 🐳 Install standalone pnpm in devenv

Install pnpm 12.5.0 from architecture-specific release archives and
verify their published checksums.

Remove the Corepack setup while allowing pnpm to honor the project
packageManager pins.

AI-assisted-by: gpt-5.6-sol

* 🔥 Remove corepack, use system pnpm everywhere

Corepack is gone from Node 25+, so every `corepack enable` call
fails. pnpm now ships as a system binary (devenv, CI runners and
Docker images install it directly) and auto-downloads the version
pinned in `packageManager` on mismatch.

Scripts, workflows and Dockerfiles call `pnpm` straight away; the
three deploy workflows use a single `pnpm/setup@v2` step; and the
new `scripts/sync-pnpm-version` stamps all 35 `packageManager`
fields from the system pnpm, replacing the `corepack use` sweep.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Fix exporter watch missing render-wasm build step

The exporter watch compiled CLJS requiring the generated
src/app/wasm/shared.js, which only render-wasm/build export
produces. Without it shadow-cljs failed with a cryptic missing
./shared.js dependency. Run build:wasm before watching, as
the frontend watch:app and exporter scripts/build already do.

AI-assisted-by: muse-spark-1.3-contributor

* 🔧 Add opencode V2 support and adapt plugins

Register the penpot tools for both opencode V1 (server())
and V2 (setup() with JSON Schema inputs) from a single
dependency-free plugin file, sharing the psql and
paren-repair runners between both paths.

Install the opencode2 binary side-by-side with V1 in the
devenv image and document the dual registration in the
paren-repair and psql memories.

AI-assisted-by: muse-spark-1.3-contributor

* ⬆️ Update pnpm and opencode
2026-09-22 10:22:31 +02:00

186 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Sync the `packageManager` field of every first-party package.json to the
# system pnpm version.
#
# Only `pnpm` and `node` are required (plus registry access to resolve the
# integrity hash, unless --field is given).
#
# Run it from the repo root with the log redirected to a file (never pipe
# tool output through filters).
set -euo pipefail
usage() {
cat <<'EOF'
Usage: scripts/sync-pnpm-version [options] [version]
Stamp every first-party package.json's `packageManager` field with the
system pnpm version (default: `pnpm --version`).
Options:
--field FIELD Use FIELD verbatim, skip the registry lookup.
For tests and offline use.
--check Verify all fields match; do not write. Exit 1 on drift.
--install After stamping, run `pnpm install` in each workspace
root to refresh lockfile metadata.
--root DIR Scan DIR instead of the repo root. For tests.
-h, --help Show this help.
external/ (vendored trees with their own lifecycles), .opencode/,
.pnpm-store/, bundles/ and docker build-context copies are never stamped.
EOF
}
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
FIELD=""
CHECK=0
INSTALL=0
VERSION=""
while [[ $# -gt 0 ]]; do
case "$1" in
--field) FIELD="$2"; shift 2 ;;
--field=*) FIELD="${1#--field=}"; shift ;;
--check) CHECK=1; shift ;;
--install) INSTALL=1; shift ;;
--root) ROOT="$2"; shift 2 ;;
--root=*) ROOT="${1#--root=}"; shift ;;
-h | --help) usage; exit 0 ;;
-*) {
echo "error: unknown option: $1" >&2
usage >&2
exit 64
} ;;
*)
if [[ -n "$VERSION" ]]; then
echo "error: only one version argument allowed" >&2
usage >&2
exit 64
fi
VERSION="$1"; shift ;;
esac
done
if [[ -z "$FIELD" ]]; then
if [[ -z "$VERSION" ]]; then
VERSION="$(pnpm --version | tr -d '[:space:]')"
fi
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then
echo "error: invalid pnpm version: $VERSION" >&2
exit 64
fi
# Same value `npm view pnpm@<v> dist.integrity` returns; pnpm is the one
# tool guaranteed to exist everywhere, so no npm needed.
integrity="$(pnpm view "pnpm@${VERSION}" dist.integrity | tr -d '[:space:]')"
b64="${integrity#sha512-}"
if ! [[ "$b64" =~ ^[A-Za-z0-9+/=]+$ ]]; then
echo "error: unexpected integrity for pnpm@${VERSION}: ${integrity}" >&2
exit 1
fi
# node ships with every pnpm, so no python3 needed for base64 -> hex.
hex="$(node -p "Buffer.from('${b64}','base64').toString('hex')")"
FIELD="pnpm@${VERSION}+sha512.${hex}"
fi
cd "$ROOT"
# Same exclusions as scripts/clean-node-modules, plus build-context copies
# (bundles/, docker/images/bundle-*) which builds regenerate from the
# stamped sources.
mapfile -t files < <(
find . \
\( -name .git -o -name node_modules -o -name .pnpm-store \
-o -name external -o -name .opencode -o -name bundles \
-o -name .angular \) -type d -prune \
-o -path './docker/images/bundle-*' -prune \
-o -name package.json -type f -print | sort
)
if [[ ${#files[@]} -eq 0 ]]; then
echo "error: no package.json files found under $ROOT" >&2
exit 1
fi
# Stamp one file with node: replaces only the packageManager value when the
# key exists (indent, key order and the rest stay byte-identical), else
# inserts the key after "name" using the file's own indent. Prints
# "updated <rel>" / "unchanged <rel>" / "missing <rel>" / "mismatch <rel>".
stamp_one() {
node -e '
const fs = require("fs");
const [file, field, check] = process.argv.slice(1);
const raw = fs.readFileSync(file, "utf8");
const data = JSON.parse(raw);
const rel = file.replace(/^\.\//, "");
if (data.packageManager === field) {
console.log(`unchanged ${rel}`);
} else if (check === "1") {
console.log(`${data.packageManager === undefined ? "missing" : "mismatch"} ${rel}`);
process.exit(3);
} else {
let out;
const valueRe = /("packageManager"\s*:\s*")[^"]*(")/;
if (valueRe.test(raw)) {
out = raw.replace(valueRe, `$1${field}$2`);
} else {
const anchor = raw.match(/^(\s*)"(name|version)"\s*:\s*"[^"]*",\s*$/m);
if (!anchor) {
console.error(`error: cannot find insertion point in ${rel}`);
process.exit(1);
}
out = raw.replace(
anchor[0],
`${anchor[0]}\n${anchor[1]}"packageManager": "${field}",`
);
}
JSON.parse(out); // refuse to write invalid JSON
fs.writeFileSync(file, out);
console.log(`updated ${rel}`);
}
' "$1" "$FIELD" "$CHECK"
}
failures=0
updated=0
unchanged=0
for file in "${files[@]}"; do
if [[ "$CHECK" -eq 1 ]]; then
if stamp_one "$file"; then
unchanged=$((unchanged + 1))
else
rc=$?
if [[ "$rc" -eq 3 ]]; then
failures=$((failures + 1))
else
exit "$rc"
fi
fi
else
if [[ "$(stamp_one "$file")" == updated* ]]; then
updated=$((updated + 1))
else
unchanged=$((unchanged + 1))
fi
fi
done
if [[ "$CHECK" -eq 1 ]]; then
if [[ "$failures" -gt 0 ]]; then
echo "check failed: ${failures} file(s) differ from ${FIELD}" >&2
exit 1
fi
echo "all ${unchanged} package.json files match ${FIELD}"
exit 0
fi
echo "stamped ${FIELD}: ${updated} updated, ${unchanged} unchanged"
if [[ "$INSTALL" -eq 1 ]]; then
# The 11 pnpm workspaces: repo root plus one per module. Members resolve
# through their parent workspace lockfile, so no per-member install.
for dir in . backend common docs exporter frontend library mcp media-processor plugins render-wasm; do
echo "--- pnpm install in ${dir}"
(cd "$dir" && pnpm install)
done
fi