Skip to content

Commit e56aaba

Browse files
turadgclaude
andcommitted
test(ci): smoke-test publishing via local Verdaccio
Adds scripts/smoketest-publishing.sh, which boots a disposable Verdaccio registry via 'npx verdaccio', runs 'yarn release:npm' against it (so the full pack-all + npm-publish loop is exercised end-to-end), then installs a representative subset of the published tarballs into a fresh consumer and imports them under SES lockdown. The pattern mirrors agoric-sdk's scripts/packing/smoketest-publishing.sh but is much smaller: endo publishes already-built tarballs from dist/ rather than running lerna publish against the source tree, so there is no dev-version bump, no lerna, and no git state dance. Key design points: - A custom verdaccio.yaml disables the default npmjs.org uplink ('uplinks: {}'). Without this, Verdaccio proxies reads upstream and refuses to let us publish over versions that already exist on the public registry, since endo's tarballs carry real release numbers rather than dev prereleases. - A free TCP port is picked via Node's net.createServer() rather than hardcoding 4873, so a stray Verdaccio on the conventional port can't collide with us and we can't collide with it. - HOME is a per-run mktemp dir, so the script never touches the developer's ~/.npmrc or leaves auth tokens behind. A SIGTERM/SIGINT trap cleans up Verdaccio and the dir on exit. - 'npm_config_registry' is inlined on the yarn release:npm command only, not exported globally. Exporting it causes 'npx npm-cli-login' earlier in the script to try to fetch npm-cli-login itself from the empty local registry and 404. - The consumer install uses 'npm install <name>' (not '<path>') so npm has to resolve each package from the registry. That proves the rewritten 'workspace:' deps resolved to concrete versions that actually exist in the same registry, and that the cross-package type graph is self-consistent in tarball form. Wired up as 'yarn smoketest:publish' and invoked from ci.yml's build matrix, replacing the prior pair of steps: - 'yarn workspaces foreach --all --topological exec npm pack', which could not have worked after the ts-node-pack refactor: the .ts-using packages (exo, patterns, eventual-send) no longer have prepack hooks, so npm pack would have shipped raw .ts files. - 'yarn lerna run prepack' for cross-package type-resolution validation. That check is now subsumed by the consumer import step: if a package's declarations depend on a neighbor whose published form is missing or mis-resolved, the import will fail under SES. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent befc94b commit e56aaba

File tree

3 files changed

+168
-10
lines changed

3 files changed

+168
-10
lines changed

.github/workflows/ci.yml

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -318,16 +318,20 @@ jobs:
318318
- name: build
319319
run: yarn run build
320320

321-
- name: pack
322-
run: yarn workspaces foreach --all --topological exec npm pack
323-
324-
# Prepack (without cleanup per package) to ensure that type resolution in
325-
# dependent packages continues to work when the typedefs are generated by
326-
# their upstream packages. This helps avoid a situation in which the types
327-
# only resolve because of the state of the local filesystem, and fails
328-
# when imported in an NPM node_modules tree.
329-
- name: Prepack packages
330-
run: yarn lerna run --reject-cycles --concurrency 1 prepack
321+
# Smoke-test the publish flow end to end. Boots a disposable
322+
# Verdaccio registry, runs `yarn release:npm` against it, then
323+
# installs a representative subset of the published tarballs into
324+
# a fresh consumer and exercises them under SES lockdown. This
325+
# replaces the previous `yarn workspaces foreach exec npm pack`
326+
# step (which could not have worked after the ts-node-pack
327+
# refactor because the .ts-using packages no longer have prepack
328+
# hooks to turn their sources into publishable JS) and also
329+
# subsumes the "Prepack packages" type-resolution check: the
330+
# consumer imports under SES prove that declarations resolve
331+
# cross-package from their published tarball state, not from
332+
# whatever source tree happens to be on disk.
333+
- name: Smoke-test publish
334+
run: yarn smoketest:publish
331335

332336
test-xs:
333337
name: test-xs

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"lint:prettier": "prettier --check .github packages",
6060
"release:npm": "scripts/release-npm.mjs",
6161
"pack:all": "scripts/pack-all.mjs",
62+
"smoketest:publish": "scripts/smoketest-publishing.sh",
6263
"test": "yarn workspaces foreach --all --exclude @endo/skel run test",
6364
"test:c8": "yarn workspaces foreach --all run test:c8",
6465
"test:xs": "yarn workspaces foreach --all run test:xs",

scripts/smoketest-publishing.sh

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#! /bin/bash
2+
# Smoke-test the ts-node-pack publish path against a local Verdaccio
3+
# registry. Runs the full `yarn release:npm` flow (which internally
4+
# calls `yarn pack:all` + `npm publish` per tarball) pointed at a
5+
# disposable Verdaccio instance, then installs a representative subset
6+
# of the published packages into a fresh consumer project and imports
7+
# them under SES lockdown.
8+
#
9+
# Usage: scripts/smoketest-publishing.sh
10+
#
11+
# Requires network access for the first `npx verdaccio` / `npx
12+
# npm-cli-login` fetch. The registry itself runs offline (no uplinks).
13+
14+
set -ueo pipefail
15+
16+
thisdir=$(cd -- "$(dirname "$0")" > /dev/null && pwd)
17+
ROOT=$(cd "$thisdir/.." && pwd)
18+
19+
# Isolate npm / yarn state to a per-run HOME so the smoketest cannot
20+
# stomp on the developer's ~/.npmrc or leave auth tokens behind.
21+
REGISTRY_HOME=$(mktemp -d -t endo-smoketest-publishing.XXXXX)
22+
export HOME="$REGISTRY_HOME"
23+
# Pick a free TCP port so a stray Verdaccio on the conventional 4873 can't
24+
# collide with us (and vice versa — we never stomp on an unrelated instance).
25+
REGISTRY_PORT=$(node -e '
26+
const s = require("net").createServer();
27+
s.listen(0, () => { console.log(s.address().port); s.close(); });
28+
')
29+
REGISTRY_URL="http://localhost:$REGISTRY_PORT"
30+
31+
cleanup() {
32+
if [ -f "$REGISTRY_HOME/verdaccio.pid" ]; then
33+
echo "smoketest-publishing: stopping Verdaccio"
34+
kill "$(cat "$REGISTRY_HOME/verdaccio.pid")" 2>/dev/null || true
35+
fi
36+
rm -rf "$REGISTRY_HOME"
37+
}
38+
trap cleanup EXIT
39+
40+
# Write a Verdaccio config that disables the default npmjs.org uplink.
41+
# We must publish real @endo versions that already exist upstream; with
42+
# the uplink enabled, Verdaccio proxies the read and rejects the publish
43+
# as "version already published" against the public registry's state.
44+
# Offline-only also guarantees that a misconfigured test cannot leak
45+
# a package to the real registry.
46+
cat > "$REGISTRY_HOME/verdaccio.yaml" <<EOF
47+
storage: $REGISTRY_HOME/storage
48+
auth:
49+
htpasswd:
50+
file: $REGISTRY_HOME/htpasswd
51+
max_users: 1000
52+
uplinks: {}
53+
packages:
54+
'**':
55+
access: \$all
56+
publish: \$authenticated
57+
unpublish: \$authenticated
58+
log:
59+
type: stdout
60+
format: pretty
61+
level: warn
62+
EOF
63+
64+
echo "smoketest-publishing: starting Verdaccio (HOME=$REGISTRY_HOME)"
65+
(
66+
cd "$REGISTRY_HOME"
67+
: > verdaccio.log
68+
nohup npx --yes verdaccio@^6 --config "$REGISTRY_HOME/verdaccio.yaml" \
69+
--listen "$REGISTRY_PORT" &> verdaccio.log &
70+
echo $! > verdaccio.pid
71+
# Block until verdaccio prints its "http address" line to the log.
72+
grep -q 'http address' <(tail -f verdaccio.log)
73+
)
74+
75+
# We do NOT globally `export npm_config_registry`. That would redirect
76+
# the next `npx npm-cli-login` at our empty local registry and fail with
77+
# E404 trying to fetch `npm-cli-login` itself. Instead we scope the
78+
# override to just the publish command below, where we want it.
79+
80+
# Create a disposable publish user. Verdaccio's default auth plugin
81+
# (htpasswd) accepts new users via npm's adduser endpoint, which
82+
# npm-cli-login automates non-interactively. `-r` pins it at our local
83+
# Verdaccio regardless of the ambient npm config.
84+
echo "smoketest-publishing: creating disposable publish user"
85+
npx --yes npm-cli-login@^1 \
86+
-u smoketest -p smoketest -e smoketest@example.com \
87+
-r "$REGISTRY_URL" --quotes
88+
89+
# Sanity: confirm we are authenticated against the local registry.
90+
npm whoami --registry "$REGISTRY_URL"
91+
92+
# Run the real release flow. `release:npm` calls `pack:all` (which
93+
# rebuilds dist/ via ts-node-pack) then `npm publish` for each .tgz;
94+
# inlining `npm_config_registry` redirects every publish inside this
95+
# one command to Verdaccio without leaking into the surrounding shell.
96+
echo "smoketest-publishing: running 'yarn release:npm'"
97+
(
98+
cd "$ROOT"
99+
npm_config_registry="$REGISTRY_URL" yarn release:npm
100+
)
101+
102+
# Install a representative subset into a throwaway consumer and exercise
103+
# it at runtime. Using `npm install <name>` (not `<path>`) forces npm to
104+
# resolve each package from the registry, which is the true end-to-end
105+
# check: it proves the published manifests, `workspace:` dep resolution,
106+
# and dependency graph are self-consistent within the registry.
107+
CONSUMER="$REGISTRY_HOME/consumer"
108+
mkdir -p "$CONSUMER"
109+
cat > "$CONSUMER/package.json" <<'EOF'
110+
{
111+
"name": "smoke-consumer",
112+
"private": true,
113+
"type": "module"
114+
}
115+
EOF
116+
echo "registry=$REGISTRY_URL" > "$CONSUMER/.npmrc"
117+
118+
echo "smoketest-publishing: installing packages from local registry"
119+
(
120+
cd "$CONSUMER"
121+
npm install --no-audit --no-fund \
122+
@endo/init \
123+
@endo/patterns \
124+
@endo/eventual-send \
125+
@endo/exo \
126+
@endo/pass-style \
127+
@endo/marshal
128+
)
129+
130+
echo "smoketest-publishing: running SES lockdown + runtime checks"
131+
cat > "$CONSUMER/smoke.mjs" <<'EOF'
132+
import '@endo/init';
133+
const p = await import('@endo/patterns');
134+
const x = await import('@endo/exo');
135+
const m = await import('@endo/marshal');
136+
const { M, matches } = p;
137+
const { makeExo } = x;
138+
const guard = M.interface('Foo', { greet: M.call().returns(M.string()) });
139+
const obj = makeExo('foo', guard, { greet: () => 'hi' });
140+
if (obj.greet() !== 'hi') {
141+
throw new Error(`makeExo.greet returned ${obj.greet()}, expected 'hi'`);
142+
}
143+
if (matches(42, M.number()) !== true) {
144+
throw new Error('matches(42, M.number()) was not true');
145+
}
146+
if (Object.keys(m).length === 0) {
147+
throw new Error('@endo/marshal has no exports');
148+
}
149+
console.log('smoketest-publishing: runtime checks passed');
150+
EOF
151+
node "$CONSUMER/smoke.mjs"
152+
153+
echo "smoketest-publishing: SUCCESS"

0 commit comments

Comments
 (0)