Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
f0b8d4a
chore: build the registry on install instead of vendoring the Daml deps
lmcorbalan Sep 2, 2026
f8ee690
docs: name npm run setup where the docs credited postinstall
lmcorbalan Sep 2, 2026
5639fd7
chore: remove registry/dist in npm run clean
lmcorbalan Sep 2, 2026
93477b9
chore: hold the root @types/node at the version the registry resolves
lmcorbalan Sep 2, 2026
9b94000
docs: correct the setup preconditions and what the npm scripts do
lmcorbalan Sep 2, 2026
3e4c446
docs: drop an absolute and an unmeasured claim from the setup paragraph
lmcorbalan Sep 2, 2026
58e3171
Merge pull request #164 from BootNodeDev/chore/registry-install-lifec…
lmcorbalan Sep 2, 2026
ca9ebca
feat: publish the registry as the root package's bin
lmcorbalan Sep 2, 2026
ec8124f
fix: raise the node floor to 20, which two runtime dependencies require
lmcorbalan Sep 2, 2026
7d04a6c
fix: keep a dist at any depth under registry ignored
lmcorbalan Sep 2, 2026
48e55f4
Merge pull request #165 from BootNodeDev/feat/registry-package-bin
lmcorbalan Sep 2, 2026
1fc5816
feat: fail when the root and registry manifests disagree on a dependency
lmcorbalan Sep 2, 2026
f281726
fix: compare every section a package is declared in, not the last one…
lmcorbalan Sep 2, 2026
3d3eb6a
fix: fail when engines.node or type is absent from both manifests
lmcorbalan Sep 2, 2026
d2cadd4
fix: name the operation that actually converges two lockfiles
lmcorbalan Sep 2, 2026
31273a2
fix: report both lockfiles when neither records a shared package
lmcorbalan Sep 2, 2026
c423634
feat: fail when a lockfile no longer records the manifest beside it
lmcorbalan Sep 2, 2026
7724fd9
fix: say a lockfile does not record a package instead of printing und…
lmcorbalan Sep 2, 2026
0a61363
docs: describe every rule the guard enforces in its header comment
lmcorbalan Sep 2, 2026
e2e28ff
fix: name the directory each lockfile is refreshed from
lmcorbalan Sep 3, 2026
a04d061
fix: rebuild both lockfiles, since deleting one does not converge
lmcorbalan Sep 3, 2026
87ed925
docs: add the three rules the header comment left out
lmcorbalan Sep 3, 2026
882af07
Merge pull request #166 from BootNodeDev/feat/registry-deps-guard
lmcorbalan Sep 3, 2026
a752abd
test: prove the packed registry installs and serves
lmcorbalan Sep 3, 2026
6ccb950
fix: report a failed pack instead of ending the run at the pack line
lmcorbalan Sep 3, 2026
3c8a5c4
fix: give the serving run the same clean environment as the no-config…
lmcorbalan Sep 3, 2026
062b2cd
fix: clean up on an interrupt instead of leaving a server and a temp dir
lmcorbalan Sep 3, 2026
4a77882
fix: name a service that died before the shutdown assertion could run
lmcorbalan Sep 3, 2026
a5be060
docs: say what the check establishes, what it does not, and what it n…
lmcorbalan Sep 3, 2026
7b47b56
fix: print a captured body without gluing the next line onto it
lmcorbalan Sep 3, 2026
8b48563
fix: read the tarball name from the last line, since npm 10 shares st…
lmcorbalan Sep 3, 2026
8539831
fix: report why the consumer install failed instead of silencing npm
lmcorbalan Sep 3, 2026
b94fcb5
fix: report a service that died before the metadata request
lmcorbalan Sep 3, 2026
ad21107
Merge pull request #167 from BootNodeDev/feat/registry-install-smoke
lmcorbalan Sep 3, 2026
f433770
ci: check the npm package installs and the manifests agree
lmcorbalan Sep 3, 2026
c660316
fix: read the ignore rules without the index, which hides tracked paths
lmcorbalan Sep 3, 2026
11e0ae0
fix: run the manifest guard before the install it guards
lmcorbalan Sep 3, 2026
1b0bd10
fix: report a check-ignore that could not answer instead of passing it
lmcorbalan Sep 3, 2026
964b35d
ci: gate on the npmrc that reconfigures every step below
lmcorbalan Sep 3, 2026
f4cf645
ci: assert where the build-output ignore rule lives, not only what it…
lmcorbalan Sep 3, 2026
b6b5ed8
docs: correct the gate's input list and why .gitignore sits in it
lmcorbalan Sep 3, 2026
5a61131
docs: state what npm ci actually refuses, since it is not this
lmcorbalan Sep 3, 2026
d33f80b
Merge pull request #168 from BootNodeDev/ci/registry-package-job
lmcorbalan Sep 3, 2026
c247e84
docs: document consuming the registry as an npm dependency
lmcorbalan Sep 3, 2026
6d29bef
docs: say the specifier pnpm prints has to be quoted before it parses
lmcorbalan Sep 3, 2026
8786fc3
docs: tie the boot check to the package name it resolves, not to the tag
lmcorbalan Sep 3, 2026
4980869
docs: name which package each pre-release version belongs to
lmcorbalan Sep 3, 2026
e39048c
docs: stop asserting a tag that has not been cut yet
lmcorbalan Sep 3, 2026
b9fd720
docs: scope the ungated comparison count to the check that runs them
lmcorbalan Sep 3, 2026
8e4c249
docs: stop denying the root commands a registry change now runs
lmcorbalan Sep 3, 2026
4965a15
docs: list both new scripts in the structure tree, not only the table
lmcorbalan Sep 3, 2026
274c410
docs: say the DAR version is irrelevant rather than that any DAR serves
lmcorbalan Sep 3, 2026
427969c
docs: rewrap the two lines the corrections left ragged
lmcorbalan Sep 3, 2026
6812b84
docs: say the ignore table reads git, and name the mutation it catches
lmcorbalan Sep 3, 2026
e814f42
docs: send a manifest dependency change to the guard that compares them
lmcorbalan Sep 3, 2026
07b83ea
docs: reflow the paragraph the previous rewrap left ragged
lmcorbalan Sep 3, 2026
42c2513
docs: stop offering a tag that is not cut and a pin that installs no …
lmcorbalan Sep 3, 2026
38f8ce5
docs: blame the reserved @ rather than the # for that key not parsing
lmcorbalan Sep 3, 2026
bc823cd
docs: say the template ids need quoting and where seed can be run
lmcorbalan Sep 3, 2026
dbc0707
docs: derive DAR byte-identity from the sources rather than the versi…
lmcorbalan Sep 3, 2026
240c695
docs: add the package.json version bump the release procedure never n…
lmcorbalan Sep 3, 2026
76a3c9a
docs: point the line-count recipe at the source dir, not the package …
lmcorbalan Sep 3, 2026
3aad5df
docs: say which package step is ungated instead of gating the whole job
lmcorbalan Sep 3, 2026
24437f2
docs: describe the release tags as one namespace rather than a split
lmcorbalan Sep 3, 2026
df3f57d
Merge pull request #170 from BootNodeDev/docs/registry-consumption
lmcorbalan Sep 3, 2026
bf5a9d5
fix: spell the smoke test's template ids out, so the sweep can see them
lmcorbalan Sep 3, 2026
8197c53
fix: clear the registry build output before rebuilding it
lmcorbalan Sep 3, 2026
c0677ae
fix: compare declared packages by own property, not by prototype memb…
lmcorbalan Sep 3, 2026
156efc4
docs: name the tag the release procedure is about to cut, not the one…
lmcorbalan Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ jobs:
registry/test/e2e/helpers/sandbox.ts
registry/test/helpers/fixtures.ts
registry/test/ledger.test.ts
# Its ids configure a service the check never lets reach a
# participant, so nothing there resolves them and a wrong one
# would not red that script. They are compared here for the same
# reason the rest are: daml/ is where they can go stale.
scripts/registry-install-smoke.sh
scripts/seed.mjs
)
# Two more files carry the prefix and must not be checked against
Expand Down Expand Up @@ -603,3 +608,184 @@ jobs:
SKIP_BUILD: '1'
ALLOW_UNTAGGED: '1'
run: bash scripts/release-notes.sh "${GITHUB_REF_NAME}"

# This job's name is the status check context that branch protection or a
# ruleset would match on. Renaming it, including a capitalisation change,
# stops that check from reporting on every pull request, this rename's own
# included. The scope gate is per-step rather than one job-level `if:` so
# that a scoped-out pull request still reports a green check carrying the
# scope log that says which paths it looked at. A job-level `if:` would
# report `skipped` instead, which a required check accepts but which reads
# on the pull request as though the job never ran.
package:
name: package
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

# registry/dist is the package's payload and must not be committed, while
# a dist directory anywhere else under registry/ is build output of a kind
# nothing ships. The rule drawing that line lives in the ROOT .gitignore
# rather than beside the code, because npm applies a nested ignore file to
# the pack walk even for a path the manifest's "files" allowlist names,
# while the allowlist outranks the root file. That placement is exactly
# what hides the rule from every other check here: the root file cannot
# subtract from the tarball, so re-anchoring it to `registry/dist` leaves
# the smoke test and the manifest guard below both at exit 0 while build
# output under registry/src and registry/test silently stops being
# ignored. That is the regression 7d04a6c fixed, and this table is the
# only thing that holds it. Three things are asserted: a rule that stops
# covering the tree is as wrong as one that reaches past registry/'s own
# build output, and either pattern satisfies both of those from inside
# registry/.gitignore while emptying the package, so where the rule lives
# is read as well. Ungated, like the sweeps at the top of the daml job: it
# needs nothing but the checkout, so it reports in seconds and cannot be
# silenced by a later narrowing of the gate below.
- name: Verify the build-output ignore rule
run: |
set -euo pipefail
status=0
# check-ignore answers 0 when a rule matches and 1 when none does,
# and both are results rather than failures, so the status is held
# rather than left to -e, which would end the step at the first
# mismatch and hide every case after it. Anything above 1 means it
# could not answer at all: taking that for "no rule matched" would
# print the second list as passing while git was failing outright.
# None of these paths need to exist; the command matches patterns.
# --no-index because without it check-ignore consults the index and
# calls every TRACKED path unignored whatever the patterns say. That
# would make the registry/src/index.ts case below vacuous, since it is
# the one tracked path here: a rule that swallowed the service's own
# source would still read as a pass.
for path in registry/dist/index.js \
registry/src/dist/bundle.js \
registry/test/dist/bundle.js; do
rc=0
git check-ignore -q --no-index "$path" || rc=$?
case "$rc" in
0) echo "ok: ${path} is ignored" ;;
1) echo "${path} is not ignored: build output under registry/ is committable" >&2
status=1 ;;
*) echo "git check-ignore could not answer for ${path}: status ${rc}" >&2
status=1 ;;
esac
done
for path in registry/dist-backup/index.js \
registry/src/index.ts \
dist/index.js \
scripts/dist/index.js; do
rc=0
git check-ignore -q --no-index "$path" || rc=$?
case "$rc" in
1) echo "ok: ${path} is not ignored" ;;
0) echo "${path} is ignored: the rule reaches past registry/'s build output" >&2
status=1 ;;
*) echo "git check-ignore could not answer for ${path}: status ${rc}" >&2
status=1 ;;
esac
done
# Both loops read what the rule COVERS, and the same patterns cover
# the same tree from inside registry/.gitignore, where npm applies
# them to the pack walk as well: measured at 8 tarball entries rather
# than 24, registry/dist gone and the bin target with it, while all
# seven verdicts above stay green. The smoke test below is the only
# other thing that sees it, and only when the gate lets it run, so the
# file carrying the rule is read here too. -v prints the deciding
# source as `<file>:<line>:<pattern>`; the first field is the whole
# assertion.
rc=0
match="$(git check-ignore -v --no-index registry/dist/index.js)" || rc=$?
case "$rc" in
0) ignore_file="${match%%:*}"
if [ "$ignore_file" = ".gitignore" ]; then
echo "ok: the rule ignoring registry/dist lives in the root .gitignore"
else
echo "registry/dist is ignored by ${ignore_file} rather than the root .gitignore: a nested ignore file subtracts it from the npm pack walk" >&2
status=1
fi ;;
1) echo "no rule ignores registry/dist, so there is no placement to read" >&2
status=1 ;;
*) echo "git check-ignore could not answer for registry/dist/index.js: status ${rc}" >&2
status=1 ;;
esac
exit "$status"

- name: Scope
id: scope
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" != "pull_request" ]; then
echo "Not a pull request: verifying unconditionally."
echo "run=true" | tee -a "$GITHUB_OUTPUT"
exit 0
fi
# A blip on this fetch reds the job having verified nothing, so it is
# retried rather than taken at its word. The step above is ungated and
# sits there so that a red here finds it already run and reported.
for attempt in 1 2 3; do
git fetch --no-tags --prune origin \
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" && break
echo "git fetch attempt ${attempt} of 3 failed" >&2
[ "$attempt" -lt 3 ] || exit 1
sleep 5
done
changed="$(git -c core.quotePath=false diff --name-only --no-renames "origin/${BASE_REF}...HEAD")"
echo "Changed files:"
echo "$changed"
# The package's own inputs: the root manifest and lockfile by name,
# the registry ones and every packed source through the registry/
# prefix, and the two scripts the steps below run. LICENSE and
# README.md are packed too, whatever "files" says, and are left out
# on purpose: no check here or downstream asserts either, so gating
# on them would only lengthen the job. .gitignore is listed to keep
# this enumeration complete rather than for coverage, since the step
# above is ungated and reads that file on every event whether or not
# the gate names it, while the three steps below are blind to the
# root rule. .npmrc is the opposite case and this gate is the only
# thing holding it. It reconfigures npm for every step below rather
# than naming a file any of them read, which is what keeps it out of
# a list written by thinking about inputs: `omit=dev` on its own
# leaves prepare with no compiler, exit 127 before anything builds.
if grep -Eq '^(package\.json$|package-lock\.json$|registry/|scripts/check-registry-deps\.mjs$|scripts/registry-install-smoke\.sh$|\.gitignore$|\.npmrc$|\.github/workflows/ci\.yml$)' <<<"$changed"; then
echo "run=true" | tee -a "$GITHUB_OUTPUT"
else
echo "run=false" | tee -a "$GITHUB_OUTPUT"
fi

- uses: actions/setup-node@v4
if: steps.scope.outputs.run == 'true'
with:
node-version: '22'
cache: npm
cache-dependency-path: package-lock.json

# Ahead of the install it guards, for the reason the daml job places its
# own sweeps first: the script only reads four tracked JSON files, so it
# needs no node_modules and reports in a moment. npm ci is not a second
# reading of this. It reds only where the lockfile can no longer satisfy
# the range beside it, and then with npm's own "Invalid: lock file's
# express@4.22.2 does not satisfy express@5.2.1"; a widened range the
# lockfile still satisfies installs clean, and a disagreement between the
# two trees is invisible to it whatever the ranges say.
- name: Check the manifests agree
if: steps.scope.outputs.run == 'true'
run: npm run check:deps

# npm ci runs `prepare`, so this compiles registry/src against the ROOT
# dependency set, which is the set a consumer gets. The registry job
# compiles the same source against registry/node_modules; only this one
# would catch a type package that arrives there transitively and is
# declared nowhere.
- name: Install
if: steps.scope.outputs.run == 'true'
run: npm ci

- name: Install smoke test
if: steps.scope.outputs.run == 'true'
run: npm run smoke:registry
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ docs/

# Canton sandbox ports/ready file written by scripts/sandbox.sh
.canton-ports.json

# The registry's build output ships inside the npm package, so the rule that
# keeps it out of git lives here rather than beside it: npm applies a NESTED
# ignore file to the pack walk even for a path the root "files" allowlist
# names, while the allowlist does outrank this file. The glob is what makes an
# anchored root rule cover what the unanchored one beside the code covered, a
# dist directory at any depth under registry/, not only the compiler's own.
registry/**/dist
10 changes: 7 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
| Language | Daml | LF target 2.1 (`build-options: --target=2.1`) |
| SDK | Daml SDK 3.4.11 | installed and driven via `dpm` |
| Build tool | `dpm` (Digital Asset Package Manager) | not the legacy `daml` assistant (removed as of SDK 3.5) |
| Task runner | `npm` scripts | thin wrappers over `dpm`; set `LANG=C.UTF-8` |
| Task runner | `npm` scripts | the Daml ones wrap `dpm` with `LANG=C.UTF-8`; `prepare` builds `registry/` |
| Standard | CIP-0056 (CN Token Standard) | interface-faithful, clean-room (no economics) |
| Dependencies | `splice-api-token-*` interface DARs | vendored from `canton-network/splice`; NOT `splice-amulet` |
| Testing | `daml-script` (`dpm test`) | lives in the separate `canton-token-forge-test` package |
Expand Down Expand Up @@ -80,6 +80,8 @@ scripts/
release-notes.sh Emit the release body for a tag, snippet extracted from consumer-smoke/consumer/daml.yaml
sandbox.sh Build the DAR and run a local Canton sandbox with the JSON Ledger API
seed.mjs Seed a running sandbox with an admin, demo users, and one InstrumentConfig
check-registry-deps.mjs Fail when the root and registry manifests disagree on a dependency
registry-install-smoke.sh Pack the npm package, install it into a scratch consumer, and run the bin
deps/ Vendored Splice sources + built DARs (gitignored; never edit or commit)
multi-package.yaml Wires the two daml/ packages into one workspace; consumer-smoke/ has its own
versions.env Single version knob: SPLICE_TAG
Expand Down Expand Up @@ -363,14 +365,16 @@ overrides from `SEED_*`/`LEDGER_*` ([`RUNBOOK.md`](RUNBOOK.md)).

| Command | Purpose |
|---------|---------|
| `npm install` / `npm run setup` | Vendor Splice into `deps/` and create the stable-name symlinks (`scripts/fetch-dep.sh`). |
| `npm run setup` | Vendor Splice into `deps/` and create the stable-name symlinks (`scripts/fetch-dep.sh`). |
| `npm run build` | Build both packages (production, then test). |
| `npm run build:canton-token-forge` | Build only the production package. |
| `npm test` | Build the production DAR, then run the `canton-token-forge-test` suite. |
| `npm run test:coverage` | Same as `npm test` with a template-focused coverage report. |
| `npm run smoke` | Compile a package that data-depends on nothing but the built DAR (`scripts/consumer-smoke.sh`); proves the release artifact is consumable on its own. |
| `npm run check:deps` | Fail if the root and `registry/` manifests disagree on any dependency. |
| `npm run smoke:registry` | Pack the npm package, install it into a scratch consumer, and run the bin; proves the published service is consumable on its own. |
| `bash scripts/release-notes.sh <tag>` | Emit the release body, with the consumer snippet extracted from `consumer-smoke/consumer/daml.yaml`. Refuses if `<tag>` does not name the checked-out commit, if the working tree is dirty, or if `deps/` carries no commit stamp (`npm run setup` writes it); `ALLOW_UNTAGGED=1` previews a body before the tag exists ([`RUNBOOK.md`](RUNBOOK.md#cutting-a-release)). |
| `npm run clean` | Remove both `.daml` build dirs and the consumer smoke test's output. |
| `npm run clean` | Remove both `.daml` build dirs, the consumer smoke test's output, and `registry/dist`. |
| `npm run sandbox` | Build the DAR and run a local Canton sandbox with the JSON Ledger API. |
| `npm run seed` | Seed a running sandbox with an admin, demo users, and one `InstrumentConfig`. |
| `bash scripts/build-harness.sh` | Build the Amulet test harness (unused by default; conformance only). |
Expand Down
Loading
Loading