Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
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
4 changes: 4 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
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 @@ -369,6 +371,8 @@ overrides from `SEED_*`/`LEDGER_*` ([`RUNBOOK.md`](RUNBOOK.md)).
| `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, 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. |
Expand Down
60 changes: 47 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ installing this repository needs no `dpm`, no JDK and no clone of Splice.
| `npm test` | Build the `canton-token-forge` DAR, then run the `canton-token-forge-test` suite. |
| `npm run test:coverage` | Same, with a coverage report focused on your templates. |
| `npm run smoke` | Build the DAR, then compile a package that data-depends on nothing but it, proving the 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. |
| `npm run clean` | Remove both `.daml` build dirs, the consumer smoke test's output, and `registry/dist`. |
| `npm run setup` | Re-vendor deps + re-create the stable symlinks. |
| `npm run sandbox` | Build the DAR and run a local Canton sandbox with the JSON Ledger API. |
Expand All @@ -119,7 +121,9 @@ installing this repository needs no `dpm`, no JDK and no clone of Splice.

`registry/` is a separate npm package with its own dependency tree; the root
`npm install` does not populate `registry/node_modules`. Run its commands from
that directory.
that directory. The root manifest is also what builds and ships this service
as an installable package, and `npm run check:deps` is what keeps the two
dependency lists in step.

| Command | Does |
| --- | --- |
Expand Down Expand Up @@ -281,24 +285,54 @@ Run before declaring work done:
- `npm run smoke` - when you changed what the production DAR exposes: a
renamed module or template, its dependencies, its interface instances, or its
`build-options`

Every pull request gets three comparisons whatever it touches, because the
`daml` check runs them ahead of its toolchain install and outside its own
scope gate: the strings that spell a template id, against `daml/`; the
consumer snippet in `README.md`, against `consumer-smoke/consumer/daml.yaml`;
and every tracked `daml.yaml`, against both the SDK version the workflow
installs and LF 2.1. One that touches
- `npm run check:deps` - when you changed a dependency in the root or the
`registry/` manifest; `npm run smoke:registry` never compares the two
- `npm run smoke:registry` - when you changed the root manifest, the
registry's dependencies, or either ignore file

Every pull request gets three comparisons in the `daml` check whatever it
touches, because that check runs them ahead of its toolchain install and
outside its own scope gate: the strings that spell a template id, against
`daml/`; the consumer snippet in `README.md`, against
`consumer-smoke/consumer/daml.yaml`; and every tracked `daml.yaml`, against
both the SDK version the workflow installs and LF 2.1. One that touches
`daml/`, `consumer-smoke/`, `scripts/consumer-smoke.sh` or the root build
inputs runs four of these five automatically in that same check, after those
three steps: only `npm run test:coverage` does not. One that touches `registry/`
runs that package's own lint, both typechecks, and unit suite as the
`registry` check, not the root commands above. `npm run test:coverage` and
the registry's `npm run test:e2e` are both off the pull-request path: the
first re-runs Splice's own suites, the second needs a live participant. The
three steps: only `npm run test:coverage` does not. One that touches
`registry/` runs that package's own lint, both typechecks, and unit suite as
the `registry` check, none of the Daml root commands above, and the `package`
check below alongside it. `npm run test:coverage` and the registry's `npm run
test:e2e` are both off the pull-request path: the first re-runs Splice's own
suites, the second needs a live participant. The
`release` workflow adds checks of its own that no pull request runs. The
end-to-end suite is typechecked on that path even so, which is the point of
typechecking it separately from the run.

A third check, `package`, gates on `package.json`, `package-lock.json`,
`registry/`, `scripts/check-registry-deps.mjs`,
`scripts/registry-install-smoke.sh`, `.gitignore`, `.npmrc` and
`.github/workflows/ci.yml` itself, so a pull request touching `registry/` runs
both the `registry` check and this one. It has four verification steps, not
three, and only three of them sit behind that gate: `npm run check:deps`;
`npm ci`, which runs `prepare` and so compiles `registry/src` against the root
dependency set rather than `registry/`'s own, catching a type package that
arrives there transitively and is declared nowhere; and `npm run
smoke:registry`. The fourth is ungated and runs on every trigger of the
workflow regardless of what changed, ahead of the other three exactly as the
`daml` check's own comparisons run ahead of its gate: a `git check-ignore`
table confirming that build output under `registry/` stays out of git,
`registry/dist` included, which must not be committed even though it is the
package's own payload, and that the rule ignoring it lives in the root
`.gitignore` rather than a nested one. Each half catches a mutation the other
cannot see. Narrowing the root rule to `registry/dist` passes the smoke test,
the manifest guard and the placement read alike, while build output under
`registry/src` and `registry/test` silently stops being ignored, so the table
reads what the rule covers. Moving the same pattern into
`registry/.gitignore` leaves that coverage intact but subtracts
`registry/dist` from the npm pack walk, emptying the package to 8 entries from
24; only the smoke test sees that, and only when the gate lets it run, so the
table reads where the rule lives as well.

A pushed tag whose `v` is followed by a digit (`v[0-9]*`, so `vnext` and
`vendor` trigger nothing) runs the `release` workflow instead: it refuses a
tag `main` does not reach, builds from a clean checkout, runs the suite,
Expand Down
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,95 @@ in both directions, so a flag, target or path that changes on one side reds
rather than ships. The release body carries the same blocks, generated straight
from that file.

## Consuming the registry service

The registry service is published from this repository as an npm package,
consumed from git at a tag rather than from the public registry:

```json
{
"dependencies": {
"@bootnodedev/canton-token-forge": "github:BootNodeDev/canton-token-forge#v0.2.0"
}
}
```

Two preconditions. This repository is private, so the install needs
credentials that can read it: a token-based HTTPS credential helper or an SSH
key for `github.com`, whichever your environment already uses for private git
dependencies. And the participant the service points at must host the
`canton-token-forge` package the configured template ids name: they are checked
at boot and resolve by package name, not by package id, so which release built
the DAR does not matter, and an id the participant cannot resolve stops the
service starting.

`v0.2.0` is the first tag to carry the npm package, and the tag this section
is written against; until it is cut, pin the commit sha instead. Do not pin
`v0.1.0`: it predates the package, so its manifest is unscoped, declares no
`bin` and no `files`, and runs `scripts/fetch-dep.sh` as a `postinstall`,
which installs no service and clones Splice into your `node_modules`. From
`v0.2.0` on the tags are one namespace, every `v[0-9]*` tag being both a DAR
release and an npm package pin. See "Consuming a release" above for the DAR
itself.

`npm install` builds `registry/src` through the package's `prepare` script and
links one bin, `canton-token-forge-registry`, unmodified. `pnpm install`
refuses by default: pnpm will not run a git-hosted package's build scripts
unless the consumer allowlists it, and `registry/dist` is gitignored, so
`prepare` is the only thing that produces the bin. Add the resolved git
specifier to `pnpm-workspace.yaml`, using whichever key your pnpm major reads:

```yaml
# pnpm 11 and later
allowBuilds:
"@bootnodedev/canton-token-forge@git+https://github.com/BootNodeDev/canton-token-forge.git#<resolved-sha>": true

# pnpm 10
onlyBuiltDependencies:
- "@bootnodedev/canton-token-forge@git+https://github.com/BootNodeDev/canton-token-forge.git#<resolved-sha>"
```

The key is the full resolved git specifier, not the bare package name, a
version range, or a wildcard: pnpm resolves the tag to its commit sha and
matches on that exact string. Its own refusal error prints that specifier
unquoted, and pasted that way it does not parse: `@` is a reserved indicator
in YAML, so a plain scalar cannot begin with one, and the parser answers `bad
indentation of a mapping entry`. Quote it as the block above does. The `#` is
harmless either way, since YAML opens a comment only at a `#` that follows
whitespace, and this one sits inside the scalar. Since the sha is resolved
from the tag, this entry changes whenever the pin does. With it present,
`pnpm exec canton-token-forge-registry` runs the same as `npm`'s link.

The service reads its whole configuration from the environment, plus a `.env`
loaded from the working directory it is started in. Required, eight:
`LEDGER_API_URL`, `LEDGER_API_TOKEN`, `ADMIN_PARTY`, and the five template ids
(`INSTRUMENT_CONFIG_TEMPLATE_ID`, `PREAPPROVAL_TEMPLATE_ID`,
`LOCKED_TOKEN_TEMPLATE_ID`, `TRANSFER_INSTRUCTION_TEMPLATE_ID`,
`ALLOCATION_TEMPLATE_ID`). Optional, four: `PORT`, `LEDGER_USER_ID`,
`SHUTDOWN_TIMEOUT_MS`, `DIRECT_TRANSFER_MARGIN_MS`. The package ships
`registry/.env.example` with the full list and what each variable is for.

Quote all five template ids in a `.env` file. Every one of them begins with
`#`, which dotenv reads as the start of a comment, so an unquoted
`INSTRUMENT_CONFIG_TEMPLATE_ID=#canton-token-forge:...` parses to the empty
string and the boot rejects it as `missing required env var
INSTRUMENT_CONFIG_TEMPLATE_ID`, naming a variable that is in fact set. Values
passed through the environment rather than a file need no quoting beyond
whatever the shell wants.

`npm run seed`, run from a clone of this repository against a local sandbox,
prints the whole block filled in and quoted with the real admin party and the
five template ids. `scripts/seed.mjs` is not part of the package, so that is a
step you take in a checkout, not in the consumer. `ADMIN_PARTY` and all five
template ids are checked against the participant at boot, so a value it cannot
resolve stops the service starting rather than failing later.

What the package contains: `registry/dist`, `registry/openapi` and
`registry/.env.example`, plus `package.json`, `README.md` and `LICENSE` (24
entries). The manifest that travels with it is this repository's own, so its
`dpm` and `daml/` scripts cannot run from an installed copy; `prepare` is the
only entry npm acts on.

## Requirements

- `dpm` (Digital Asset Package Manager) and a JDK 17+ on `PATH`
Expand Down
24 changes: 22 additions & 2 deletions RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,23 @@ reason the seed script looks the way it does.

## Cutting a release

Every `v[0-9]*` tag is a DAR release, including the tag a consumer's
`package.json` pins the npm package at (see the README's "Consuming the
registry service"). `daml/canton-token-forge/daml.yaml` stays at `0.0.1`
whatever tag is cut, so every release attaches an asset under the same name,
`canton-token-forge-0.0.1.dar`. Whether two releases' assets are the same
bytes does not follow from that version string: it follows from `daml/`,
`versions.env` and the SDK being unchanged between them. Nothing under those
paths has moved since `v0.1.0`, so a tag cut from this commit would carry the
sha256 the `v0.1.0` body records; read it off the workflow's own output rather
than assuming it, as steps 1 and 4 below do.

The root `package.json`'s `version` is the npm package's, and nothing checks
it against the tag: `release.yml` reads only `daml/canton-token-forge/daml.yaml`.
Bring it in step with the tag you are about to cut, in a commit merged to
`main` before you tag, or consumers install a package whose manifest names a
version the pin does not.

`.github/workflows/release.yml` builds and publishes. It runs the full suite,
checks the DAR is byte-reproducible, and compiles `consumer-smoke/` against the
built artifact before anything is published. The release body, including the
Expand Down Expand Up @@ -357,15 +374,18 @@ CI just proved compile.
```

Then `npm run clean`, and delete that release and its tag.
2. Tag and push. This is the decision that matters: a downstream repository pins
2. Set the root `package.json`'s `version` to the tag without its `v`, and land
that on `main`. Nothing enforces this, and the tag is what a consumer's
`package.json` resolves to.
3. Tag and push. This is the decision that matters: a downstream repository pins
it permanently.

```bash
git tag v0.1.0
git push origin v0.1.0
```

3. Confirm the release carries `canton-token-forge-0.0.1.dar` and that its body
4. Confirm the release carries `canton-token-forge-0.0.1.dar` and that its body
shows the sha256 and package-id. The download-and-compile from step 1 is
worth repeating here, against the real tag.

Expand Down
Loading
Loading