feat: check-git-install --check-links refuses local dependencies

- --check-links DIR... reports every link:, file:, workspace: or portal:
  dependency in each DIR/package.json and exits 1 (L4 CR-01)
- the tag mode runs the same check on the installed layer before building
- --self-test adds planted package.json cases next to the lockfile case
- README Releasing: swap the link for the seo tag pin, run --check-links,
  then the tag mode after pushing
This commit is contained in:
Jakub Zych
2026-11-04 22:54:18 +01:00
parent d64fe47adf
commit d4ba3e2ddf
2 changed files with 95 additions and 7 deletions

View File

@@ -1,12 +1,23 @@
#!/usr/bin/env bash
# Usage: scripts/check-git-install.sh <tag>
# scripts/check-git-install.sh --self-test (the lockfile check only)
# scripts/check-git-install.sh --check-links DIR...
# scripts/check-git-install.sh --self-test (the lockfile and link checks only)
#
# Proves the layer's distribution end to end: a scratch app installs
# vue-journal-layer from git+https://git.golem15.com/golem15/vue-journal-layer.git#<tag>
# anonymously, the lockfile pins the tag's commit, `nuxt build` of the layer's
# playground config succeeds, and the built server passes
# testing/expect-acme.txt against the fixture API.
# anonymously, the installed package.json has no local dependency, the
# lockfile pins the tag's commit, `nuxt build` of the layer's playground
# config succeeds, and the built server passes testing/expect-acme.txt
# against the fixture API.
#
# --check-links is the release guard to run before tagging any content
# layer (README, Releasing): it reads DIR/package.json for each DIR and
# prints `check-git-install: <dir>: depends on <name> through <spec>` for
# every dependencies, devDependencies, peerDependencies or
# optionalDependencies entry whose spec starts with link:, file:,
# workspace: or portal: (a path that resolves outside the package once it
# is installed from git), exiting 1 when it finds any or cannot read a
# package.json.
# Nothing outside a temporary directory is touched.
set -euo pipefail
@@ -24,9 +35,55 @@ lock_commits() {
} | sort -u
}
# local_links LABEL DIR... prints one line per local dependency of each
# DIR/package.json (prefixed with LABEL, else the dir) and fails when there
# is any, or when a package.json cannot be read.
local_links() {
local label="$1"
shift
node -e '
const fs = require("node:fs")
const path = require("node:path")
const [label, ...dirs] = process.argv.slice(1)
const LOCAL = /^(link|file|workspace|portal):/
let bad = false
for (const dir of dirs) {
const who = label || dir
let pkg
try {
pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"))
} catch (err) {
console.error(`check-git-install: ${who}: cannot read package.json (${err.message})`)
bad = true
continue
}
for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
const deps = pkg[section] && typeof pkg[section] === "object" ? pkg[section] : {}
for (const [name, spec] of Object.entries(deps)) {
if (typeof spec === "string" && LOCAL.test(spec)) {
console.error(`check-git-install: ${who}: depends on ${name} through ${spec} (${section})`)
bad = true
}
}
}
}
process.exit(bad ? 1 : 0)
' "$label" "$@"
}
if [[ "${1:-}" == "--check-links" ]]; then
shift
[[ $# -gt 0 ]] || die "usage: $0 --check-links DIR..."
command -v node >/dev/null 2>&1 || die "node is not installed"
local_links "" "$@" || exit 1
echo "check-git-install: no local dependencies in $*"
exit 0
fi
if [[ "${1:-}" == "--self-test" ]]; then
lock="$(mktemp)"
trap 'rm -f "$lock"' EXIT
pkgs="$(mktemp -d)"
trap 'rm -f "$lock"; rm -rf "$pkgs"' EXIT
j=1111111111111111111111111111111111111111
s=2222222222222222222222222222222222222222
cat >"$lock" <<LOCK
@@ -39,6 +96,26 @@ if [[ "${1:-}" == "--self-test" ]]; then
resolution: {commit: $s, repo: https://git.golem15.com/golem15/vue-seo-layer.git, type: git}
LOCK
[[ "$(lock_commits "$lock")" == "$j" ]] || die "self-test: lock_commits gave $(lock_commits "$lock" | tr '\n' ' '), want only $j"
# A local link: dependency is reported and fails; a git+https tag pin passes.
mkdir -p "$pkgs/linked" "$pkgs/pinned" "$pkgs/dev"
cat >"$pkgs/linked/package.json" <<'JSON'
{ "name": "vue-menu-layer", "dependencies": { "vue-seo-layer": "link:../vue-seo-layer", "nuxt": "4.4.8" } }
JSON
cat >"$pkgs/pinned/package.json" <<'JSON'
{ "name": "vue-menu-layer", "dependencies": { "vue-seo-layer": "git+https://git.golem15.com/golem15/vue-seo-layer.git#v0.2.0", "nuxt": "4.4.8" } }
JSON
cat >"$pkgs/dev/package.json" <<'JSON'
{ "name": "x", "devDependencies": { "a": "file:../a" }, "optionalDependencies": { "b": "workspace:*" }, "peerDependencies": { "c": "portal:../c" } }
JSON
if out="$(local_links "" "$pkgs/linked" 2>&1)"; then die "self-test: a link: dependency passed"; fi
[[ "$out" == "check-git-install: $pkgs/linked: depends on vue-seo-layer through link:../vue-seo-layer (dependencies)" ]] \
|| die "self-test: the link: report was: $out"
out="$(local_links "" "$pkgs/pinned" 2>&1)" || die "self-test: a git+https pin failed: $out"
[[ -z "$out" ]] || die "self-test: a git+https pin printed: $out"
if out="$(local_links "" "$pkgs/dev" 2>&1)"; then die "self-test: file:, workspace: and portal: dependencies passed"; fi
[[ "$(grep -c 'depends on' <<<"$out")" -eq 3 ]] || die "self-test: want three reports for file:, workspace: and portal:, got: $out"
if out="$(local_links "the tag's package.json" "$pkgs/missing" 2>&1)"; then die "self-test: a missing package.json passed"; fi
if out="$(local_links "" "$pkgs/pinned" "$pkgs/linked" 2>&1)"; then die "self-test: one linked package among several passed"; fi
echo "check-git-install: self-test ok"
exit 0
fi
@@ -89,7 +166,12 @@ allowBuilds:
vue-demi: true
YAML
GIT_TERMINAL_PROMPT=0 pnpm install || die "pnpm install failed"
GIT_TERMINAL_PROMPT=0 pnpm install || die "pnpm install failed (a link: or file: dependency in the tag's package.json fails here too; run --check-links before tagging)"
# The tag must not carry a local dependency (link:, file:, workspace:,
# portal:): it would resolve next to the pnpm store entry, not to a package.
local_links "the tag's package.json" node_modules/vue-journal-layer \
|| die "the tag's package.json still has a local dependency (see above); pin it to a git+https tag before tagging"
# The lockfile must pin the tag's commit, not a branch. Only the
# vue-journal-layer entries count: the layer's own git dependencies