Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
56f5d62
Move maplibre-gl to optional peerDependencies
alexluckett Jun 9, 2026
c59682c
refactor: flatten beta/ provider and plugin directories
alexluckett Jun 9, 2026
12273a5
feat: add package.json to each provider
alexluckett Jun 9, 2026
0352532
feat: add package.json to engine-agnostic plugins
alexluckett Jun 9, 2026
7c4826d
feat: add package.json to engine-coupled plugins
alexluckett Jun 9, 2026
f6dda29
feat: add workspaces, trim exports, remove engine peer deps from root
alexluckett Jun 9, 2026
719625d
fix: update beta/ paths to flat paths in jest coveragePathIgnorePatterns
alexluckett Jun 9, 2026
fac5193
fix: remove beta/ from rollup build paths
alexluckett Jun 9, 2026
b209019
fix: remove beta/ from webpack build paths
alexluckett Jun 9, 2026
b9cca0d
fix: update relative import paths after beta/ directory flatten
alexluckett Jun 10, 2026
7e87b93
feat: extend publish script for monorepo — stamp versions, publish al…
alexluckett Jun 10, 2026
8cb8ee2
docs: update import paths to new monorepo package names
alexluckett Jun 10, 2026
493535b
fix: update beta/ paths in govuk-prototype-kit asset list
alexluckett Jun 10, 2026
ee2854b
fix: correct node_modules relative import depth in draw-ml modes
alexluckett Jun 10, 2026
5b3be7b
fix: add draw-ol to webpack UMD builds; fix draw-ml node_modules impo…
alexluckett Jun 10, 2026
fbb0320
fix: replace direct node_modules imports with public MapboxDraw API
alexluckett Jun 10, 2026
ce268a2
fix: update beta/ paths in demo JS files
alexluckett Jun 10, 2026
b2b13da
chore: update lockfile for workspace packages
alexluckett Jun 10, 2026
a5f420b
fix: add **/*.scss to sideEffects in packages with SCSS imports
alexluckett Jun 10, 2026
7e30c5c
fix: correct CSS exports, add datasets adapter entry, fix publish script
alexluckett Jun 17, 2026
3cffe5a
fix: expose ./package.json in root exports for tooling resolution
alexluckett Jun 18, 2026
8f7c7f8
Merge remote-tracking branch 'origin/main' into maplibre-peer-dependency
alexluckett Jun 18, 2026
5f25548
fix: correct logger import depth after beta/ flatten in merged files
alexluckett Jun 18, 2026
a791ed5
Add package bootstrap tooling and release runbook
alexluckett Jul 6, 2026
a297aa8
fix: harden release scripts — preflight path bug, version gate holes,…
alexluckett Jul 7, 2026
8942681
fix: update remaining plugins/beta paths in demo files after flatten
alexluckett Jul 7, 2026
8371d19
fix: remove phantom ./css export from draw-ml package
alexluckett Jul 7, 2026
234110b
fix: declare externalized runtime deps in workspace packages
alexluckett Jul 7, 2026
d9a78fb
docs: migrate remaining examples and guides to new package names
alexluckett Jul 7, 2026
e124a1f
docs: add network-error failure mode to preflight table in RELEASING.md
alexluckett Jul 7, 2026
d275449
Merge remote-tracking branch 'origin/main' into maplibre-peer-dependency
alexluckett Jul 7, 2026
1b7dd5f
fix: reorder imports above const bindings in draw-ml modes (import/fi…
alexluckett Jul 7, 2026
6287d1b
fix: update sonar exclusions from beta/ paths to flat beta-package paths
alexluckett Jul 7, 2026
f714bd2
fix: update plugins/beta paths in prototype kit templates after flatten
alexluckett Jul 8, 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
43 changes: 43 additions & 0 deletions .github/workflows/seed-packages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Seed new packages to NPM

# One-time bootstrap for brand-new workspace packages (providers/* and plugins/*).
#
# npm OIDC trusted publishing can only be configured for a package that already
# exists on the registry, so a new package cannot be published by the OIDC-only
# publish.yml on its first run. This workflow publishes a bare placeholder for
# any not-yet-existing package using a temporary NPM_TOKEN secret.
#
# This token is NOT meant to live in the repo long term. The procedure is:
# 1. Create a short-lived granular npm token (publish + create-package on @defra).
# 2. Add it as a repository secret named NPM_TOKEN.
# 3. Run this workflow (dry run first, then for real).
# 4. DELETE the NPM_TOKEN secret and revoke the token on npmjs.com.
# See RELEASING.md for the full procedure.

on:
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run — list what would be seeded without publishing.'
required: false
type: boolean
default: false

permissions:
contents: read

jobs:
seed:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v5
with:
node-version-file: .nvmrc
registry-url: 'https://registry.npmjs.org'

- name: Seed new packages
run: ./scripts/seed-packages.sh "${{ inputs.dry_run }}"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
241 changes: 241 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# Releasing

This repo is an npm workspaces monorepo. A single release publishes the core
package and every provider/plugin **in lockstep at the same version**:

| Package | npm name |
| --- | --- |
| Core | `@defra/interactive-map` |
| Providers | `@defra/interactive-map-provider-*` |
| Plugins | `@defra/interactive-map-plugin-*` |

Publishing is driven by [`scripts/publish-package.sh`](scripts/publish-package.sh)
and runs entirely on **npm OIDC trusted publishing** — there is no long-lived
npm token in CI.

There are two distinct flows:

- **[One-time bootstrap](#one-time-bootstrap-new-packages-only)** — required once
per brand-new package before it can ever be released.
- **[Cutting a release](#cutting-a-release)** — the normal, repeatable flow.

---

## One-time bootstrap (new packages only)

> Skip this entirely if every package already exists on npm. You only do this
> when you add a *new* provider or plugin.

### Steps at a glance

For a brand-new package, all four steps are required and **in this order** —
don't stop after seeding:

1. **Merge** the PR (new packages + workflows) to `main`.
2. **[Step 1 — Seed](#step-1--seed-the-new-package-names)** the package names
onto npm, using a temporary token.
3. **[Step 2 — Configure OIDC trusted publishing](#step-2--configure-oidc-trusted-publishing-on-npmjs)**
on npmjs.com, **once per new package**. Skipping this makes the release in the
next step fail with an auth error.
4. **[Cut a release](#cutting-a-release)** — the normal flow; publishes the real
packages via OIDC.

Steps 2–3 are one-time per package. After that, every future release is just
step 4.

### Why it's needed

npm OIDC trusted publishing can only be configured on a package that **already
exists** on the registry — there is no "pending publisher" concept. So the
OIDC-only release pipeline cannot publish a package for the very first time:
there is no trusted-publisher config to authenticate against yet.

The fix is a two-step bootstrap: **seed** the package name onto npm with a
short-lived token, then **configure trusted publishing** on it. After that the
normal OIDC pipeline owns it forever.

> **Order matters: merge first.** A `workflow_dispatch` workflow only appears in
> the Actions UI once it's on the **default branch** (`main`), and the seed job
> checks out whatever branch it's dispatched from. So the new packages and the
> seed workflow must both be on `main` before you can seed. The full first-time
> sequence is: **merge the PR to `main` → seed (Step 1, from `main`) → configure
> OIDC (Step 2) → cut the first release.**

### Step 1 — Seed the new package names

This publishes a bare placeholder (just `package.json`, no build output) under a
dedicated `seed` dist-tag, so it never occupies `latest`/`beta`/`alpha`. It only
touches packages that don't yet exist on npm, so it's safe to re-run.

1. On npmjs.com, create a **granular access token**:
- Scope: **Read and write** packages under `@defra`.
- Permission to **create new packages** in the scope.
- Shortest practical expiry (e.g. 1 day).
2. In GitHub → repo **Settings → Secrets and variables → Actions**, add a
repository secret named **`NPM_TOKEN`** with that token.
3. Run the **"Seed new packages to NPM"** workflow
(Actions tab → select workflow → **Run workflow**):
- First with **`dry_run: true`** and confirm the list of packages to be
seeded looks right.
- Then with **`dry_run: false`** to actually seed them.
4. Confirm each seeded package landed **only** under the `seed` dist-tag and has
no `latest` (a bare seed under `latest` would make `npm install` resolve an
empty, build-less package):

```bash
npm view @defra/interactive-map-plugin-<x> dist-tags # expect: { seed: '...' }, no latest
```
5. **Immediately delete the `NPM_TOKEN` secret** and **revoke the token** on
npmjs.com. The token has done its job and must not linger.

> Why a temporary secret rather than a workflow input: GitHub secrets are
> encrypted, auto-masked, and never shown in the UI. `workflow_dispatch` inputs
> are not — they're recorded in run metadata in plaintext, so they're the wrong
> place for a credential even for a one-off.

### Step 2 — Configure OIDC trusted publishing on npmjs

Do this **once per newly-seeded package** (the core package is already
configured). For each new package:

1. Go to `https://www.npmjs.com/package/<package-name>` → **Settings** tab.
2. Find the **Trusted Publisher** section → **Select your publisher** →
**GitHub Actions**.
3. Fill in:
- **Organization / owner:** `DEFRA`
- **Repository:** `interactive-map`
- **Workflow filename:** `publish.yml`
- **Environment:** leave blank (the publish job does not use one).
4. Save. (npm does not validate the config on save — a wrong value only surfaces
as an auth failure at publish time, so double-check the fields.)

Each package can have only one trusted publisher. Once configured, that package
is published by OIDC on every future release with no token.

> Tip: this is the tedious part — you repeat it for each new package. Verify it
> worked by doing your next release as a **pre-release** first (see below) and
> checking every package published.

---

## Cutting a release

Once all packages exist and have trusted publishing configured, releasing is just
creating a GitHub Release. **Merging to `main` does not publish anything** — CI
on `main` only runs lint/test. The release pipeline triggers **only** on a
published GitHub Release.

### 1. Pick the version

- All packages are versioned in lockstep from the release **tag**.
- The tag must be `vX.Y.Z` (e.g. `v1.4.0`) or a pre-release `vX.Y.Z-alpha.N`
(e.g. `v1.4.0-alpha.1`).
- The version must be **greater than the latest already-published version in the
same major line** for the core package, or the pipeline aborts
(`validate_version_bump`).

### 2. Create the GitHub Release

1. Make sure `main` is green and contains the commit you want to ship.
2. GitHub → **Releases → Draft a new release**.
3. **Choose a tag** → type the new tag (e.g. `v1.4.0`) → **Create new tag on
publish**, targeting `main`.
4. Add release notes.
5. **Pre-release vs standard:**
- Tick **"Set as a pre-release"** (or use an `-alpha.N` tag suffix) to publish
everything under the `alpha` dist-tag — use this to validate a release
safely.
- Leave it unticked for a standard release.
6. **Publish release.**

That triggers the **"Publish to NPM"** workflow:

1. Runs CI (lint/test) and builds all packages.
2. **Pre-flight gate** — before anything is published or stamped,
`scripts/publish-package.sh` asserts that **every** package (core + all
providers/plugins) can be published at the target version. If any package
fails, the whole release aborts here with **nothing published**, so you can
never get a partial release. See [Pre-flight checks](#3-pre-flight-checks)
below.
3. Stamps the release version into every `package.json` (and pins each package's
`@defra/interactive-map` peer dependency to that exact version).
4. Publishes core first, followed by every provider and plugin.

### 3. Pre-flight checks

The pre-flight gate runs once over all packages and reports **every** problem
before publishing starts. The failure modes and how to fix each:

| Pre-flight failure | Meaning | Fix |
| --- | --- | --- |
| `not found on npm — needs bootstrap` | A new package was never seeded / OIDC-configured (npm returned an explicit E404) | Do the [One-time bootstrap](#one-time-bootstrap-new-packages-only), then re-release |
| `could not query npm (network/registry error)` | npm couldn't be reached — the gate can't tell whether the package exists | Re-run the release; if it persists, check [status.npmjs.org](https://status.npmjs.org) |
| `version X is already published` | This version exists (often a partial earlier release) | Cut a new patch version — see [Troubleshooting](#troubleshooting) |
| `npm publish --dry-run failed` | Missing build output or invalid `package.json` | Fix the package/build — see [Troubleshooting](#troubleshooting) |

It also prints an **ℹ first real release** reminder for any package still on only
its `seed` placeholder version, so you double-check trusted publishing is
configured before its first real publish.

> **Residual gap:** the gate can confirm a package *exists* on npm, but cannot
> verify its OIDC trusted-publisher config is correct without actually publishing
> (there's no way to exercise the OIDC token exchange in a dry run). The safest
> validation is to cut a **pre-release** first — it publishes every package under
> `alpha` and surfaces any auth problem without touching `latest`/`beta`.

### 4. Dist-tags — what lands where

| Release type | Core | `provider-maplibre`, `plugin-interact`, `plugin-search` | All other providers/plugins |
| --- | --- | --- | --- |
| **Pre-release** (pre-release flag **or** `-alpha.N` tag) | `alpha` | `alpha` | `alpha` |
| **Standard release** | `latest` | `latest` | `beta` |

The split on a standard release comes from each package's
`publishConfig.tag`: packages considered stable have none (→ `latest`), the rest
carry `"publishConfig": { "tag": "beta" }`.

> **Consequence to be aware of:** a package published only under `beta` has **no
> `latest` dist-tag**, so a plain `npm install @defra/interactive-map-plugin-<x>`
> (which resolves `latest`) will fail for the beta packages — consumers must use
> `@beta`. The stable packages (core, `provider-maplibre`, `plugin-interact`,
> `plugin-search`) install normally. If you want a package to start resolving on
> `latest`, remove its `publishConfig.tag`.

**Graduating a package to stable** is a three-place change: remove its
`publishConfig.tag`, remove its directory from `sonar.exclusions` in
`sonar-project.properties`, and remove it from `coveragePathIgnorePatterns` in
`jest.config.mjs` — beta packages are deliberately outside the Sonar quality
gate and coverage collection (carried over from the old `beta/` directory
layout), and a stable package should be inside both.

### 5. Verify

Check the Actions run succeeded, then spot-check npm:

```bash
npm view @defra/interactive-map version dist-tags
npm view @defra/interactive-map-provider-maplibre version dist-tags
npm view @defra/interactive-map-plugin-datasets dist-tags # expect a beta tag
```

---

## Troubleshooting

**A release failed partway through.** The publish loop uses `set -e` and is **not
resumable**: if it dies after publishing some packages, the rest are missing, and
re-running the release fails immediately because the already-published packages
(and core) can't be republished at the same version. Recovery options:

- Publish the missing packages by hand at the same version (requires appropriate
npm auth), **or**
- Cut a new patch release (e.g. `v1.4.1`) so every package re-publishes cleanly
in lockstep. This is usually the simpler path.

**A new package failed to publish with an auth error.** It almost certainly skipped
the [one-time bootstrap](#one-time-bootstrap-new-packages-only) — it was never
seeded, or its trusted publisher isn't configured. Complete both steps, then
re-release.

**Version rejected.** The new version isn't greater than the latest published in
that major line. Bump higher.
4 changes: 2 additions & 2 deletions assets/templates/add-polygons.njk
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{% block head %}
<link href="/public/stylesheets/application.css" rel="stylesheet" type="text/css">
<link href="/plugin-assets/%40defra%2Finteractive-map/dist/css/index.css" rel="stylesheet" type="text/css">
<link href="/plugin-assets/%40defra%2Finteractive-map/plugins/beta/datasets/dist/css/index.css" rel="stylesheet" type="text/css">
<link href="/plugin-assets/%40defra%2Finteractive-map/plugins/datasets/dist/css/index.css" rel="stylesheet" type="text/css">
{% endblock %}

{% block bodyStart %}
Expand Down Expand Up @@ -39,7 +39,7 @@
<!-- Defra library scripts -->
<script src="/plugin-assets/%40defra%2Finteractive-map/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/providers/maplibre/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/plugins/beta/datasets/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/plugins/datasets/dist/umd/index.js"></script>

<script>
// GeoJSON can also be loaded from a URL; use the tiles property for vector tile sources
Expand Down
4 changes: 2 additions & 2 deletions assets/templates/add-symbols.njk
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{% block head %}
<link href="/public/stylesheets/application.css" rel="stylesheet" type="text/css">
<link href="/plugin-assets/%40defra%2Finteractive-map/dist/css/index.css" rel="stylesheet" type="text/css">
<link href="/plugin-assets/%40defra%2Finteractive-map/plugins/beta/datasets/dist/css/index.css" rel="stylesheet" type="text/css">
<link href="/plugin-assets/%40defra%2Finteractive-map/plugins/datasets/dist/css/index.css" rel="stylesheet" type="text/css">
{% endblock %}

{% block bodyStart %}
Expand Down Expand Up @@ -39,7 +39,7 @@
<!-- Defra library scripts -->
<script src="/plugin-assets/%40defra%2Finteractive-map/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/providers/maplibre/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/plugins/beta/datasets/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/plugins/datasets/dist/umd/index.js"></script>

<script>
const symbolGraphic = 'M3 15H1V1h2v2h2V1h2v5h2V4h2v2h2V4h2v11H6V9H3v6z'
Expand Down
2 changes: 1 addition & 1 deletion assets/templates/draw-tools.njk
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<script src="/plugin-assets/%40defra%2Finteractive-map/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/providers/maplibre/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/plugins/interact/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/plugins/beta/draw-ml/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/plugins/draw-ml/dist/umd/index.js"></script>

<script>
const DRAW_LAYERS = ['fill-inactive.cold', 'stroke-inactive.cold']
Expand Down
4 changes: 2 additions & 2 deletions assets/templates/style-switcher.njk
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{% block head %}
<link href="/public/stylesheets/application.css" rel="stylesheet" type="text/css">
<link href="/plugin-assets/%40defra%2Finteractive-map/dist/css/index.css" rel="stylesheet" type="text/css">
<link href="/plugin-assets/%40defra%2Finteractive-map/plugins/beta/map-styles/dist/css/index.css" rel="stylesheet" type="text/css">
<link href="/plugin-assets/%40defra%2Finteractive-map/plugins/map-styles/dist/css/index.css" rel="stylesheet" type="text/css">
{% endblock %}

{% block bodyStart %}
Expand Down Expand Up @@ -39,7 +39,7 @@
<!-- Defra library scripts -->
<script src="/plugin-assets/%40defra%2Finteractive-map/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/providers/maplibre/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/plugins/beta/map-styles/dist/umd/index.js"></script>
<script src="/plugin-assets/%40defra%2Finteractive-map/plugins/map-styles/dist/umd/index.js"></script>

<script>
const attribution = 'OpenFreeMap © OpenMapTiles Data from OpenStreetMap'
Expand Down
2 changes: 1 addition & 1 deletion demo/DemoMapDrawTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function MapInner () {
import('../src/index.js'),
import('../providers/maplibre/src/index.js'),
import('../plugins/interact/src/index.js'),
import('../plugins/beta/draw-ml/src/index.js')
import('../plugins/draw-ml/src/index.js')
]).then(([
{ default: InteractiveMap },
{ default: maplibreProvider },
Expand Down
2 changes: 1 addition & 1 deletion demo/DemoMapPolygons.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function MapInner () {
Promise.all([
import('../src/index.js'),
import('../providers/maplibre/src/index.js'),
import('../plugins/beta/datasets/src/index.js')
import('../plugins/datasets/src/index.js')
]).then(([
{ default: InteractiveMap },
{ default: maplibreProvider },
Expand Down
2 changes: 1 addition & 1 deletion demo/DemoMapStyleSwitcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function MapInner () {
Promise.all([
import('../src/index.js'),
import('../providers/maplibre/src/index.js'),
import('../plugins/beta/map-styles/src/index.js')
import('../plugins/map-styles/src/index.js')
]).then(([
{ default: InteractiveMap },
{ default: maplibreProvider },
Expand Down
2 changes: 1 addition & 1 deletion demo/DemoMapSymbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function MapInner () {
Promise.all([
import('../src/index.js'),
import('../providers/maplibre/src/index.js'),
import('../plugins/beta/datasets/src/index.js')
import('../plugins/datasets/src/index.js')
]).then(([
{ default: InteractiveMap },
{ default: maplibreProvider },
Expand Down
Loading
Loading