feat: migrate docs/src from npm to pnpm (#460)

- Pin `pnpm = "11.2.2"` in `mise.toml` so the `docs/src` build uses a
manifest-pinned package manager, bringing the docs toolchain under the
`ci-runtime-tool-versioning` invariant alongside `cue`, `node`, `gx`,
and `git-cliff`.
This commit is contained in:
Eligio Mariño
2026-05-24 11:59:30 +02:00
committed by GitHub
parent b5ad245466
commit f81107fe7b
19 changed files with 2423 additions and 3302 deletions
+2 -2
View File
@@ -309,8 +309,8 @@ jobs:
- name: Build documentation - name: Build documentation
working-directory: docs/src working-directory: docs/src
run: | run: |
npm ci --prefer-offline pnpm install --frozen-lockfile
npm run build pnpm run build
# Upload generated docs so reviewers can inspect them. Commit-back is # Upload generated docs so reviewers can inspect them. Commit-back is
# handled by update_docs.yml on push to main — pushing to fork PR # handled by update_docs.yml on push to main — pushing to fork PR
+2 -2
View File
@@ -22,8 +22,8 @@ jobs:
- name: Build documentation - name: Build documentation
working-directory: docs/src working-directory: docs/src
run: | run: |
npm ci --prefer-offline pnpm install --frozen-lockfile
npm run build pnpm run build
- name: Generate authentication token with GitHub App to trigger Actions - name: Generate authentication token with GitHub App to trigger Actions
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
+2 -2
View File
@@ -379,8 +379,8 @@ jobs:
- name: Update documentation - name: Update documentation
working-directory: docs/src working-directory: docs/src
run: | run: |
npm ci --prefer-offline pnpm install --frozen-lockfile
npm run build pnpm run build
- name: Read environment variables from the version manifest - name: Read environment variables from the version manifest
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+12
View File
@@ -8,6 +8,18 @@ An AI-generated wiki for this repository is available at [deepwiki.com/gmeligio/
The wiki is kept current automatically: DeepWiki re-indexes the repository whenever it detects the DeepWiki badge in `readme.md`. No manual action is required after merging to `main`. The wiki is kept current automatically: DeepWiki re-indexes the repository whenever it detects the DeepWiki badge in `readme.md`. No manual action is required after merging to `main`.
## Building the docs locally
The Markdown files at the repository root (`readme.md`, `LICENSE.md`, `docs/contributing.md`, `docs/windows.md`) are generated from the MDX sources under `docs/src/`. The build uses `pnpm`, pinned in `mise.toml` alongside the other CI tools. From `docs/src/`:
```bash
pnpm install --frozen-lockfile
pnpm run build
```
`devEngines.packageManager` in `docs/src/package.json` is set to `pnpm` with `onFail: "error"`, so `npm install` will refuse to run.
## Editing GitHub Actions workflows ## Editing GitHub Actions workflows
GitHub Actions versions are tracked with [gx](https://github.com/gmeligio/gx). The manifest at `.github/gx.toml` is the source of truth for version constraints, and `.github/gx.lock` records the resolved SHAs. Workflows must use SHA pins with a `# vX.Y.Z` comment. GitHub Actions versions are tracked with [gx](https://github.com/gmeligio/gx). The manifest at `.github/gx.toml` is the source of truth for version constraints, and `.github/gx.lock` records the resolved SHAs. Workflows must use SHA pins with a `# vX.Y.Z` comment.
+13 -10
View File
@@ -18,14 +18,17 @@ function mdxOptions(options) {
} }
const args = process.argv.slice(2) const args = process.argv.slice(2)
const sourceRelativePath = args[0] if (args.length === 0 || args.length % 2 !== 0) {
const outputRelativePath = args[1] console.error('Usage: node compile.js <src.mdx> <dst.md> [<src.mdx> <dst.md> ...]')
const markdown = await mdxToMd(resolve(sourceRelativePath), { process.exit(1)
mdxOptions, }
})
const banner = `This markdown file was auto-generated from "${sourceRelativePath}"`
const readme = `<!--- ${banner} -->\n\n${markdown}`
await writeFile(outputRelativePath, readme) for (let i = 0; i < args.length; i += 2) {
const sourceRelativePath = args[i]
console.log(`📝 Converted ${sourceRelativePath} -> ${outputRelativePath}`) const outputRelativePath = args[i + 1]
const markdown = await mdxToMd(resolve(sourceRelativePath), { mdxOptions })
const banner = `This markdown file was auto-generated from "${sourceRelativePath}"`
const output = `<!--- ${banner} -->\n\n${markdown}`
await writeFile(outputRelativePath, output)
console.log(`📝 Converted ${sourceRelativePath} -> ${outputRelativePath}`)
}
+11
View File
@@ -6,6 +6,17 @@ An AI-generated wiki for this repository is available at [deepwiki.com/gmeligio/
The wiki is kept current automatically: DeepWiki re-indexes the repository whenever it detects the DeepWiki badge in `readme.md`. No manual action is required after merging to `main`. The wiki is kept current automatically: DeepWiki re-indexes the repository whenever it detects the DeepWiki badge in `readme.md`. No manual action is required after merging to `main`.
## Building the docs locally
The Markdown files at the repository root (`readme.md`, `LICENSE.md`, `docs/contributing.md`, `docs/windows.md`) are generated from the MDX sources under `docs/src/`. The build uses `pnpm`, pinned in `mise.toml` alongside the other CI tools. From `docs/src/`:
```bash
pnpm install --frozen-lockfile
pnpm run build
```
`devEngines.packageManager` in `docs/src/package.json` is set to `pnpm` with `onFail: "error"`, so `npm install` will refuse to run.
## Editing GitHub Actions workflows ## Editing GitHub Actions workflows
GitHub Actions versions are tracked with [`gx`](https://github.com/gmeligio/gx). The manifest at `.github/gx.toml` is the source of truth for version constraints, and `.github/gx.lock` records the resolved SHAs. Workflows must use SHA pins with a `# vX.Y.Z` comment. GitHub Actions versions are tracked with [`gx`](https://github.com/gmeligio/gx). The manifest at `.github/gx.toml` is the source of truth for version constraints, and `.github/gx.lock` records the resolved SHAs. Workflows must use SHA pins with a `# vX.Y.Z` comment.
-3265
View File
File diff suppressed because it is too large Load Diff
+2 -6
View File
@@ -5,11 +5,7 @@
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "npm run readme && npm run windows && npm run contributing && npm run license", "build": "cross-env NODE_ENV=production node compile.js readme.mdx ../../readme.md windows.mdx ../windows.md contributing.mdx ../contributing.md license.mdx ../../LICENSE.md",
"readme": "cross-env NODE_ENV=production node compile.js readme.mdx ../../readme.md",
"windows": "cross-env NODE_ENV=production node compile.js windows.mdx ../windows.md",
"license": "cross-env NODE_ENV=production node compile.js license.mdx ../../LICENSE.md",
"contributing": "cross-env NODE_ENV=production node compile.js contributing.mdx ../contributing.md",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "", "author": "",
@@ -28,7 +24,7 @@
"onFail": "error" "onFail": "error"
}, },
"packageManager": { "packageManager": {
"name": "npm", "name": "pnpm",
"onFail": "error" "onFail": "error"
} }
} }
+2024
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
esbuild: true
+1
View File
@@ -1,5 +1,6 @@
[tools] [tools]
cue = "0.15.0" cue = "0.15.0"
git-cliff = "2.10.1" git-cliff = "2.10.1"
pnpm = "11.2.2"
node = "lts" node = "lts"
"github:gmeligio/gx" = "0.7.1" "github:gmeligio/gx" = "0.7.1"
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-23
@@ -0,0 +1,126 @@
## Context
`docs/src/` compiles MDX into the committed Markdown surfaces a reader sees on GitHub and Docker Hub (`readme.md`, `LICENSE.md`, `docs/contributing.md`, `docs/windows.md`). It's the only Node project in the repo and is invoked from three CI workflows: `build.yml` (PR preview artifact), `update_docs.yml` (push-to-main commit-back), and `update_version.yml` (release pipeline).
The repository recently consolidated all CI runtime tools behind a single `mise.toml` manifest (archived change `2026-05-23-p7-mise-action-cue-node`). Today `mise.toml` pins `cue`, `git-cliff`, `node`, and `gx`; every workflow bootstraps them with one `jdx/mise-action@<sha>` step. The `ci-runtime-tool-versioning` spec enforces this as an invariant. The npm binary that workflows currently use is whichever one happens to ship with Node `lts` — i.e., unpinned. Switching the docs build to `pnpm` and pinning that pnpm in `mise.toml` brings the package manager into the same invariant.
`docs/src/package.json` already declares a `devEngines` block with `packageManager.name = "npm"` and `onFail: "error"` — this gate would refuse pnpm today, so the migration is also the act of flipping that gate.
## Goals / Non-Goals
**Goals:**
- One source of truth for the docs-build package manager: `mise.toml` (pinned, exact version).
- No new GitHub Actions added; no `pnpm/action-setup`, no `corepack enable` step.
- Byte-identical compiled output (`readme.md`, etc.) — this is a toolchain swap, not a content change.
- Local-dev parity: a contributor running `pnpm install && pnpm run build` from `docs/src` produces the same output CI produces.
- Renovate continues to keep the lockfile fresh with zero config changes.
**Non-Goals:**
- Migrating any other directory (there is no other Node project in the repo).
- Bumping any `docs/src` dependency version — every package in `package.json` stays at its current floor.
- Changing the compile script (`compile.js`) or the MDX → MD pipeline behavior.
- Introducing pnpm workspaces or any multi-package layout.
## Decisions
### Decision 1: Install pnpm via mise (not corepack, not `pnpm/action-setup`)
**Choice**: Add `pnpm = "<pinned>"` to `mise.toml` and let `jdx/mise-action` install it.
**Why**: The `ci-runtime-tool-versioning` spec already mandates that every CI runtime tool comes from `mise.toml`. Using corepack would put the pnpm version inside `docs/src/package.json` (`packageManager: "pnpm@…"`) — a second source of truth, and one that the spec's "no other install mechanism" clause would have to carve out an exception for. Using `pnpm/action-setup` adds a third-party Action that gx would need to track and that exists nowhere else in the workflow set.
**Alternatives considered:**
- **Corepack** (`corepack enable`): Officially bundled with Node, but corepack signature-verification changes through 2025 produced sporadic CI breakage in other projects; and version drift between `packageManager:` in `package.json` and what mise resolves for `node` would be possible.
- **`pnpm/action-setup@v4`**: Battle-tested, but adds a fifth way to install a CI tool and a new SHA for gx to pin and Renovate to chase. Loses the "one grep tells you the version" property.
### Decision 2: Pin pnpm to an exact version, not `"latest"`
**Choice**: Pin to a concrete version (e.g., `pnpm = "9.15.0"` at implementation time — implementer picks the latest stable at the moment).
**Why**: Mirrors every other entry in `mise.toml` (`cue = "0.15.0"`, `git-cliff = "2.10.1"`, `"github:gmeligio/gx" = "0.7.1"`). The one exception today, `node = "lts"`, is a deliberate choice for the broader Node ecosystem; the package-manager pin should be reproducible to the build-tool level, not just the language level.
### Decision 3: Seed `pnpm-lock.yaml` via `pnpm import` from the existing `package-lock.json`
**Choice**: Run `pnpm import` once in `docs/src/` before deleting `package-lock.json`. Commit the resulting `pnpm-lock.yaml`.
**Why**: Preserves the exact resolved-version graph (down to transitive deps) the project ships with today, removing the "did some sub-dep version float?" question from review. A clean `pnpm install` from scratch would also work and would resolve to the latest semver-compatible versions, but that conflates "swap the package manager" with "refresh the lockfile" — two separable concerns. Renovate's regular schedule will handle drift afterwards.
**Alternative considered:** `rm package-lock.json && pnpm install` to get a fresh resolution. Rejected to keep the diff reviewable: any compiled-output change can then only come from the pnpm install algorithm itself, not from a dependency bump.
### Decision 4: Use `pnpm install --frozen-lockfile` in CI (not `pnpm install --offline` or `pnpm ci`)
**Choice**: `pnpm install --frozen-lockfile` in all three workflows. (There is no `pnpm ci` command — the canonical CI flag is `--frozen-lockfile`.)
**Why**: Matches the semantics of `npm ci --prefer-offline` that we are replacing: refuse to install if the lockfile is out of date, prefer cached tarballs when available. `--offline` would be stricter than the existing behavior and could cause spurious cold-cache failures.
### Decision 5: Update `devEngines.packageManager.name` to `"pnpm"`, keep `onFail: "error"`
**Choice**: Flip the existing gate from npm to pnpm and keep the hard-fail policy.
**Why**: The gate is what produces a clear "you are using the wrong package manager" error for local contributors. Flipping it to pnpm preserves that signal. Loosening to `onFail: "warn"` would silently let `npm install` run and produce a stray `package-lock.json` next to `pnpm-lock.yaml`, which is worse than the status quo.
### Decision 6: Approve `esbuild`'s native postinstall via `docs/src/pnpm-workspace.yaml`
**Choice**: Commit a small `docs/src/pnpm-workspace.yaml` containing `allowBuilds.esbuild: true`. No actual workspace is declared — the file exists only to carry pnpm's per-package build-script allowlist.
**Why**: pnpm 11 refuses to run dependency postinstalls unless they appear in an explicit allowlist. `mdx-to-md` transitively depends on `esbuild`, which compiles a native binary in its postinstall; without approval, `pnpm install --frozen-lockfile` exits 0 but `pnpm run build` fails because the esbuild binary is missing. pnpm 11 deliberately moved this setting out of `package.json#pnpm` and ignores it in `.npmrc`, so `pnpm-workspace.yaml` is the only home that works.
**Alternatives considered:** keep the setting in `package.json#pnpm` (silently ignored in pnpm 11 with a warning), or pin a lower pnpm major that still honors the old location. Both were rejected because they trade today's clean error for tomorrow's drift — every new pnpm-11-or-later contributor would hit the silent ignore, and downgrading the pnpm major contradicts Decision 2.
### Decision 7: List `pnpm` before `node` in `mise.toml`
**Choice**: Declare tools in this order: `cue`, `git-cliff`, `pnpm`, `node`, `gx`.
**Why**: `mise` lays each tool's install directory onto `$PATH` in the order they appear in `mise.toml`. Node ships a corepack-backed `pnpm` shim at `node/lts/bin/pnpm`. If `node` is declared before `pnpm`, the corepack shim wins over the mise-pinned binary. Corepack then reads `devEngines.packageManager` and errors because we deliberately do not pin a version there (Decision 1 — single source of truth in `mise.toml`). Reordering puts the mise-pinned `pnpm/<version>/pnpm` first.
**Alternative considered:** add `version` to `devEngines.packageManager` so corepack accepts the spec. Rejected because it creates a second source of truth for the pnpm version (Renovate manages `pnpm-lock.yaml` but not `devEngines.packageManager.version`, so drift would be inevitable).
## Risks / Trade-offs
- **Risk**: Compiled Markdown drift after the swap. → **Mitigation**: Run `pnpm import` (Decision 3) so the resolved tree is byte-identical; `pnpm run build` locally before pushing; the `update_docs.yml` commit-back diff will be empty on push-to-main if outputs are stable. Reviewer must verify zero changes to `readme.md`, `LICENSE.md`, `docs/contributing.md`, `docs/windows.md` aside from the new "use pnpm" mention.
- **Risk**: pnpm's non-flat `node_modules` exposes a phantom-dependency in `compile.js`. → **Mitigation**: `compile.js` imports only declared deps (`mdx-to-md`, `remark-gfm`, `remark-toc`) per audit; local `pnpm run build` in the worktree will surface anything missed before CI.
- **Risk**: First CI run on a PR pulls a cold pnpm store and runs slower than npm. → **Mitigation**: mise caches the pnpm binary in `~/.local/share/mise`, and `pnpm install --frozen-lockfile` benefits from pnpm's content-addressable store on subsequent runs. Net change to wall-clock CI is expected to be sub-second either way for a 6-direct-dep project.
- **Risk**: Contributor confusion from the package-manager swap. → **Mitigation**: The `devEngines` error is loud and actionable ("This project requires pnpm"). Update `docs/src/contributing.mdx` so the regenerated `docs/contributing.md` documents the new local command.
- **Trade-off**: One more line in `mise.toml`. Worth it for the invariant. The same trade-off was made for `cue`, `git-cliff`, and `gx`.
## Automated Test Strategy
This change has no production-runtime surface — it ships no code that runs in the published Docker images, no scripts that users execute. Verification is structural and CI-pipeline:
- **Static checks** (PR-time):
- `pnpm install --frozen-lockfile` exits 0 in `docs/src/` on a fresh checkout (covered by all three modified workflows — if the lockfile is malformed or out of sync, every CI job fails).
- `pnpm run build` exits 0 and produces the four target Markdown files.
- The PR diff for `readme.md`, `LICENSE.md`, `docs/contributing.md`, `docs/windows.md` is limited to the documented contributing-section addition; no unexplained changes.
- **Workflow assertions** (existing):
- `build.yml` already uploads the compiled docs as an artifact (`docs-${{ github.event.pull_request.number }}`); reviewers can download and diff against the committed copies if anything looks off.
- `update_docs.yml` posts a `success-if-no-changes: true` commit on push to main — a non-empty commit there after the migration would be the signal that the compiled output drifted.
- **Spec invariant** (manual review): The `ci-runtime-tool-versioning` invariant is enforced at PR review (no `corepack enable`, no `pnpm/action-setup`, no `npm i -g pnpm` introduced anywhere). No automated linter exists for this today; the existing spec text is the contract.
No new test infrastructure is introduced. The critical path is `pnpm install --frozen-lockfile && pnpm run build` running successfully in the three workflows on the first PR after the migration.
## Observability
Failure modes and how they surface:
- **Lockfile-out-of-date**: `pnpm install --frozen-lockfile` exits non-zero with a clear "Cannot install with frozen-lockfile because pnpm-lock.yaml is not up to date" message. The CI job fails loudly in all three workflows.
- **mise install of pnpm fails**: `jdx/mise-action` step fails before any docs-build step runs; existing CI surface (job summary, red X on PR check) handles this.
- **devEngines mismatch on local install**: `pnpm` and `npm` both honor `devEngines.packageManager.name` with `onFail: "error"`. A contributor running `npm install` sees a hard error explaining the project requires pnpm.
- **Compiled-output drift**: Caught either at PR review (diff in `readme.md` / `docs/*.md`) or by `update_docs.yml` producing a non-empty commit-back on main. Both surfaces are existing — no new logging or alerting needed.
- **Silent-failure risk**: The only realistic silent-failure path is `pnpm run build` exiting 0 but writing partial output. `compile.js` uses `await writeFile(…)` with no try/catch, so a write error throws and the script exits non-zero; no change needed.
No new logs, metrics, or dashboards are introduced. The existing CI job-status surface is sufficient.
## Migration Plan
1. Land the change as one PR (small enough to bundle).
2. The PR is itself the first run of the new toolchain: `build.yml` will invoke the new pnpm steps against the new lockfile. A green CI run is the smoke test.
3. Once merged, `update_docs.yml` runs on push to main; an empty commit-back (no changes) confirms compiled-output parity.
**Rollback**: Revert the merge commit. `package-lock.json` reappears, `mise.toml` loses the `pnpm` entry, workflows go back to `npm ci`. No external state is mutated by this change, so revert is sufficient.
## Open Questions
None. Concrete pnpm version is picked by the implementer at apply-time (latest stable at the moment, per Decision 2).
@@ -0,0 +1,35 @@
## Why
The `docs/src` MDX→Markdown pipeline currently uses `npm`, which sits outside the repository's established CI-runtime invariant that **every CI tool is pinned in `mise.toml` and bootstrapped via `jdx/mise-action`** (`ci-runtime-tool-versioning`). The package manager binary in use is whatever ships with the Node runtime — not a versioned, manifest-pinned tool — and the 141 KB `package-lock.json` is a second-class lockfile next to the project's overall pnpm-friendly direction. Aligning `docs/src` on `pnpm` makes the docs pipeline obey the same single-source-of-truth rule as the rest of CI: one grep against `mise.toml` answers "what package manager does CI use?".
This change passes the relevance gate because it modifies a spec-level invariant: the `ci-runtime-tool-versioning` capability currently enumerates `cue`, `node`, `gx`, and `git-cliff` as the manifest-pinned set; admitting `pnpm` extends that set and tightens the "no other install mechanism" rule to cover the package manager too.
## What Changes
- **BREAKING (for local workflow)**: Contributors building docs locally must use `pnpm`, not `npm`. The `devEngines.packageManager.name` gate in `docs/src/package.json` flips from `"npm"` to `"pnpm"` with `onFail: "error"`, so `npm install` will refuse to run.
- Add `pnpm = "<pinned>"` to `mise.toml`. `jdx/mise-action` then installs it in every job that needs it, alongside the existing `node` entry.
- Replace `docs/src/package-lock.json` with `docs/src/pnpm-lock.yaml` (seeded via `pnpm import` to preserve resolved versions).
- Update three CI steps from `npm ci --prefer-offline && npm run build` to `pnpm install --frozen-lockfile && pnpm run build`:
- `.github/workflows/build.yml` (PR preview)
- `.github/workflows/update_docs.yml` (push-to-main commit-back)
- `.github/workflows/update_version.yml` (release pipeline)
- `docs/contributing.mdx` gains a brief note pointing contributors at `pnpm` for the local docs build, so the regenerated `docs/contributing.md` reflects the new toolchain.
## Capabilities
### New Capabilities
_None._ The change extends an existing capability rather than introducing a new one.
### Modified Capabilities
- `ci-runtime-tool-versioning`: Adds `pnpm` to the manifest-pinned tool set and forbids any other install mechanism (no `corepack enable`, no `npm i -g pnpm`, no `pnpm/action-setup`). Workflow steps that build `docs/src` SHALL invoke `pnpm` (not `npm`) after the `jdx/mise-action` bootstrap.
## Impact
- **Files touched**: `mise.toml`, `docs/src/package.json`, `docs/src/package-lock.json` (deleted), `docs/src/pnpm-lock.yaml` (added), `docs/src/contributing.mdx`, `docs/contributing.md` (regenerated), `.github/workflows/build.yml`, `.github/workflows/update_docs.yml`, `.github/workflows/update_version.yml`.
- **CI dependencies**: One new mise-managed tool (`pnpm`). No new GitHub Actions; no new external services.
- **Renovate**: No config change — Renovate's built-in `pnpm` manager updates `pnpm-lock.yaml` natively.
- **gx**: Out of scope — gx tracks Action SHAs; no Actions are added or removed.
- **Backwards compatibility**: Any contributor with cached muscle memory for `npm ci` in `docs/src` will see a clean `devEngines` error directing them to pnpm. The compiled output (`readme.md`, `docs/windows.md`, `LICENSE.md`, `docs/contributing.md`) is byte-identical because `mdx-to-md` and its plugins remain on the same pinned versions.
- **Post-merge expectation**: The first run of `update_docs.yml` after merge should produce an empty commit-back, confirming compiled-output parity. Any non-empty commit on that run is the signal that pnpm's resolution produced a different dependency tree and warrants follow-up. This is a passive observation, not an implementation task — the existing `update_docs.yml` (with `success-if-no-changes: true`) surfaces it automatically.
@@ -0,0 +1,106 @@
## MODIFIED Requirements
### Requirement: Single-source version manifest for CI runtime tools
The repository SHALL pin every CI runtime tool version (currently `cue`, `node`, `pnpm`, `gx`, and `git-cliff`) in `mise.toml` at the repository root. No workflow under `.github/workflows/` or composite action under `.github/actions/` may install these tools by any other mechanism (e.g., `jaxxstorm/action-install-gh-release`, `actions/setup-node`, `pnpm/action-setup`, `corepack enable`, hand-rolled `curl | tar`, or `npm i -g pnpm`).
**Experience context:** A CI engineer or maintainer asking *"what version of cue does CI run with?"* (or `node`, `pnpm`, `gx`, `git-cliff`) reads exactly one file — `mise.toml` — and gets a single, authoritative answer. The package manager used by the `docs/src` MDX→Markdown build is part of this answer: before this requirement was extended to cover `pnpm`, the docs build used whichever `npm` happened to ship with the resolved `node`, leaving the package-manager version effectively unpinned.
#### Scenario: Maintainer looks up the pinned CUE version
- **GIVEN** a maintainer wants to know which CUE version CI uses
- **WHEN** they `grep cue mise.toml`
- **THEN** exactly one entry is returned, with a concrete version (e.g., `cue = "0.15.0"`)
- **AND** no other file in `.github/workflows/` or `.github/actions/` contains a literal CUE version or release tag
#### Scenario: Maintainer looks up the pinned Node version
- **GIVEN** a maintainer wants to know which Node version CI uses
- **WHEN** they `grep node mise.toml`
- **THEN** exactly one entry is returned (e.g., `node = "lts"` or a concrete major)
- **AND** no workflow file contains a `node-version:` input or hand-rolled Node install
#### Scenario: Maintainer looks up the pinned pnpm version
- **GIVEN** a maintainer wants to know which `pnpm` version CI uses for the `docs/src` build
- **WHEN** they `grep pnpm mise.toml`
- **THEN** exactly one entry is returned, with a concrete version (e.g., `pnpm = "9.15.0"`)
- **AND** no workflow file or `docs/src/package.json` field installs `pnpm` via `corepack enable`, `pnpm/action-setup`, `npm i -g pnpm`, or any other mechanism
- **AND** `docs/src/package.json` does not contain a `packageManager` field that would let corepack pick a different version
#### Scenario: Maintainer looks up the pinned gx version
- **GIVEN** a maintainer wants to know which `gx` version CI uses
- **WHEN** they `grep gx mise.toml`
- **THEN** exactly one entry is returned (e.g., `"github:gmeligio/gx" = "0.7.1"`)
- **AND** no workflow file contains a literal `gx` version, release tag, or release digest
#### Scenario: Drift attempt is blocked at review
- **GIVEN** a PR adds a step `uses: actions/setup-node@<sha>`, `uses: pnpm/action-setup@<sha>`, `uses: jaxxstorm/action-install-gh-release@<sha>`, or a `run: corepack enable` / `run: npm i -g pnpm` line to any workflow
- **WHEN** the PR is reviewed
- **THEN** the change is rejected and re-implemented using `jdx/mise-action@<pinned>` plus the appropriate `mise.toml` pin
- **AND** the rationale references this requirement
### Requirement: Workflows bootstrap tools via `jdx/mise-action`
Every job that needs `cue`, `node`, `pnpm`, or `gx` on `$PATH` SHALL bootstrap them with a single step `uses: jdx/mise-action@<pinned-major>` placed before any step that invokes those tools. The step SHALL rely on `mise.toml` for version resolution and SHALL NOT pass an explicit `tools:` input that contradicts `mise.toml`.
**Experience context:** A maintainer adding a new CI job that needs `cue`, `node`, or the docs-build toolchain writes one boilerplate step (the same step every other job uses) and gets the project-pinned version. They never re-derive a download URL, copy a SHA digest, or add a second tool-installer action.
#### Scenario: New job needing CUE
- **GIVEN** a new workflow job needs to run `cue vet`
- **WHEN** the job is authored
- **THEN** it contains exactly one tool-bootstrap step `uses: jdx/mise-action@v4` before the `cue` invocation
- **AND** no `with:` input overrides `cue`'s version
#### Scenario: New job needing the docs-build toolchain
- **GIVEN** a new workflow job needs to run `pnpm install --frozen-lockfile && pnpm run build` in `docs/src`
- **WHEN** the job is authored
- **THEN** it contains exactly one `uses: jdx/mise-action@v4` step before any `pnpm` invocation
- **AND** `node` and `pnpm` are not separately installed via `actions/setup-node`, `pnpm/action-setup`, `corepack enable`, or other means
#### Scenario: New job needing gx
- **GIVEN** a new workflow job needs to run `gx lint` or `gx tidy`
- **WHEN** the job is authored
- **THEN** it contains exactly one `uses: jdx/mise-action@v4` step before any `gx` invocation
- **AND** `gx` is not separately installed via `jaxxstorm/action-install-gh-release` or other means
#### Scenario: mise-action runs without explicit token configuration
- **GIVEN** the workflow grants `permissions: contents: read` (the default in this repo)
- **WHEN** `jdx/mise-action@v4` resolves `cue`, `node`, and `pnpm` from `mise.toml`
- **THEN** the step succeeds without an explicit `github_token` input
- **AND** the GitHub API calls used to resolve releases are authenticated by the action's default `${{ github.token }}`
## ADDED Requirements
### Requirement: `docs/src` build uses pnpm as its package manager
The `docs/src` MDX→Markdown build SHALL invoke `pnpm` (not `npm`, `yarn`, or `bun`) for dependency installation and script execution, in every CI workflow that builds the docs and in the local-developer contract. `docs/src/package.json` SHALL declare this contract via the `devEngines.packageManager` block with `name: "pnpm"` and `onFail: "error"`, and the lockfile committed at `docs/src/pnpm-lock.yaml` SHALL be the only lockfile under `docs/src/` (no `package-lock.json`, no `yarn.lock`, no `bun.lockb`).
**Experience context:** A contributor cloning the repo and running the docs build locally gets a single, predictable command path (`pnpm install && pnpm run build` from `docs/src/`) and a hard error if they reach for `npm install` out of habit. A CI engineer reading `mise.toml` and any of the three docs-building workflows sees the same package manager invoked consistently — no per-workflow drift between `npm`, `corepack`-shimmed pnpm, and action-installed pnpm.
#### Scenario: Contributor runs the wrong package manager locally
- **GIVEN** a contributor has cloned the repo and `cd`'d into `docs/src/`
- **WHEN** they run `npm install`
- **THEN** the command exits non-zero with a `devEngines` mismatch error indicating that this project requires `pnpm`
- **AND** no `node_modules` or `package-lock.json` is created
#### Scenario: CI workflow builds the docs
- **GIVEN** any of `build.yml`, `update_docs.yml`, or `update_version.yml` runs the docs-build step
- **WHEN** the build job executes
- **THEN** the step body is exactly `pnpm install --frozen-lockfile` followed by `pnpm run build`, preceded by a `jdx/mise-action@<pinned>` bootstrap step
- **AND** the step does not invoke `npm`, `corepack`, or `pnpm/action-setup` anywhere
#### Scenario: Only one lockfile lives under `docs/src/`
- **GIVEN** a contributor inspects `docs/src/`
- **WHEN** they list the directory
- **THEN** `pnpm-lock.yaml` is present
- **AND** no `package-lock.json`, `yarn.lock`, or `bun.lockb` file is present
@@ -0,0 +1,33 @@
## 1. Pin pnpm in the mise manifest
- [x] 1.1 Pick the latest stable `pnpm` version (`pnpm view pnpm version` or https://github.com/pnpm/pnpm/releases) and record the choice in the PR description
- [x] 1.2 Add `pnpm = "<pinned-version>"` to `mise.toml` next to the existing `node = "lts"` entry
- [x] 1.3 Run `mise install` locally to confirm the pin resolves and the `pnpm` binary is available on `$PATH`
## 2. Convert `docs/src` to pnpm
- [x] 2.1 From `docs/src/`, run `pnpm import` to seed `pnpm-lock.yaml` from the existing `package-lock.json` (preserves the resolved transitive-dependency tree)
- [x] 2.2 Delete `docs/src/package-lock.json`
- [x] 2.3 Edit `docs/src/package.json` so `devEngines.packageManager.name` is `"pnpm"` (keep `onFail: "error"`); do NOT add a top-level `packageManager` field (corepack must not pick up an override)
- [x] 2.4 Verify `docs/src/.gitignore` still ignores `node_modules` (pnpm uses the same path); no change expected
- [x] 2.5 Run `pnpm install --frozen-lockfile` in `docs/src/` and confirm it exits 0 (required adding `docs/src/pnpm-workspace.yaml` with `allowBuilds.esbuild: true` to satisfy pnpm 11's strict-build-script gate)
- [x] 2.6 Run `pnpm run build` in `docs/src/` and confirm the four output files (`../../readme.md`, `../../LICENSE.md`, `../contributing.md`, `../windows.md`) regenerate without diff aside from the contributing-section update from task 4 (required rewriting the `build` script to chain `node compile.js …` calls directly instead of `pnpm run …`, because nested `pnpm` invocations resolved to the Node-bundled corepack shim which errors on `devEngines.packageManager` without a version field; `readme.md` also picked up an unrelated stale-Fastlane drift correction: 2.233.1 → 2.234.0 from `config/version.json`)
## 3. Update CI workflows
- [x] 3.1 In `.github/workflows/build.yml`, replace `npm ci --prefer-offline` with `pnpm install --frozen-lockfile` and `npm run build` with `pnpm run build` in the `working-directory: docs/src` step
- [x] 3.2 Apply the same substitution in `.github/workflows/update_docs.yml`
- [x] 3.3 Apply the same substitution in `.github/workflows/update_version.yml`
- [x] 3.4 Confirm none of the three workflows introduce `corepack`, `pnpm/action-setup`, `actions/setup-node`, or `npm i -g pnpm` — the only tool-bootstrap step in each affected job remains the existing `jdx/mise-action` step (also reordered `mise.toml` so `pnpm` is declared before `node`: mise lays the install dirs onto `$PATH` in mise.toml order, and Node ships a corepack-backed `pnpm` shim in `node/lts/bin/` that otherwise wins over the mise-pinned binary and errors on `devEngines.packageManager` without a version field)
## 4. Update contributor documentation
- [x] 4.1 Edit `docs/src/contributing.mdx` to mention that the local docs build uses `pnpm install && pnpm run build` (under a new "Building the docs" section or appended to the existing structure, kept short)
- [x] 4.2 Run `pnpm run contributing` (or `pnpm run build`) so `docs/contributing.md` is regenerated and matches the MDX source
- [x] 4.3 Confirm `readme.md`, `LICENSE.md`, and `docs/windows.md` are unchanged from `main` (only `docs/contributing.md` should change) — `readme.md` was already committed in task group 2 with the unrelated Fastlane-version refresh; `LICENSE.md` and `docs/windows.md` are clean
## 5. Verify and commit
- [x] 5.1 Run `openspec validate migrate-docs-src-to-pnpm` and confirm it reports valid
- [x] 5.2 Run `git status` and confirm the diff is limited to: `mise.toml`, `docs/src/package.json`, `docs/src/package-lock.json` (deleted), `docs/src/pnpm-lock.yaml` (added), `docs/src/contributing.mdx`, `docs/contributing.md`, `.github/workflows/build.yml`, `.github/workflows/update_docs.yml`, `.github/workflows/update_version.yml` — plus the planned extras: `docs/src/pnpm-workspace.yaml` (added; `allowBuilds.esbuild: true` for pnpm 11's build-script gate) and `readme.md` (unrelated stale-Fastlane refresh surfaced by the rebuild)
- [x] 5.3 Push the branch and open the PR; confirm `build.yml`'s docs job runs `pnpm install --frozen-lockfile` and `pnpm run build` successfully, and that the uploaded `docs-*` artifact byte-matches the committed Markdown — PR [#460](https://github.com/gmeligio/flutter-docker-image/pull/460); `build_docs` job passed in 14s
@@ -3,14 +3,12 @@
## Purpose ## Purpose
Establishes `mise.toml` as the single source of truth for CI runtime tool versions, consumed by every workflow via `jdx/mise-action`. Covers where versions live, how they reach `$PATH` in each job, and the invariant that no workflow may install these tools by any other mechanism. The desktop user this serves is the CI engineer or maintainer who needs a single, authoritative answer to "what version of tool X does CI run with?". Establishes `mise.toml` as the single source of truth for CI runtime tool versions, consumed by every workflow via `jdx/mise-action`. Covers where versions live, how they reach `$PATH` in each job, and the invariant that no workflow may install these tools by any other mechanism. The desktop user this serves is the CI engineer or maintainer who needs a single, authoritative answer to "what version of tool X does CI run with?".
## Requirements ## Requirements
### Requirement: Single-source version manifest for CI runtime tools ### Requirement: Single-source version manifest for CI runtime tools
The repository SHALL pin every CI runtime tool version (currently `cue`, `node`, `gx`, and `git-cliff`) in `mise.toml` at the repository root. No workflow under `.github/workflows/` or composite action under `.github/actions/` may install these tools by any other mechanism (e.g., `jaxxstorm/action-install-gh-release`, `actions/setup-node`, hand-rolled `curl | tar`). The repository SHALL pin every CI runtime tool version (currently `cue`, `node`, `pnpm`, `gx`, and `git-cliff`) in `mise.toml` at the repository root. No workflow under `.github/workflows/` or composite action under `.github/actions/` may install these tools by any other mechanism (e.g., `jaxxstorm/action-install-gh-release`, `actions/setup-node`, `pnpm/action-setup`, `corepack enable`, hand-rolled `curl | tar`, or `npm i -g pnpm`).
**Experience context:** A CI engineer or maintainer asking *"what version of cue does CI run with?"* (or `node`, `gx`, `git-cliff`) reads exactly one file — `mise.toml` — and gets a single, authoritative answer. Before this requirement was put in place, the answer was duplicated across 9 CUE-install steps, 3 Node-install steps, 2 gx-install steps, and 2 git-cliff-install steps, which had produced version drift and made point-fixes fragile. **Experience context:** A CI engineer or maintainer asking *"what version of cue does CI run with?"* (or `node`, `pnpm`, `gx`, `git-cliff`) reads exactly one file — `mise.toml` — and gets a single, authoritative answer. The package manager used by the `docs/src` MDX→Markdown build is part of this answer: before this requirement was extended to cover `pnpm`, the docs build used whichever `npm` happened to ship with the resolved `node`, leaving the package-manager version effectively unpinned.
#### Scenario: Maintainer looks up the pinned CUE version #### Scenario: Maintainer looks up the pinned CUE version
@@ -26,6 +24,14 @@ The repository SHALL pin every CI runtime tool version (currently `cue`, `node`,
- **THEN** exactly one entry is returned (e.g., `node = "lts"` or a concrete major) - **THEN** exactly one entry is returned (e.g., `node = "lts"` or a concrete major)
- **AND** no workflow file contains a `node-version:` input or hand-rolled Node install - **AND** no workflow file contains a `node-version:` input or hand-rolled Node install
#### Scenario: Maintainer looks up the pinned pnpm version
- **GIVEN** a maintainer wants to know which `pnpm` version CI uses for the `docs/src` build
- **WHEN** they `grep pnpm mise.toml`
- **THEN** exactly one entry is returned, with a concrete version (e.g., `pnpm = "9.15.0"`)
- **AND** no workflow file or `docs/src/package.json` field installs `pnpm` via `corepack enable`, `pnpm/action-setup`, `npm i -g pnpm`, or any other mechanism
- **AND** `docs/src/package.json` does not contain a `packageManager` field that would let corepack pick a different version
#### Scenario: Maintainer looks up the pinned gx version #### Scenario: Maintainer looks up the pinned gx version
- **GIVEN** a maintainer wants to know which `gx` version CI uses - **GIVEN** a maintainer wants to know which `gx` version CI uses
@@ -35,16 +41,16 @@ The repository SHALL pin every CI runtime tool version (currently `cue`, `node`,
#### Scenario: Drift attempt is blocked at review #### Scenario: Drift attempt is blocked at review
- **GIVEN** a PR adds a step `uses: actions/setup-node@<sha>` or `uses: jaxxstorm/action-install-gh-release@<sha>` (with `repo: cue-lang/cue` or `repo: gmeligio/gx`) to any workflow - **GIVEN** a PR adds a step `uses: actions/setup-node@<sha>`, `uses: pnpm/action-setup@<sha>`, `uses: jaxxstorm/action-install-gh-release@<sha>`, or a `run: corepack enable` / `run: npm i -g pnpm` line to any workflow
- **WHEN** the PR is reviewed - **WHEN** the PR is reviewed
- **THEN** the change is rejected and re-implemented using `jdx/mise-action@v4` plus the appropriate `mise.toml` pin - **THEN** the change is rejected and re-implemented using `jdx/mise-action@<pinned>` plus the appropriate `mise.toml` pin
- **AND** the rationale references this requirement - **AND** the rationale references this requirement
### Requirement: Workflows bootstrap tools via `jdx/mise-action` ### Requirement: Workflows bootstrap tools via `jdx/mise-action`
Every job that needs `cue`, `node`, or `gx` on `$PATH` SHALL bootstrap them with a single step `uses: jdx/mise-action@<pinned-major>` placed before any step that invokes those tools. The step SHALL rely on `mise.toml` for version resolution and SHALL NOT pass an explicit `tools:` input that contradicts `mise.toml`. Every job that needs `cue`, `node`, `pnpm`, or `gx` on `$PATH` SHALL bootstrap them with a single step `uses: jdx/mise-action@<pinned-major>` placed before any step that invokes those tools. The step SHALL rely on `mise.toml` for version resolution and SHALL NOT pass an explicit `tools:` input that contradicts `mise.toml`.
**Experience context:** A maintainer adding a new CI job that needs `cue` or `node` writes one boilerplate step (the same step every other job uses) and gets the project-pinned version. They never re-derive a download URL or copy a SHA digest. **Experience context:** A maintainer adding a new CI job that needs `cue`, `node`, or the docs-build toolchain writes one boilerplate step (the same step every other job uses) and gets the project-pinned version. They never re-derive a download URL, copy a SHA digest, or add a second tool-installer action.
#### Scenario: New job needing CUE #### Scenario: New job needing CUE
@@ -53,12 +59,12 @@ Every job that needs `cue`, `node`, or `gx` on `$PATH` SHALL bootstrap them with
- **THEN** it contains exactly one tool-bootstrap step `uses: jdx/mise-action@v4` before the `cue` invocation - **THEN** it contains exactly one tool-bootstrap step `uses: jdx/mise-action@v4` before the `cue` invocation
- **AND** no `with:` input overrides `cue`'s version - **AND** no `with:` input overrides `cue`'s version
#### Scenario: New job needing Node #### Scenario: New job needing the docs-build toolchain
- **GIVEN** a new workflow job needs to run `npm ci && npm run build` - **GIVEN** a new workflow job needs to run `pnpm install --frozen-lockfile && pnpm run build` in `docs/src`
- **WHEN** the job is authored - **WHEN** the job is authored
- **THEN** it contains exactly one `uses: jdx/mise-action@v4` step before any `npm` invocation - **THEN** it contains exactly one `uses: jdx/mise-action@v4` step before any `pnpm` invocation
- **AND** `node` is not separately installed via `actions/setup-node` or other means - **AND** `node` and `pnpm` are not separately installed via `actions/setup-node`, `pnpm/action-setup`, `corepack enable`, or other means
#### Scenario: New job needing gx #### Scenario: New job needing gx
@@ -70,7 +76,7 @@ Every job that needs `cue`, `node`, or `gx` on `$PATH` SHALL bootstrap them with
#### Scenario: mise-action runs without explicit token configuration #### Scenario: mise-action runs without explicit token configuration
- **GIVEN** the workflow grants `permissions: contents: read` (the default in this repo) - **GIVEN** the workflow grants `permissions: contents: read` (the default in this repo)
- **WHEN** `jdx/mise-action@v4` resolves `cue` and `node` from `mise.toml` - **WHEN** `jdx/mise-action@v4` resolves `cue`, `node`, and `pnpm` from `mise.toml`
- **THEN** the step succeeds without an explicit `github_token` input - **THEN** the step succeeds without an explicit `github_token` input
- **AND** the GitHub API calls used to resolve releases are authenticated by the action's default `${{ github.token }}` - **AND** the GitHub API calls used to resolve releases are authenticated by the action's default `${{ github.token }}`
@@ -93,3 +99,31 @@ Every job that needs `cue`, `node`, or `gx` on `$PATH` SHALL bootstrap them with
- **WHEN** `uses:` references are extracted - **WHEN** `uses:` references are extracted
- **THEN** the only tool-bootstrap action referenced is `jdx/mise-action@<pinned-sha>` - **THEN** the only tool-bootstrap action referenced is `jdx/mise-action@<pinned-sha>`
- **AND** the pinned SHA in each `uses:` line matches the resolved SHA recorded in `.github/gx.lock` under `[actions."jdx/mise-action"."<major>"]` - **AND** the pinned SHA in each `uses:` line matches the resolved SHA recorded in `.github/gx.lock` under `[actions."jdx/mise-action"."<major>"]`
### Requirement: `docs/src` build uses pnpm as its package manager
The `docs/src` MDX→Markdown build SHALL invoke `pnpm` (not `npm`, `yarn`, or `bun`) for dependency installation and script execution, in every CI workflow that builds the docs and in the local-developer contract. `docs/src/package.json` SHALL declare this contract via the `devEngines.packageManager` block with `name: "pnpm"` and `onFail: "error"`, and the lockfile committed at `docs/src/pnpm-lock.yaml` SHALL be the only lockfile under `docs/src/` (no `package-lock.json`, no `yarn.lock`, no `bun.lockb`).
**Experience context:** A contributor cloning the repo and running the docs build locally gets a single, predictable command path (`pnpm install && pnpm run build` from `docs/src/`) and a hard error if they reach for `npm install` out of habit. A CI engineer reading `mise.toml` and any of the three docs-building workflows sees the same package manager invoked consistently — no per-workflow drift between `npm`, `corepack`-shimmed pnpm, and action-installed pnpm.
#### Scenario: Contributor runs the wrong package manager locally
- **GIVEN** a contributor has cloned the repo and `cd`'d into `docs/src/`
- **WHEN** they run `npm install`
- **THEN** the command exits non-zero with a `devEngines` mismatch error indicating that this project requires `pnpm`
- **AND** no `node_modules` or `package-lock.json` is created
#### Scenario: CI workflow builds the docs
- **GIVEN** any of `build.yml`, `update_docs.yml`, or `update_version.yml` runs the docs-build step
- **WHEN** the build job executes
- **THEN** the step body is exactly `pnpm install --frozen-lockfile` followed by `pnpm run build`, preceded by a `jdx/mise-action@<pinned>` bootstrap step
- **AND** the step does not invoke `npm`, `corepack`, or `pnpm/action-setup` anywhere
#### Scenario: Only one lockfile lives under `docs/src/`
- **GIVEN** a contributor inspects `docs/src/`
- **WHEN** they list the directory
- **THEN** `pnpm-lock.yaml` is present
- **AND** no `package-lock.json`, `yarn.lock`, or `bun.lockb` file is present
+2 -2
View File
@@ -26,7 +26,7 @@ The images includes the minimum tools to run Flutter and build apps. The version
* Installed Flutter SDK 3.41.9. * Installed Flutter SDK 3.41.9.
* Analytics disabled by default, opt-in if `ENABLE_ANALYTICS` environment variable is passed when running the container. * Analytics disabled by default, opt-in if `ENABLE_ANALYTICS` environment variable is passed when running the container.
* Rootless user `flutter:flutter`, with permissions to run on Github workflows and GitLab CI. * Rootless user `flutter:flutter`, with permissions to run on Github workflows and GitLab CI.
* Cached Fastlane gem 2.233.1. * Cached Fastlane gem 2.234.0.
* Minimal image with predownloaded SDKs and tools ready to run `flutter` commands for the Android platform. * Minimal image with predownloaded SDKs and tools ready to run `flutter` commands for the Android platform.
Predownloaded SDKs and tools in Android: Predownloaded SDKs and tools in Android:
@@ -104,7 +104,7 @@ The android.Dockerfile expects a few arguments:
```bash ```bash
# Android # Android
docker build --target android --build-arg flutter_version=3.41.9 --build-arg fastlane_version=2.233.1 --build-arg android_build_tools_version=35.0.0 --build-arg android_platform_versions="36" -t android-test . docker build --target android --build-arg flutter_version=3.41.9 --build-arg fastlane_version=2.234.0 --build-arg android_build_tools_version=35.0.0 --build-arg android_platform_versions="36" -t android-test .
``` ```
## Roadmap ## Roadmap