From 45a1d701848d60533bf601b011ca5e85fbf1d57b Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 11:56:19 +0200 Subject: [PATCH 01/15] docs: add CI/release design spec and implementation plan Co-Authored-By: Claude Opus 4.8 --- .../2026-06-28-github-actions-ci-release.md | 602 ++++++++++++++++++ ...-06-28-github-actions-ci-release-design.md | 228 +++++++ 2 files changed, 830 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-28-github-actions-ci-release.md create mode 100644 docs/superpowers/specs/2026-06-28-github-actions-ci-release-design.md diff --git a/docs/superpowers/plans/2026-06-28-github-actions-ci-release.md b/docs/superpowers/plans/2026-06-28-github-actions-ci-release.md new file mode 100644 index 0000000..a4bafb1 --- /dev/null +++ b/docs/superpowers/plans/2026-06-28-github-actions-ci-release.md @@ -0,0 +1,602 @@ +# GitHub Actions CI + Release + Security Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add GitHub Actions to lint, type-check, test, build, and release `@ebuildy/docusaurus-plugin-gitlab` to npm (via release-please + OIDC trusted publishing), plus security checks (CodeQL, Dependabot + dependency review, OpenSSF Scorecard). + +**Architecture:** Plan A — two core workflows (`ci.yml`, `release.yml`) plus three security workflows (`codeql.yml`, `scorecard.yml`, `dependabot.yml`). CI runs lint/typecheck once and the full test suite (unit + real Docusaurus e2e) on a Node 18/20/22 matrix. Releases are conventional-commit driven by release-please; publishing uses npm OIDC trusted publishing with provenance (no long-lived token). All third-party actions are pinned to commit SHAs. + +**Tech Stack:** GitHub Actions, release-please (`googleapis/release-please-action@v4`), CodeQL (`github/codeql-action@v3`), OpenSSF Scorecard (`ossf/scorecard-action@v2`), Dependabot, npm 11+ (OIDC), actionlint, pinact. + +**Spec:** `docs/superpowers/specs/2026-06-28-github-actions-ci-release-design.md` + +--- + +## Prerequisites (one-time, mostly outside this repo) + +These are tracked here but are not code tasks. Do them before/around the first release: + +- On npmjs.com, register `ebuildy/docusaurus-plugin-gitlab` + `.github/workflows/release.yml` as a **trusted publisher** for the package. +- First publish bootstrap: package `@ebuildy/docusaurus-plugin-gitlab` is not yet on npm. The first publish may need a manual `npm publish --access public` (NO `--provenance` — provenance requires CI/OIDC) to create the package name, after which OIDC publishing works. Documented in CONTRIBUTING.md (Task 9). +- In repo Settings: enable Dependabot alerts/security updates, secret scanning + push protection. + +## Tooling used for verification + +- **actionlint** validates workflow YAML. Install: `brew install actionlint` (macOS), or `bash <(curl -s https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)` which drops an `./actionlint` binary in the cwd. Run: `actionlint` (auto-discovers `.github/workflows/`). +- **pinact** pins `uses:` tags to SHAs (Task 8). Install: `go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest`, or `brew install suzuki-shunsuke/pinact/pinact`. +- JSON validity: `node -e "JSON.parse(require('node:fs').readFileSync('','utf8')); console.log('ok')"`. + +## File structure + +| File | Responsibility | +|---|---| +| `CLAUDE.md` (modify) | Retire the stale "DO NOT use git" hard rule; document the CI/CD flow | +| `.claude/.../memory/never-use-git.md` (delete) + `MEMORY.md` (modify) | Remove the stale no-git memory | +| `.github/workflows/ci.yml` (create) | lint + typecheck job; test/build matrix job; dependency-review job | +| `.github/workflows/release.yml` (create) | release-please job + OIDC publish job | +| `.github/workflows/codeql.yml` (create) | CodeQL JS/TS SAST | +| `.github/workflows/scorecard.yml` (create) | OpenSSF Scorecard | +| `.github/dependabot.yml` (create) | npm + github-actions weekly updates | +| `release-please-config.json` (create) | release-please single-package config | +| `.release-please-manifest.json` (create) | version manifest seeded at 0.1.0 | +| `package.json` (modify) | add `publishConfig.access = public` | +| `CONTRIBUTING.md` (modify) | document the release process + bootstrap | + +--- + +## Task 1: Retire the stale no-git rule + +This authorizes git usage for the rest of execution. Do it first. + +**Files:** +- Modify: `CLAUDE.md` (the "Hard rules" list) +- Delete: `/Users/tdecaux/.claude/projects/-Users-tdecaux-dev-ebuildy-mdx-gitlab-set/memory/never-use-git.md` +- Modify: `/Users/tdecaux/.claude/projects/-Users-tdecaux-dev-ebuildy-mdx-gitlab-set/memory/MEMORY.md` + +- [ ] **Step 1: Edit `CLAUDE.md` to remove the no-git rule** + +Replace this bullet in the "Hard rules" section: + +```markdown +- **DO NOT use git.** Never run any git command (no `init`, `status`, `add`, + `commit`, anything). This repo is intentionally not a git repository. +``` + +with: + +```markdown +- **Git/GitHub:** This is a live GitHub repo (`ebuildy/docusaurus-plugin-gitlab`). + Normal git usage is fine. CI runs on PRs and pushes to `main` via GitHub + Actions (`.github/workflows/`); releases are automated with release-please and + published to npm via OIDC trusted publishing — see CONTRIBUTING.md. +``` + +- [ ] **Step 2: Delete the stale memory file** + +```bash +rm "/Users/tdecaux/.claude/projects/-Users-tdecaux-dev-ebuildy-mdx-gitlab-set/memory/never-use-git.md" +``` + +- [ ] **Step 3: Remove its index line from `MEMORY.md`** + +Delete this line from `MEMORY.md`: + +```markdown +- [Never use git](never-use-git.md) — no git commands in this project, ever +``` + +- [ ] **Step 4: Commit** + +```bash +git add CLAUDE.md +git commit -m "docs: retire stale no-git rule now that repo is on GitHub" +``` + +(The memory files live outside the repo and are not committed.) + +--- + +## Task 2: CI workflow (`ci.yml`) + +**Files:** +- Create: `.github/workflows/ci.yml` + +- [ ] **Step 1: Create the workflow file** + +```yaml +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: {} + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run lint + - run: npm run typecheck + + test: + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + node: [18, 20, 22] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: npm + cache-dependency-path: | + package-lock.json + examples/site/package-lock.json + - run: npm ci + - name: Build package (needed by the example site file: link) + run: npm run build + - name: Install example site deps + working-directory: examples/site + run: npm ci + - name: Run tests (unit + e2e docusaurus build) + run: npm test + + dependency-review: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high +``` + +- [ ] **Step 2: Validate the workflow with actionlint** + +Run: `actionlint .github/workflows/ci.yml` +Expected: no output (exit 0). Any error message means fix the YAML. + +- [ ] **Step 3: Confirm the referenced npm scripts succeed locally** + +Run: `npm run lint && npm run typecheck && npm run build` +Expected: all exit 0; `dist/` is (re)generated. + +(`npm test` runs the slow ~1min e2e — optional to run locally here; CI will run it. To run it locally: `npm run build && (cd examples/site && npm ci) && npm test`.) + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/ci.yml +git commit -m "ci: add lint, typecheck, test matrix, and dependency review" +``` + +--- + +## Task 3: release-please config files + +**Files:** +- Create: `release-please-config.json` +- Create: `.release-please-manifest.json` + +- [ ] **Step 1: Create `release-please-config.json`** + +```json +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "node", + "package-name": "@ebuildy/docusaurus-plugin-gitlab" + } + } +} +``` + +- [ ] **Step 2: Create `.release-please-manifest.json` seeded at the current version** + +```json +{ + ".": "0.1.0" +} +``` + +- [ ] **Step 3: Validate both files parse as JSON** + +Run: +```bash +node -e "JSON.parse(require('node:fs').readFileSync('release-please-config.json','utf8')); JSON.parse(require('node:fs').readFileSync('.release-please-manifest.json','utf8')); console.log('ok')" +``` +Expected: `ok` + +- [ ] **Step 4: Commit** + +```bash +git add release-please-config.json .release-please-manifest.json +git commit -m "build: add release-please config and manifest" +``` + +--- + +## Task 4: `publishConfig` for public scoped publishing + +**Files:** +- Modify: `package.json` + +- [ ] **Step 1: Add `publishConfig` to `package.json`** + +Insert this block immediately after the `"license": "MIT",` line: + +```json + "publishConfig": { + "access": "public" + }, +``` + +(Scoped packages default to restricted; this makes both CI and any manual bootstrap publish public. Provenance is passed as a CI-only flag, not here, so a local bootstrap publish without OIDC still works.) + +- [ ] **Step 2: Verify the package tarball contains only `dist/`** + +Run: `npm pack --dry-run` +Expected: the file list shows `dist/...` entries and package metadata, and does NOT include `src/`, `test/`, or `examples/`. + +- [ ] **Step 3: Commit** + +```bash +git add package.json +git commit -m "build: publish scoped package with public access" +``` + +--- + +## Task 5: Release workflow (`release.yml`) + +**Files:** +- Create: `.github/workflows/release.yml` + +- [ ] **Step 1: Create the workflow file** + +```yaml +name: Release + +on: + push: + branches: [main] + +permissions: {} + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release-please: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - id: release + uses: googleapis/release-please-action@v4 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + publish: + needs: release-please + if: needs.release-please.outputs.release_created == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org + cache: npm + - name: Use OIDC-capable npm + run: npm install -g npm@latest + - run: npm ci + - run: npm run build + - run: npm publish --provenance --access public +``` + +- [ ] **Step 2: Validate with actionlint** + +Run: `actionlint .github/workflows/release.yml` +Expected: no output (exit 0). + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/release.yml +git commit -m "ci: add release-please + OIDC npm publish workflow" +``` + +--- + +## Task 6: CodeQL workflow (`codeql.yml`) + +**Files:** +- Create: `.github/workflows/codeql.yml` + +- [ ] **Step 1: Create the workflow file** + +```yaml +name: CodeQL + +on: + pull_request: + branches: [main] + push: + branches: [main] + schedule: + - cron: "27 3 * * 1" + +permissions: {} + +jobs: + analyze: + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + actions: read + steps: + - uses: actions/checkout@v4 + - uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + - uses: github/codeql-action/analyze@v3 + with: + category: "/language:javascript-typescript" +``` + +- [ ] **Step 2: Validate with actionlint** + +Run: `actionlint .github/workflows/codeql.yml` +Expected: no output (exit 0). + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/codeql.yml +git commit -m "ci: add CodeQL JS/TS static analysis" +``` + +--- + +## Task 7: Dependabot + Scorecard + +**Files:** +- Create: `.github/dependabot.yml` +- Create: `.github/workflows/scorecard.yml` + +- [ ] **Step 1: Create `.github/dependabot.yml`** + +```yaml +version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 5 +``` + +- [ ] **Step 2: Create `.github/workflows/scorecard.yml`** + +```yaml +name: Scorecard + +on: + push: + branches: [main] + schedule: + - cron: "18 4 * * 1" + +permissions: {} + +jobs: + analysis: + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: ossf/scorecard-action@v2 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif +``` + +- [ ] **Step 3: Validate the scorecard workflow with actionlint** + +Run: `actionlint .github/workflows/scorecard.yml` +Expected: no output (exit 0). + +(`dependabot.yml` is not a workflow; actionlint does not cover it. GitHub validates it on push and surfaces errors in the repo's Insights → Dependency graph → Dependabot tab. Visually confirm it matches the block above.) + +- [ ] **Step 4: Commit** + +```bash +git add .github/dependabot.yml .github/workflows/scorecard.yml +git commit -m "ci: add Dependabot and OpenSSF Scorecard" +``` + +--- + +## Task 8: Pin all third-party actions to commit SHAs + +Hardening per the spec. Tags above are working/testable; this converts them to SHAs. Dependabot's `github-actions` ecosystem (Task 7) keeps them current afterward. + +**Files:** +- Modify: every file under `.github/workflows/` + +- [ ] **Step 1: Install pinact** + +Run (pick one): +```bash +brew install suzuki-shunsuke/pinact/pinact +# or: +go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest +``` +Expected: `pinact --version` prints a version. + +- [ ] **Step 2: Provide a GitHub token so pinact can resolve tags** + +Run: `export GITHUB_TOKEN="$(gh auth token)"` +Expected: no error (requires `gh auth login` already done). + +- [ ] **Step 3: Pin tags to SHAs** + +Run: `pinact run` +Expected: `pinact` rewrites each `uses: owner/repo@vTAG` to `uses: owner/repo@<40-char-sha> # vTAG` across all workflow files. `git diff .github/workflows` shows only `uses:` lines changing. + +Manual fallback if pinact is unavailable — for each `uses: OWNER/REPO@TAG`, resolve and replace by hand: +```bash +gh api repos/OWNER/REPO/commits/TAG --jq '.sha' +``` +Then edit the line to `uses: OWNER/REPO@ # TAG`. + +- [ ] **Step 4: Re-validate all workflows** + +Run: `actionlint` +Expected: no output (exit 0). + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows +git commit -m "ci: pin third-party actions to commit SHAs" +``` + +--- + +## Task 9: Document the release process + +**Files:** +- Modify: `CONTRIBUTING.md` + +- [ ] **Step 1: Append a "Releasing" section to `CONTRIBUTING.md`** + +Add this to the end of the file: + +```markdown +## Releasing + +Releases are automated with [release-please](https://github.com/googleapis/release-please) +and published to npm with provenance via OIDC trusted publishing. + +1. Land changes on `main` using **conventional commits** (`feat:`, `fix:`, + `chore:`, …). `feat:` bumps the minor version, `fix:` the patch version. +2. release-please opens/maintains a **release PR** (`chore: release x.y.z`) that + bumps `package.json` + `.release-please-manifest.json` and updates the + changelog. +3. Merge the release PR. The `Release` workflow tags the release and the + `publish` job runs `npm publish --provenance --access public`. No npm token is + stored — publishing authenticates via GitHub OIDC. + +### One-time setup + +- On npmjs.com, add `ebuildy/docusaurus-plugin-gitlab` + `.github/workflows/release.yml` + as a **trusted publisher** for `@ebuildy/docusaurus-plugin-gitlab`. +- **First publish only:** because the package name does not yet exist on npm, the + very first publish may need to be bootstrapped manually from a clean checkout: + `npm ci && npm run build && npm publish --access public` (without + `--provenance`, which requires CI/OIDC). Subsequent releases publish + automatically. +``` + +- [ ] **Step 2: Lint the docs** + +Run: `npm run lint:md` +Expected: exit 0 (no markdownlint errors). If it reports issues, run `npm run lint:md -- --fix` and re-check. + +- [ ] **Step 3: Commit** + +```bash +git add CONTRIBUTING.md +git commit -m "docs: document the automated release process" +``` + +--- + +## Task 10: Open a PR and verify the pipeline end-to-end + +**Files:** none (verification only) + +- [ ] **Step 1: Push the branch and open a PR** + +```bash +git push -u origin HEAD +gh pr create --fill +``` + +- [ ] **Step 2: Watch the CI checks** + +Run: `gh pr checks --watch` +Expected: `lint`, `test (18)`, `test (20)`, `test (22)`, `dependency-review`, and `CodeQL` all pass. (The `test` jobs include the ~1min e2e Docusaurus build.) + +- [ ] **Step 3: Confirm CodeQL produced results** + +Check the PR's checks / the repo Security tab → Code scanning. Expected: a CodeQL run completed (0 or more alerts). + +- [ ] **Step 4: Merge and confirm release-please opens a release PR** + +After merging this PR to `main`, run: `gh pr list` +Expected: a `chore: release ...` PR opened by release-please appears (driven by the conventional commits on `main`). + +- [ ] **Step 5 (gated on one-time npm setup): cut the first release** + +Complete the npm trusted-publisher setup + first-publish bootstrap (see Prerequisites / CONTRIBUTING.md), then merge the release PR. Confirm with: `npm view @ebuildy/docusaurus-plugin-gitlab version` +Expected: the published version matches the release tag, and the npm page shows a provenance attestation. + +--- + +## Notes for the executor + +- Workflow YAML and config files are not unit-testable; their "tests" are + `actionlint` (syntax/contract) plus a real PR run (Task 10). Treat a clean + `actionlint` + green PR checks as the pass criteria. +- Keep commits conventional — release-please derives the next version from them. +- If `npm test` fails in CI only on the e2e step, confirm the `npm run build` + (root) and `examples/site` `npm ci` steps ran before it; the e2e spawns a real + `docusaurus build` that loads the freshly built `dist/` via the `file:../..` + link. diff --git a/docs/superpowers/specs/2026-06-28-github-actions-ci-release-design.md b/docs/superpowers/specs/2026-06-28-github-actions-ci-release-design.md new file mode 100644 index 0000000..74a0993 --- /dev/null +++ b/docs/superpowers/specs/2026-06-28-github-actions-ci-release-design.md @@ -0,0 +1,228 @@ +# GitHub Actions: CI + Release — Design + +**Date:** 2026-06-28 +**Package:** `@ebuildy/docusaurus-plugin-gitlab` +**Repo:** `github.com/ebuildy/docusaurus-plugin-gitlab` + +## Goal + +Add GitHub Actions to lint, type-check, test, build, and release the package. +Releases are automated with **release-please** and published to npm via **OIDC +trusted publishing** (no long-lived token). + +## Decisions (locked) + +| Question | Decision | +|---|---| +| Release model | release-please (conventional-commit driven release PR) | +| npm auth | Trusted publishing (OIDC), provenance enabled | +| Node test matrix | 18, 20, 22 | +| e2e in CI | Yes — runs on every PR/push, all matrix versions | +| Workflow structure | Plan A: two workflows (`ci.yml` + `release.yml`) | +| Security checks | CodeQL, Dependabot + dependency review, OpenSSF Scorecard (no standalone `npm audit` gate) | +| CI hardening | Least-privilege `permissions`, SHA-pinned third-party actions, npm provenance | + +## Repo facts that shape the design + +- ESM-first dual build via tsup → `dist/` (ESM + CJS + d.ts). `npm run build`. +- Scripts: `build` (tsup), `test` (`vitest run`), `typecheck` (`tsc --noEmit`), + `lint` (`eslint . && markdownlint-cli2`). +- Vitest `include` is `src/**/*.test.{ts,tsx}` and `test/**/*.test.{ts,tsx}` — + so `npm test` **already includes** the e2e suite (`test/e2e/build.test.ts`). +- The e2e test spawns `npm run build` inside `examples/site` (a real Docusaurus + 3 build) against a local GitLab stub. It therefore requires: + 1. the package built (`dist/`) — the site links it via `file:../..`; + 2. the example site's own deps installed (`examples/site` has its own + `package-lock.json`). +- Scoped package (`@ebuildy/...`) → publishing needs `--access public`. +- Node 20 local; CLAUDE.md states Node 18+ support. +- Existing local quality gates: husky `pre-commit` → `lint-staged` + (eslint --fix, markdownlint --fix). CI re-checks lint without `--fix`. + +## Architecture + +Files under `.github/`: + +- `.github/workflows/ci.yml` — lint, typecheck, test/build matrix, dependency review. +- `.github/workflows/release.yml` — release-please + OIDC publish. +- `.github/workflows/codeql.yml` — SAST. +- `.github/workflows/scorecard.yml` — OpenSSF Scorecard. +- `.github/dependabot.yml` — npm + github-actions update schedule. + +The two core CI/CD workflows (`ci.yml`, `release.yml`) are detailed first; the +security workflows are specified in the **Security** section below. + +### `ci.yml` + +```text +on: pull_request, push (branches: [main]) +concurrency: group per ref, cancel-in-progress: true + +jobs: + lint: # runtime-independent checks, Node 20 only + - actions/checkout + - actions/setup-node (node 20, cache: npm) + - npm ci + - npm run lint # eslint . && markdownlint-cli2 + - npm run typecheck # tsc --noEmit + + test: # matrix: node 18, 20, 22 + strategy.matrix.node: [18, 20, 22] + - actions/checkout + - actions/setup-node (matrix node, cache: npm, + cache-dependency-path: [package-lock.json, examples/site/package-lock.json]) + - npm ci # root deps + - npm run build # produce dist/ for the file: link + - npm ci --prefix examples/site # example site deps (Docusaurus) + - npm test # vitest: unit + e2e (real docusaurus build) +``` + +Notes: +- Build is verified implicitly: `test` cannot pass without a successful + `npm run build`, so no standalone build job is needed. +- e2e runs on all three Node versions (heaviest part, ~3–4 min total); accepted + for maximum confidence per decision. + +### `release.yml` + +```text +on: push (branches: [main]) +concurrency: group "release", cancel-in-progress: false + +jobs: + release-please: + permissions: { contents: write, pull-requests: write } + - googleapis/release-please-action (release-type: node) + outputs: release_created, tag_name + + publish: + needs: release-please + if: needs.release-please.outputs.release_created == 'true' + permissions: { id-token: write, contents: read } + - actions/checkout + - actions/setup-node (node 20, registry-url: https://registry.npmjs.org) + - npm install -g npm@latest # ensure OIDC-capable npm (>= 11.5) + - npm ci + - npm run build + - npm publish --provenance --access public +``` + +Notes: +- `release-please` maintains a "release PR" (version bump + CHANGELOG) from + conventional commits. Merging that PR sets `release_created=true`, tags, and + triggers `publish`. +- `publish` uses OIDC trusted publishing: no `NODE_AUTH_TOKEN` / `NPM_TOKEN`. + `id-token: write` is required for both OIDC auth and provenance. + +### release-please config files + +- `release-please-config.json` — single package at repo root, `release-type: + node`, `package-name: @ebuildy/docusaurus-plugin-gitlab`. +- `.release-please-manifest.json` — seeded to current version `"." : "0.1.0"`. + +## Security + +### Baseline hardening (applied to all workflows) + +- **Least privilege**: each workflow declares a top-level `permissions: {}` (deny + all by default) and grants the minimum tokens per job (e.g. `contents: read`, + and `security-events: write` only where SARIF is uploaded). +- **SHA-pinned actions**: all third-party actions (release-please, checkout, + setup-node, codeql, scorecard, dependency-review) are pinned to a full commit + SHA, not a floating tag, to defend against tag-retargeting / action compromise. + Dependabot (below) keeps these SHAs updated. +- **npm provenance**: publish runs `--provenance` under OIDC (see release.yml). + +### `codeql.yml` — static analysis (SAST) + +```text +on: pull_request, push (branches: [main]), schedule (weekly cron) +permissions: { security-events: write, contents: read, actions: read } + +jobs: + analyze: + - github/codeql-action/init (languages: javascript-typescript) + - github/codeql-action/analyze +``` + +Relevant because the package renders sanitized HTML via +`dangerouslySetInnerHTML`; CodeQL's JS/TS queries flag injection-style patterns. +Findings surface in the Security tab and as PR checks. + +### `dependabot.yml` + dependency review + +- `.github/dependabot.yml` — two ecosystems on a weekly schedule: + - `npm` (root `package.json`; `examples/site` is a test fixture — include if + desired, otherwise root only), + - `github-actions` (keeps the SHA-pinned action versions patched). +- **Dependency review** step in `ci.yml` on `pull_request` + (`actions/dependency-review-action`): blocks PRs that introduce dependencies + with known high/critical CVEs or disallowed licenses. + +### `scorecard.yml` — OpenSSF Scorecard + +```text +on: push (branches: [main]), schedule (weekly cron) +permissions: { security-events: write, id-token: write, contents: read } + +jobs: + analysis: + - ossf/scorecard-action (results_format: sarif, publish_results: true) + - github/codeql-action/upload-sarif (results.sarif) +``` + +Posture report (branch protection, pinned deps, token perms, etc.), published to +the Security tab; a README badge is optional. + +### Repo settings to enable (manual, outside code) + +- Secret scanning + push protection (GitHub native, free for public repos). +- Dependabot alerts/security updates enabled in repo settings. + +## One-time manual setup (outside this codebase) + +1. **npmjs.com trusted publisher**: register the repo + `ebuildy/docusaurus-plugin-gitlab` + workflow `release.yml` as a trusted + publisher for the package. +2. **First publish bootstrap** ⚠️: the package is not yet published (v0.1.0). + Trusted publishing for a brand-new package name may require a one-time manual + `npm publish --access public` to create the package, after which OIDC + publishing works for subsequent releases. Document this in the README / + CONTRIBUTING release notes. + +## Stale-instruction cleanup (in scope) + +`CLAUDE.md` currently states "DO NOT use git / this repo is intentionally not a +git repository," and the auto-memory `never-use-git` mirrors it. The repo is now +a live GitHub repository with CI/CD. As part of this work: + +- Update `CLAUDE.md` to remove the no-git hard rule and reflect the GitHub + Actions workflow (CI on PRs, release-please + OIDC publish). +- Update/remove the `never-use-git` memory accordingly. + +(During the brainstorming session itself, no git commands were run, honoring the +rule until it is formally retired.) + +## Optional / nice-to-have (decide at planning time) + +- `actionlint` to validate workflow YAML locally or as a small CI step. +- Dependabot for GitHub Actions version bumps. + +## Testing strategy for this change + +Workflows are validated by execution: +1. Open a PR with the new workflows → confirm `lint` + `test` (3 Node versions, + including e2e) pass. +2. Land a conventional commit on `main` → confirm release-please opens a release + PR. +3. Merge the release PR → confirm `publish` runs and the package appears on npm + with provenance. + +Static check before pushing: `actionlint` (optional) and a manual review of the +two workflow files. + +## Out of scope + +- Multi-package / monorepo release flows (single package). +- Publishing the example site (it is a test fixture only). +- Coverage upload / external reporting services. From 6d420f09b1485f0baecd2663e39655e10f92de12 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 11:57:26 +0200 Subject: [PATCH 02/15] docs: retire stale no-git rule now that repo is on GitHub Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bcc4048..bde3636 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,8 +13,10 @@ static HTML. The browser never holds a token or calls the GitLab API. ## Hard rules -- **DO NOT use git.** Never run any git command (no `init`, `status`, `add`, - `commit`, anything). This repo is intentionally not a git repository. +- **Git/GitHub:** This is a live GitHub repo (`ebuildy/docusaurus-plugin-gitlab`). + Normal git usage is fine. CI runs on PRs and pushes to `main` via GitHub + Actions (`.github/workflows/`); releases are automated with release-please and + published to npm via OIDC trusted publishing — see CONTRIBUTING.md. - **Docusaurus 3 only** (MDX v3, unified ESM, Node 18+ `fetch`). - Prefer the latest versions of libraries. - ESM-first. Intra-package imports use explicit `.js` extensions From ebe0c2b1c1397ac90abbf20687f58b93104a04f1 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 12:27:21 +0200 Subject: [PATCH 03/15] ci: add lint, typecheck, test matrix, and dependency review Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..13e72fb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: {} + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run lint + - run: npm run typecheck + + test: + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + node: [18, 20, 22] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: npm + cache-dependency-path: | + package-lock.json + examples/site/package-lock.json + - run: npm ci + - name: "Build package (needed by the example site file: link)" + run: npm run build + - name: Install example site deps + working-directory: examples/site + run: npm ci + - name: Run tests (unit + e2e docusaurus build) + run: npm test + + dependency-review: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high From 6b995473d91ed1841b3e0228f2728a4d87fbe024 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 12:29:33 +0200 Subject: [PATCH 04/15] build: add release-please config and manifest Co-Authored-By: Claude Opus 4.8 --- .release-please-manifest.json | 3 +++ release-please-config.json | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..466df71 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..e497d5b --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "node", + "package-name": "@ebuildy/docusaurus-plugin-gitlab" + } + } +} From e3cee3a893399a23383a8c8a1e7eace058f61cd7 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 12:30:37 +0200 Subject: [PATCH 05/15] build: publish scoped package with public access Co-Authored-By: Claude Opus 4.8 --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 2082687..eac28b6 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.1.0", "description": "MDX extensions to embed GitLab resources in Docusaurus 3 docs", "license": "MIT", + "publishConfig": { + "access": "public" + }, "type": "module", "exports": { ".": { From 2e2e33f4087d681df54148012d67f2eed0889b00 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 12:31:28 +0200 Subject: [PATCH 06/15] ci: add release-please + OIDC npm publish workflow Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b4b5ad1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +name: Release + +on: + push: + branches: [main] + +permissions: {} + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release-please: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - id: release + uses: googleapis/release-please-action@v4 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + publish: + needs: release-please + if: needs.release-please.outputs.release_created == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org + cache: npm + - name: Use OIDC-capable npm + run: npm install -g npm@latest + - run: npm ci + - run: npm run build + - run: npm publish --provenance --access public From 6373486ca36d01084e3e67e6d4623d97c85dd2c9 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 12:32:15 +0200 Subject: [PATCH 07/15] ci: add CodeQL JS/TS static analysis Co-Authored-By: Claude Opus 4.8 --- .github/workflows/codeql.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..6e5127a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,27 @@ +name: CodeQL + +on: + pull_request: + branches: [main] + push: + branches: [main] + schedule: + - cron: "27 3 * * 1" + +permissions: {} + +jobs: + analyze: + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + actions: read + steps: + - uses: actions/checkout@v4 + - uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + - uses: github/codeql-action/analyze@v3 + with: + category: "/language:javascript-typescript" From de6286f9d3475b2a865c51a86d21e75ed9ef4b7e Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 12:33:04 +0200 Subject: [PATCH 08/15] ci: add Dependabot and OpenSSF Scorecard Co-Authored-By: Claude Opus 4.8 --- .github/dependabot.yml | 12 ++++++++++++ .github/workflows/scorecard.yml | 29 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/scorecard.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0e0cacc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..397acf8 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,29 @@ +name: Scorecard + +on: + push: + branches: [main] + schedule: + - cron: "18 4 * * 1" + +permissions: {} + +jobs: + analysis: + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: ossf/scorecard-action@v2 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif From db1d349d6b282704cb5243324f600fa5bce30786 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 12:53:41 +0200 Subject: [PATCH 09/15] docs: fix unquoted colon in ci.yml plan snippet Co-Authored-By: Claude Opus 4.8 --- docs/superpowers/plans/2026-06-28-github-actions-ci-release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-06-28-github-actions-ci-release.md b/docs/superpowers/plans/2026-06-28-github-actions-ci-release.md index a4bafb1..ed4461d 100644 --- a/docs/superpowers/plans/2026-06-28-github-actions-ci-release.md +++ b/docs/superpowers/plans/2026-06-28-github-actions-ci-release.md @@ -150,7 +150,7 @@ jobs: package-lock.json examples/site/package-lock.json - run: npm ci - - name: Build package (needed by the example site file: link) + - name: "Build package (needed by the example site file: link)" run: npm run build - name: Install example site deps working-directory: examples/site From c65c603a512e7f57adc8ebb8a097d09927f4da85 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 13:19:31 +0200 Subject: [PATCH 10/15] ci: pin third-party actions to commit SHAs Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/codeql.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- .github/workflows/scorecard.yml | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13e72fb..fc1b5c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,8 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 cache: npm @@ -35,8 +35,8 @@ jobs: matrix: node: [18, 20, 22] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node }} cache: npm @@ -58,7 +58,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 - - uses: actions/dependency-review-action@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 with: fail-on-severity: high diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6e5127a..6377007 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -18,10 +18,10 @@ jobs: contents: read actions: read steps: - - uses: actions/checkout@v4 - - uses: github/codeql-action/init@v3 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: github/codeql-action/init@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2 with: languages: javascript-typescript - - uses: github/codeql-action/analyze@v3 + - uses: github/codeql-action/analyze@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2 with: category: "/language:javascript-typescript" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b4b5ad1..0c43da0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: tag_name: ${{ steps.release.outputs.tag_name }} steps: - id: release - uses: googleapis/release-please-action@v4 + uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 with: config-file: release-please-config.json manifest-file: .release-please-manifest.json @@ -34,8 +34,8 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 registry-url: https://registry.npmjs.org diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 397acf8..5b54f7f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -16,14 +16,14 @@ jobs: id-token: write contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: persist-credentials: false - - uses: ossf/scorecard-action@v2 + - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif publish_results: true - - uses: github/codeql-action/upload-sarif@v3 + - uses: github/codeql-action/upload-sarif@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2 with: sarif_file: results.sarif From 9f907c32b4c7db9037eade71d091cc6d18ea3c51 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 13:24:01 +0200 Subject: [PATCH 11/15] docs: document the automated release process Co-Authored-By: Claude Opus 4.8 --- CONTRIBUTING.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 606c741..3927992 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,6 +61,31 @@ npm run lint && npm run typecheck && npm test Keep changes focused, and add/adjust tests for any behavior you change. +## Releasing + +Releases are automated with [release-please](https://github.com/googleapis/release-please) +and published to npm with provenance via OIDC trusted publishing. + +1. Land changes on `main` using **conventional commits** (`feat:`, `fix:`, + `chore:`, …). `feat:` bumps the minor version, `fix:` the patch version. +2. release-please opens/maintains a **release PR** (`chore: release x.y.z`) that + bumps `package.json` + `.release-please-manifest.json` and updates the + changelog. +3. Merge the release PR. The `Release` workflow tags the release and the + `publish` job runs `npm publish --provenance --access public`. No npm token is + stored — publishing authenticates via GitHub OIDC. + +### One-time setup + +- On npmjs.com, add `ebuildy/docusaurus-plugin-gitlab` + + `.github/workflows/release.yml` as a **trusted publisher** for + `@ebuildy/docusaurus-plugin-gitlab`. +- **First publish only:** because the package name does not yet exist on npm, the + very first publish may need to be bootstrapped manually from a clean checkout: + `npm ci && npm run build && npm publish --access public` (without + `--provenance`, which requires CI/OIDC). Subsequent releases publish + automatically. + ## Working with AI (Claude Code) This repo is set up to be worked on with **[Claude Code](https://claude.com/claude-code)**, From aecee62f775921da7085e8f5bf6c49a25ff10105 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 14:24:46 +0200 Subject: [PATCH 12/15] fix: ship ESM-only to avoid broken require(ESM) interop The CJS build require()d pure-ESM deps (unified, remark-*, rehype-*). Under Node >=20.19 / >=22 (require(ESM) enabled, as in CI), esbuild interop returned the module namespace { default: fn } instead of the default export, so unified().use() threw 'Expected usable value but received an empty preset' and the Docusaurus e2e build failed. Passed locally only because older Node could not require() the ESM deps and fell back to the ESM build. Drop the cjs tsup format and the require export condition (default now points at the ESM build). Add test/packaging.test.ts to guard against reintroducing a CJS build. Verified the e2e passes both with and without --experimental-require-module. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 7 ++++++- CONTRIBUTING.md | 2 +- README.md | 2 +- package.json | 6 +++--- test/packaging.test.ts | 44 ++++++++++++++++++++++++++++++++++++++++++ tsup.config.ts | 2 +- 6 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 test/packaging.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index bde3636..6e9ac37 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,12 @@ static HTML. The browser never holds a token or calls the GitLab API. ## Commands -- `npm run build` — build the package with tsup (ESM + CJS + d.ts) into `dist/`. +- `npm run build` — build the package with tsup (ESM + d.ts) into `dist/`. + The package is **ESM-only**: every remark/rehype/unified dependency is pure ESM, + so a CJS build would `require()` them — which breaks under Node's `require(ESM)` + interop (`unified().use()` receives `{ default: fn }` → "empty preset", failing + the Docusaurus build). Do not re-add a `cjs` tsup format or a `require` export + condition; `test/packaging.test.ts` guards this. - `npm run test` — run all tests (Vitest). Use `npx vitest run ` for one file. - `npm run typecheck` — `tsc --noEmit`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3927992..f0edb9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ npm run build # bundle the package into dist/ | `npm run typecheck` | `tsc --noEmit` | | `npm run lint` | ESLint + markdownlint | | `npm run lint:fix` | Auto-fix lint issues | -| `npm run build` | Build with tsup (ESM + CJS + types) | +| `npm run build` | Build with tsup (ESM-only + types) | The example sites have their own READMEs: [`examples/site`](./examples/site/README.md) (mocked, drives the e2e) and diff --git a/README.md b/README.md index 20f2c25..665a25e 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ components, so you can wrap or restyle them as needed. ```bash npm install -npm run build # bundle with tsup (ESM + CJS + types) +npm run build # bundle with tsup (ESM-only + types) npm run test # unit tests (Vitest) npm run typecheck # tsc --noEmit ``` diff --git a/package.json b/package.json index eac28b6..afa1136 100644 --- a/package.json +++ b/package.json @@ -11,17 +11,17 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", - "require": "./dist/index.cjs" + "default": "./dist/index.js" }, "./remark": { "types": "./dist/remark/index.d.ts", "import": "./dist/remark/index.js", - "require": "./dist/remark/index.cjs" + "default": "./dist/remark/index.js" }, "./components": { "types": "./dist/components/index.d.ts", "import": "./dist/components/index.js", - "require": "./dist/components/index.cjs" + "default": "./dist/components/index.js" } }, "files": [ diff --git a/test/packaging.test.ts b/test/packaging.test.ts new file mode 100644 index 0000000..433f628 --- /dev/null +++ b/test/packaging.test.ts @@ -0,0 +1,44 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, it, expect } from "vitest"; + +/** + * This package is **ESM-only on purpose**. + * + * Every runtime dependency in the markdown pipeline (`unified`, `remark-*`, + * `rehype-*`, `unist-util-*`) is pure ESM. A CJS build of this package therefore + * emits `require("remark-gfm")` etc., and under Node >= 20.19 / >= 22 (where + * `require(ESM)` is enabled) esbuild's interop returns the module *namespace* + * (`{ default: fn }`) instead of the default export. Passing that to + * `unified().use()` throws "Expected usable value but received an empty preset", + * which broke the Docusaurus build in CI. + * + * Shipping ESM-only removes the broken artifact. These assertions guard against + * accidentally reintroducing a CJS build / `require` export condition. + */ +describe("packaging: ESM-only", () => { + const pkg = JSON.parse( + readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"), + ); + + const subpaths = [".", "./remark", "./components"]; + + it("exposes the documented export subpaths", () => { + for (const sub of subpaths) { + expect(pkg.exports[sub], `missing export "${sub}"`).toBeTruthy(); + } + }); + + it("declares no CJS `require` condition on any export", () => { + for (const sub of subpaths) { + expect(pkg.exports[sub].require, `export "${sub}" must not have a require condition`).toBeUndefined(); + } + }); + + it("points every export target at an ESM `.js` file (never `.cjs`)", () => { + const targets = subpaths.flatMap((sub) => Object.values(pkg.exports[sub] as Record)); + for (const target of targets) { + expect(target.endsWith(".cjs"), `export target ${target} must not be .cjs`).toBe(false); + } + }); +}); diff --git a/tsup.config.ts b/tsup.config.ts index ef8bfc2..c290f2b 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts", "src/remark/index.ts", "src/components/index.ts"], - format: ["esm", "cjs"], + format: ["esm"], dts: true, clean: true, sourcemap: true, From cce44a9bce9ad58c04a5ee28d5a04eecc6b606e2 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 14:27:15 +0200 Subject: [PATCH 13/15] docs: note ESM-only usage and Node 20.19+ recommendation Co-Authored-By: Claude Opus 4.8 --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 665a25e..c551998 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ tokens or network calls ever reach the browser, and pages stay fast. - ✅ README images **and badges are downloaded and localized** (offline-safe, frozen at build time) - ✅ On-disk caching, theme-aware (Infima) styling, graceful error fallbacks -> Requires Docusaurus **3.x** and Node **18+**. +> Requires Docusaurus **3.x** and Node **18+** (Node **20.19+** recommended). ## Installation @@ -21,6 +21,14 @@ tokens or network calls ever reach the browser, and pages stay fast. npm install @ebuildy/docusaurus-plugin-gitlab ``` +> **ESM-only.** This package ships as ES modules (all of its remark/rehype +> dependencies are ESM). Load it from an ESM config — `docusaurus.config.ts` or +> `docusaurus.config.mjs` (the examples below use `import`). If your site still +> uses a CommonJS `docusaurus.config.js` on **Node < 20.19**, either switch the +> config to ESM or load the plugin with `await import(...)`. On **Node 20.19+** +> (and 22+) CommonJS configs work too, since Node can `require()` ES modules +> natively. + ## Setup Two one-time steps in your Docusaurus site. From 1deb1bb47ad3227d2e3498f255a44bfcf97e9a45 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 14:49:23 +0200 Subject: [PATCH 14/15] ci: target Node 20/22/24 and drop Node 18 Docusaurus 3 (@docusaurus/core) requires Node >=20, so the e2e build cannot run on Node 18 (verified in a node:18 container: unit tests pass but docusaurus build fails). Update the CI matrix to [20, 22, 24], add engines.node >=20, and refresh the Node version references in the docs. Verified the full suite (incl. e2e) on Node 24 in a container. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 2 +- CLAUDE.md | 3 ++- CONTRIBUTING.md | 4 +++- README.md | 3 ++- package.json | 3 +++ 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc1b5c2..ef81639 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 22] + node: [20, 22, 24] steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 diff --git a/CLAUDE.md b/CLAUDE.md index 6e9ac37..5ac9667 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,8 @@ static HTML. The browser never holds a token or calls the GitLab API. Normal git usage is fine. CI runs on PRs and pushes to `main` via GitHub Actions (`.github/workflows/`); releases are automated with release-please and published to npm via OIDC trusted publishing — see CONTRIBUTING.md. -- **Docusaurus 3 only** (MDX v3, unified ESM, Node 18+ `fetch`). +- **Docusaurus 3 only** (MDX v3, unified ESM, native `fetch`). **Node 20, 22, 24** + (Docusaurus 3 requires Node 20+; the e2e build will not run on Node 18). - Prefer the latest versions of libraries. - ESM-first. Intra-package imports use explicit `.js` extensions (e.g. `import { Fallback } from "./Fallback.js"`) — required by the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0edb9d..e82f160 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,8 +5,10 @@ you from clone to a green build. ## Prerequisites -- **Node 18+** and **npm** +- **Node 20, 22, or 24** and **npm** (Docusaurus 3 requires Node 20+) - A GitLab token is **not** required for development (the tests mock the API). +- Optionally, a **Dev Container** (`.devcontainer/`) — open the repo in a container + with nvm preloaded with Node 20/22/24; switch with `nvm use 22`. ## Setup diff --git a/README.md b/README.md index c551998..15aa0aa 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ tokens or network calls ever reach the browser, and pages stay fast. - ✅ README images **and badges are downloaded and localized** (offline-safe, frozen at build time) - ✅ On-disk caching, theme-aware (Infima) styling, graceful error fallbacks -> Requires Docusaurus **3.x** and Node **18+** (Node **20.19+** recommended). +> Requires Docusaurus **3.x** and Node **20, 22, or 24** (Docusaurus 3 itself +> needs Node 20+). ## Installation diff --git a/package.json b/package.json index afa1136..12eeaef 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "access": "public" }, "type": "module", + "engines": { + "node": ">=20" + }, "exports": { ".": { "types": "./dist/index.d.ts", From 6fba560b40b0b981ac9ac75b5f772a65eba03d6d Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Sun, 28 Jun 2026 14:49:25 +0200 Subject: [PATCH 15/15] chore: add dev container for switching Node versions nvm preloaded with Node 20/22/24 (default 20); switch with e.g. `nvm use 22`. Co-Authored-By: Claude Opus 4.8 --- .devcontainer/devcontainer.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..93a5a36 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,26 @@ +{ + "name": "docusaurus-plugin-gitlab", + + // Image ships with nvm preinstalled (default Node 20). We additionally install + // the other CI matrix versions so you can switch instantly, e.g. `nvm use 18`. + "image": "mcr.microsoft.com/devcontainers/javascript-node:20-bookworm", + + // Install the full CI matrix (20 / 22 / 24) and keep 20 (the minimum) as default. + "onCreateCommand": "bash -c 'export NVM_DIR=/usr/local/share/nvm && . \"$NVM_DIR/nvm.sh\" && nvm install 20 && nvm install 22 && nvm install 24 && nvm alias default 20'", + + // Install dependencies once the container is up. + "postCreateCommand": "npm ci", + + // Docusaurus dev server (used by the example sites) listens here. + "forwardPorts": [3000], + + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "DavidAnson.vscode-markdownlint", + "vitest.explorer" + ] + } + } +}