@monte3l/groundwork 1.0.0-rc.2 → 1.0.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -5
- package/bin/m3l-groundwork.mjs +36 -2
- package/dist/assets.d.ts +10 -0
- package/dist/assets.js +14 -0
- package/dist/baseline-stage.d.ts +173 -0
- package/dist/baseline-stage.js +215 -0
- package/dist/caps.d.ts +4 -1
- package/dist/caps.js +17 -3
- package/dist/conflicts.d.ts +23 -2
- package/dist/conflicts.js +89 -11
- package/dist/customize-paths.d.ts +92 -0
- package/dist/customize-paths.js +115 -0
- package/dist/emit.d.ts +50 -1
- package/dist/emit.js +121 -13
- package/dist/fatal.d.ts +70 -0
- package/dist/fatal.js +132 -0
- package/dist/format-error.d.ts +113 -0
- package/dist/format-error.js +553 -0
- package/dist/fs-guard.d.ts +142 -0
- package/dist/fs-guard.js +222 -0
- package/dist/git.js +10 -1
- package/dist/harness/conformance.js +2 -0
- package/dist/harness/frontmatter.js +2 -13
- package/dist/harness/grade.js +37 -8
- package/dist/harness/rules.d.ts +20 -3
- package/dist/harness/rules.js +108 -6
- package/dist/harness/types.d.ts +13 -0
- package/dist/harness/types.js +3 -0
- package/dist/inventory.d.ts +77 -3
- package/dist/inventory.js +103 -12
- package/dist/jsonc.d.ts +49 -2
- package/dist/jsonc.js +140 -7
- package/dist/main.d.ts +62 -3
- package/dist/main.js +538 -67
- package/dist/merge-json.d.ts +47 -4
- package/dist/merge-json.js +120 -8
- package/dist/mode.js +12 -2
- package/dist/pack-stage.d.ts +210 -0
- package/dist/pack-stage.js +287 -0
- package/dist/packs.d.ts +19 -13
- package/dist/packs.js +231 -28
- package/dist/palette.d.ts +23 -0
- package/dist/palette.js +22 -0
- package/dist/plugin.d.ts +122 -6
- package/dist/plugin.js +687 -47
- package/dist/report.d.ts +21 -2
- package/dist/report.js +93 -5
- package/dist/staging.d.ts +176 -0
- package/dist/staging.js +375 -0
- package/dist/survey/fs-walk.d.ts +34 -2
- package/dist/survey/fs-walk.js +70 -5
- package/dist/survey/internal/blocked-path.d.ts +27 -0
- package/dist/survey/internal/blocked-path.js +129 -0
- package/dist/survey/internal/package-json.d.ts +13 -0
- package/dist/survey/internal/package-json.js +37 -0
- package/dist/survey/internal/read-guard.d.ts +178 -0
- package/dist/survey/internal/read-guard.js +276 -0
- package/dist/survey/survey-docs.d.ts +17 -2
- package/dist/survey/survey-docs.js +61 -21
- package/dist/survey/survey-harness.d.ts +20 -2
- package/dist/survey/survey-harness.js +87 -46
- package/dist/survey/survey-shape.d.ts +17 -2
- package/dist/survey/survey-shape.js +60 -46
- package/dist/survey/survey-toolchain.d.ts +16 -1
- package/dist/survey/survey-toolchain.js +69 -66
- package/dist/survey/survey.js +6 -4
- package/dist/survey/types.d.ts +79 -0
- package/dist/survey/types.js +2 -6
- package/dist/term.d.ts +80 -0
- package/dist/term.js +145 -0
- package/dist/tokens.js +2 -0
- package/dist/toolchain/conformance.js +2 -0
- package/dist/toolchain/grade.js +26 -8
- package/dist/toolchain/rules.d.ts +13 -4
- package/dist/toolchain/rules.js +16 -0
- package/dist/toolchain/tsconfig-chain.d.ts +2 -0
- package/dist/toolchain/tsconfig-chain.js +32 -8
- package/dist/toolchain/types.d.ts +16 -4
- package/dist/toolchain/types.js +3 -0
- package/package.json +4 -3
- package/plugin/skills/customize/SKILL.md +292 -18
- package/plugin/src/domain-map.ts +39 -14
- package/plugin/src/index.ts +5 -1
- package/plugin/src/kind-facet-map.ts +25 -8
- package/plugin/src/pack-map.ts +147 -25
- package/plugin/src/plugin-map.ts +236 -0
- package/templates/core/.claude/agents/Explore.md +0 -1
- package/templates/core/.claude/agents/code-implementer.md +4 -4
- package/templates/core/.claude/agents/code-reviewer.md +4 -4
- package/templates/core/.claude/agents/silent-failure-hunter.md +2 -2
- package/templates/core/.claude/agents/test-author.md +7 -5
- package/templates/core/.claude/hooks/guard-branch-isolation.mjs +56 -10
- package/templates/core/.claude/hooks/guard-double-background.mjs +13 -8
- package/templates/core/.claude/hooks/guard-git-push-signed.mjs +12 -9
- package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +1890 -38
- package/templates/core/.claude/hooks/guard-js-extension.mjs +2 -1
- package/templates/core/.claude/hooks/guard-no-commonjs.mjs +7 -5
- package/templates/core/.claude/hooks/guard-secret-writes.mjs +7 -5
- package/templates/core/.claude/hooks/inject-decision-gate.mjs +12 -9
- package/templates/core/.claude/hooks/post-edit-verify.mjs +276 -98
- package/templates/core/.claude/rules/agent-dispatch.md +7 -0
- package/templates/core/.claude/rules/tests.md +2 -2
- package/templates/core/.claude/settings.json +5 -0
- package/templates/core/.claude/skills/finishing-work/SKILL.md +58 -10
- package/templates/core/.claude/skills/harness-guidance/SKILL.md +16 -7
- package/templates/core/.claude/skills/starting-work/SKILL.md +5 -0
- package/templates/core/.claude/skills/triaging-ci/SKILL.md +9 -8
- package/templates/core/.claude/skills/typescript-guidance/SKILL.md +137 -20
- package/templates/core/.claude/skills/typescript-guidance/references/area-catalog.md +135 -0
- package/templates/core/.claude/skills/typescript-guidance/references/tooling-sources.md +113 -0
- package/templates/core/.claude/skills/writing-commits/SKILL.md +2 -2
- package/templates/core/.github/dependabot.yml +18 -0
- package/templates/core/.github/workflows/ci.yml +15 -15
- package/templates/core/.github/workflows/dependency-review.yml +2 -2
- package/templates/core/.github/workflows/security-audit.yml +10 -4
- package/templates/core/.prettierignore +4 -0
- package/templates/core/CLAUDE.md +53 -3
- package/templates/core/README.md +30 -10
- package/templates/core/_gitignore +17 -0
- package/templates/core/bin/check-exports.mjs +11 -5
- package/templates/core/bin/lib/agent-roster.mjs +1 -1
- package/templates/core/bin/lib/frontmatter.mjs +4 -2
- package/templates/core/bin/lib/harness-rules.mjs +169 -20
- package/templates/core/bin/lib/protected-paths.mjs +93 -5
- package/templates/core/bin/lib/toolchain-rules.mjs +187 -26
- package/templates/core/bin/lib/verify-steps.mjs +29 -11
- package/templates/core/bin/verify.mjs +12 -0
- package/templates/core/eslint.config.js +5 -0
- package/templates/core/package.json +7 -7
- package/templates/core/vitest.config.ts +8 -1
- package/templates/packs/README.md +35 -24
- package/templates/packs/github/files/.claude/skills/reviewing-dependabot-prs/SKILL.md +133 -0
- package/templates/packs/github/files/.claude/skills/triaging-scan-alerts/SKILL.md +88 -0
- package/templates/packs/github/files/.claude/skills/watching-pr-checks/SKILL.md +94 -0
- package/templates/packs/github/files/.github/workflows/claude-pr-review.yml +267 -0
- package/templates/packs/github/files/.github/workflows/claude.yml +106 -0
- package/templates/packs/github/pack.json +19 -0
- package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +1122 -65
- package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +147 -13
- package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline.mjs +6 -2
- package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +227 -44
- package/templates/packs/harness-extras/pack.json +18 -13
- package/templates/packs/publishing/files/.changeset/README.md +25 -0
- package/templates/packs/publishing/files/.changeset/config.json +7 -0
- package/templates/packs/publishing/files/.github/release-tools/package.json +9 -0
- package/templates/packs/publishing/files/.github/workflows/release.yml +293 -0
- package/templates/packs/publishing/files/REUSE.toml +28 -0
- package/templates/packs/publishing/files/bin/check-dts-deps.mjs +234 -0
- package/templates/packs/publishing/files/bin/check-license-headers.mjs +217 -0
- package/templates/packs/publishing/files/bin/check-publish-version.mjs +149 -0
- package/templates/packs/publishing/files/bin/lib/npm-publish-args.mjs +86 -0
- package/templates/packs/publishing/files/bin/pnpm-publish-shim.mjs +86 -0
- package/templates/packs/publishing/pack.json +35 -0
- package/templates/packs/{harness-extras → quality}/files/bin/check-file-budget.mjs +6 -4
- package/templates/packs/quality/pack.json +29 -0
- package/templates/packs/supply-chain/files/.github/workflows/gitleaks.yml +52 -0
- package/templates/packs/supply-chain/files/.github/workflows/scorecard.yml +48 -0
- package/templates/packs/supply-chain/files/.gitleaks.toml +2 -0
- package/templates/packs/supply-chain/pack.json +19 -0
- package/templates/packs/worktrees/files/.claude/hooks/ensure-worktree-deps.mjs +283 -0
- package/templates/packs/worktrees/files/.claude/hooks/guard-worktree-only.mjs +245 -0
- package/templates/packs/worktrees/files/.claude/hooks/repair-core-bare.mjs +335 -0
- package/templates/packs/worktrees/files/.claude/skills/working-in-worktrees/SKILL.md +139 -0
- package/templates/packs/worktrees/files/.worktreeinclude +11 -0
- package/templates/packs/worktrees/pack.json +64 -0
- package/templates/packs/statusline/pack.json +0 -31
- /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline-layout.mjs +0 -0
- /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/subagent-statusline.mjs +0 -0
- /package/templates/packs/{harness-extras → quality}/files/.claude/agents/type-design-analyzer.md +0 -0
- /package/templates/packs/{harness-extras → quality}/files/bin/file-budget-baseline.json +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changesets
|
|
2
|
+
|
|
3
|
+
A change to this package's user-visible behavior lands with a changeset: run
|
|
4
|
+
`pnpm changeset`, pick the bump, and commit the generated file with the PR.
|
|
5
|
+
Merging to the release branch opens (or updates) a
|
|
6
|
+
`chore(release): version packages` PR; merging that publishes to npm. See
|
|
7
|
+
`.github/workflows/release.yml`.
|
|
8
|
+
|
|
9
|
+
This pack ships the `changeset`/`version:packages` scripts and the release
|
|
10
|
+
workflow, but not the `@changesets/cli` package itself -- the wiring
|
|
11
|
+
contract a pack installs through never edits `package.json`'s
|
|
12
|
+
`dependencies`/`devDependencies` (see `templates/packs/README.md`). Add it
|
|
13
|
+
by hand once, **before the first `pnpm verify`, not just before the first
|
|
14
|
+
real release** -- both scripts reference the `changeset` binary, and
|
|
15
|
+
`knip`'s unlisted-binaries check correctly fails until it actually resolves:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
pnpm add -D @changesets/cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`config.json`'s `changelog` is the plain built-in generator
|
|
22
|
+
(`@changesets/cli/changelog`), which needs no further setup. Swap in
|
|
23
|
+
`@changesets/changelog-github` (and its `repo` option) once this project's
|
|
24
|
+
GitHub repository is known, for changelog entries that link back to the
|
|
25
|
+
originating PR/commit.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "release-tools",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Pins the exact npm version the release workflow's publish job installs, so the pin is verified by a committed lockfile's integrity hash rather than a bare version string in YAML.",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"npm": "11.20.0"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright the __PROJECT_NAME__ contributors
|
|
2
|
+
# SPDX-License-Identifier: NOASSERTION
|
|
3
|
+
|
|
4
|
+
name: Release
|
|
5
|
+
|
|
6
|
+
# What this does: on every push to `main`, this workflow either opens a PR
|
|
7
|
+
# that bumps the package version and changelog, or -- once that PR has
|
|
8
|
+
# merged -- packs and publishes the package to npm. It runs
|
|
9
|
+
# [Changesets](https://github.com/changesets/changesets), the tool this
|
|
10
|
+
# project uses to track pending releases (each PR that changes the
|
|
11
|
+
# package's user-facing behavior adds a small Markdown file under
|
|
12
|
+
# `.changeset/` describing the change and its version bump; this workflow
|
|
13
|
+
# reads those files to decide what to do next).
|
|
14
|
+
#
|
|
15
|
+
# The flow, one job each:
|
|
16
|
+
# 1. select-mode reads the repo and decides: "version", "publish", or "none"
|
|
17
|
+
# 2. version opens or updates the "Version Packages" PR
|
|
18
|
+
# 3. pack builds, runs every gate, and packs the tarball
|
|
19
|
+
# 4. publish uploads that tarball to npm via trusted publishing
|
|
20
|
+
#
|
|
21
|
+
# "Trusted publishing" is npm's passwordless publish mechanism: instead of a
|
|
22
|
+
# long-lived npm token stored as a secret, this workflow proves its identity
|
|
23
|
+
# to npm via OIDC (OpenID Connect -- a short-lived, cryptographically signed
|
|
24
|
+
# token GitHub Actions mints for the running job, which npm verifies against
|
|
25
|
+
# the trusted-publisher config for this exact repo and workflow file). This
|
|
26
|
+
# is the shape the changesets docs recommend for trusted publishing: the one
|
|
27
|
+
# job that holds the OIDC token (`publish`, via `id-token: write`) never runs
|
|
28
|
+
# build, test, or repo-script code -- it only runs the changesets CLI and the
|
|
29
|
+
# pnpm shim below, with `--ignore-scripts`. `pack` holds nothing beyond
|
|
30
|
+
# `contents: read`.
|
|
31
|
+
#
|
|
32
|
+
# "Staged" publish (vs. a "direct" one) means `npm stage publish` uploads the
|
|
33
|
+
# tarball and creates the version, but it isn't installable yet until a
|
|
34
|
+
# maintainer separately approves it with 2FA (`npm stage approve`) -- npm's
|
|
35
|
+
# own stronger default over a direct `npm publish`, which would make the
|
|
36
|
+
# version live immediately. "Provenance" is the signed, publicly verifiable
|
|
37
|
+
# record npm attaches to a trusted-published package, linking it back to the
|
|
38
|
+
# exact commit and workflow run that built it.
|
|
39
|
+
#
|
|
40
|
+
# npm trusted publishers are configured per package, against this exact file
|
|
41
|
+
# name (`release.yml`, not a path) -- see this pack's adoptNotes for the
|
|
42
|
+
# one-time setup this depends on.
|
|
43
|
+
on:
|
|
44
|
+
push:
|
|
45
|
+
branches: [main]
|
|
46
|
+
|
|
47
|
+
# Reset, then grant per job.
|
|
48
|
+
permissions: {}
|
|
49
|
+
|
|
50
|
+
# Releases must not be cancelled midway: a half-published release is worse
|
|
51
|
+
# than a queued one.
|
|
52
|
+
concurrency:
|
|
53
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
54
|
+
|
|
55
|
+
jobs:
|
|
56
|
+
select-mode:
|
|
57
|
+
name: Select mode
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
timeout-minutes: 10
|
|
60
|
+
outputs:
|
|
61
|
+
mode: ${{ steps.select-mode.outputs.mode }}
|
|
62
|
+
publish-plan-artifact-id: ${{ steps.select-mode.outputs.publish-plan-artifact-id }}
|
|
63
|
+
permissions:
|
|
64
|
+
contents: read # actions/checkout
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
67
|
+
with:
|
|
68
|
+
persist-credentials: false
|
|
69
|
+
# `.node-version` is the single Node pin, so this file must never carry
|
|
70
|
+
# a literal version (see check-node-version.mjs). No package-manager
|
|
71
|
+
# cache anywhere in this file: a restored cache is an input an attacker
|
|
72
|
+
# can poison, and these jobs are the ones that publish.
|
|
73
|
+
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
74
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
75
|
+
with:
|
|
76
|
+
node-version-file: .node-version
|
|
77
|
+
package-manager-cache: false
|
|
78
|
+
- run: pnpm install --frozen-lockfile
|
|
79
|
+
- id: select-mode
|
|
80
|
+
uses: changesets/action/select-mode@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2
|
|
81
|
+
|
|
82
|
+
version:
|
|
83
|
+
name: Open version PR
|
|
84
|
+
needs: select-mode
|
|
85
|
+
if: needs.select-mode.outputs.mode == 'version'
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
timeout-minutes: 15
|
|
88
|
+
permissions:
|
|
89
|
+
contents: read # actions/checkout; see below for who does the actual writing
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
92
|
+
with:
|
|
93
|
+
persist-credentials: false
|
|
94
|
+
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
95
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
96
|
+
with:
|
|
97
|
+
node-version-file: .node-version
|
|
98
|
+
package-manager-cache: false
|
|
99
|
+
- run: pnpm install --frozen-lockfile
|
|
100
|
+
# If this project's branch protection requires status checks on every
|
|
101
|
+
# PR with no bypass actor, a PR opened with the default GITHUB_TOKEN
|
|
102
|
+
# never triggers `pull_request`-event workflows (GitHub's own
|
|
103
|
+
# anti-recursion rule) and could therefore never merge -- this mints a
|
|
104
|
+
# short-lived token from a GitHub App installed on this repo instead.
|
|
105
|
+
# See this pack's adoptNotes: if that's not this project's situation,
|
|
106
|
+
# drop this step and pass `github-token: ${{ secrets.GITHUB_TOKEN }}`
|
|
107
|
+
# to the version step below instead. The App needs only
|
|
108
|
+
# `contents: write` and `pull-requests: write`; its token auto-expires
|
|
109
|
+
# in an hour and this action revokes it when the job ends.
|
|
110
|
+
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
111
|
+
id: app-token
|
|
112
|
+
with:
|
|
113
|
+
# Both created as repo secrets (`gh secret set`), not vars: the
|
|
114
|
+
# client ID isn't sensitive on its own, but keeping both next to
|
|
115
|
+
# each other avoids a second place to remember to update.
|
|
116
|
+
client-id: ${{ secrets.APP_CLIENT_ID }}
|
|
117
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
118
|
+
- uses: changesets/action/version@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2
|
|
119
|
+
with:
|
|
120
|
+
github-token: ${{ steps.app-token.outputs.token }}
|
|
121
|
+
script: pnpm version:packages
|
|
122
|
+
commit-message: "chore(release): version packages"
|
|
123
|
+
pr-title: "chore(release): version packages"
|
|
124
|
+
|
|
125
|
+
pack:
|
|
126
|
+
name: Verify and pack
|
|
127
|
+
needs: select-mode
|
|
128
|
+
if: needs.select-mode.outputs.mode == 'publish'
|
|
129
|
+
runs-on: ubuntu-latest
|
|
130
|
+
timeout-minutes: 45
|
|
131
|
+
outputs:
|
|
132
|
+
pack-dir-artifact-id: ${{ steps.pack.outputs.pack-dir-artifact-id }}
|
|
133
|
+
permissions:
|
|
134
|
+
contents: read # actions/checkout
|
|
135
|
+
steps:
|
|
136
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
137
|
+
with:
|
|
138
|
+
persist-credentials: false
|
|
139
|
+
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
140
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
141
|
+
with:
|
|
142
|
+
node-version-file: .node-version
|
|
143
|
+
package-manager-cache: false
|
|
144
|
+
- run: pnpm install --frozen-lockfile
|
|
145
|
+
# The whole gate, before a tarball exists. `pnpm verify` is one full
|
|
146
|
+
# run of every group, the same steps ci.yml spreads across its lanes.
|
|
147
|
+
- run: pnpm verify
|
|
148
|
+
# Deliberately not one of pnpm verify's own steps: package.json's
|
|
149
|
+
# version only changes in the version PR, so this check would fail
|
|
150
|
+
# every ordinary push between releases (the version it's checking is
|
|
151
|
+
# always the one already published) if it ran there. This is the one
|
|
152
|
+
# place "is this version about to collide with an already-published
|
|
153
|
+
# one" is the right question to ask -- right before packing it.
|
|
154
|
+
- run: node bin/check-publish-version.mjs
|
|
155
|
+
- id: pack
|
|
156
|
+
uses: changesets/action/pack@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2
|
|
157
|
+
with:
|
|
158
|
+
publish-plan-artifact-id: ${{ needs.select-mode.outputs.publish-plan-artifact-id }}
|
|
159
|
+
|
|
160
|
+
publish:
|
|
161
|
+
name: Publish to npm
|
|
162
|
+
needs: pack
|
|
163
|
+
runs-on: ubuntu-latest
|
|
164
|
+
timeout-minutes: 15
|
|
165
|
+
# Gates the git tag, the GitHub Release, and `npm stage publish` behind a
|
|
166
|
+
# required reviewer -- without this, all three already exist by the time
|
|
167
|
+
# a maintainer even sees there's a staged version to approve with
|
|
168
|
+
# `npm stage approve`. The environment (created once, by hand, via
|
|
169
|
+
# `gh api` -- see this pack's adoptNotes) has no branch restriction
|
|
170
|
+
# beyond the release branch and no wait timer: it exists to require a
|
|
171
|
+
# person, not to add a delay.
|
|
172
|
+
environment: npm-publish
|
|
173
|
+
permissions:
|
|
174
|
+
contents: write # changesets/action/publish pushes tags and creates GitHub Releases
|
|
175
|
+
id-token: write # npm trusted publishing (OIDC) and provenance
|
|
176
|
+
attestations: write # actions/attest-build-provenance on the packed tarball
|
|
177
|
+
steps:
|
|
178
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
179
|
+
with:
|
|
180
|
+
persist-credentials: false
|
|
181
|
+
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
182
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
|
183
|
+
with:
|
|
184
|
+
node-version-file: .node-version
|
|
185
|
+
package-manager-cache: false
|
|
186
|
+
# Only the changesets CLI is needed here, not any package's lifecycle scripts.
|
|
187
|
+
- run: pnpm install --frozen-lockfile --ignore-scripts
|
|
188
|
+
# `npm stage publish` needs npm >= 11.15.0 (Node >= 22.14.0 -- already
|
|
189
|
+
# covered by .node-version). Pinned to an exact version, not a range or
|
|
190
|
+
# `latest`: this job holds the OIDC token, and an unpinned
|
|
191
|
+
# package-manager install is exactly what OpenSSF Scorecard's
|
|
192
|
+
# Pinned-Dependencies check flags. `.github/release-tools/` pins the
|
|
193
|
+
# exact tarball via its committed lockfile's integrity hash, and
|
|
194
|
+
# `npm ci` verifies the install against it -- bumping the pin is then
|
|
195
|
+
# a normal reviewed PR (a lockfile diff), not a hand-edited version
|
|
196
|
+
# string.
|
|
197
|
+
- run: npm ci --ignore-scripts --prefix .github/release-tools
|
|
198
|
+
- run: echo "$GITHUB_WORKSPACE/.github/release-tools/node_modules/.bin" >> "$GITHUB_PATH"
|
|
199
|
+
# changesets publishes through `pnpm publish` in a pnpm-managed
|
|
200
|
+
# project, and pnpm 12's native publish is rejected by npmjs.com's OIDC
|
|
201
|
+
# exchange (403 "OIDC permission denied"). This puts a `pnpm` ahead of
|
|
202
|
+
# the real one that forwards everything except `publish`, which goes
|
|
203
|
+
# to `npm stage publish` instead (see bin/pnpm-publish-shim.mjs -- the
|
|
204
|
+
# trusted publisher only allows staged publishing, npm's own default
|
|
205
|
+
# since 2026-09-03; a direct `npm publish` gets the same 403 for a
|
|
206
|
+
# different reason). Remove both once pnpm's own publish authenticates.
|
|
207
|
+
- name: Route publish through the npm CLI
|
|
208
|
+
run: |
|
|
209
|
+
shim_dir="$RUNNER_TEMP/pnpm-shim"
|
|
210
|
+
mkdir -p "$shim_dir"
|
|
211
|
+
printf '#!/bin/sh\nexport PNPM_SHIM_DIR=%s\nexec node "%s/bin/pnpm-publish-shim.mjs" "$@"\n' \
|
|
212
|
+
"$shim_dir" "$GITHUB_WORKSPACE" > "$shim_dir/pnpm"
|
|
213
|
+
chmod +x "$shim_dir/pnpm"
|
|
214
|
+
echo "$shim_dir" >> "$GITHUB_PATH"
|
|
215
|
+
- uses: changesets/action/publish@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2
|
|
216
|
+
id: publish
|
|
217
|
+
with:
|
|
218
|
+
pack-dir-artifact-id: ${{ needs.pack.outputs.pack-dir-artifact-id }}
|
|
219
|
+
# npm provenance alone already meets most supply-chain "signed
|
|
220
|
+
# releases" criteria (a Sigstore-backed, OIDC-issued attestation with
|
|
221
|
+
# no long-lived key). These three steps add a second, independent
|
|
222
|
+
# proof on the GitHub Release itself: the exact tarball `pack` already
|
|
223
|
+
# built and tested, fetched back out of its own artifact (never
|
|
224
|
+
# rebuilt), attested, and attached. `if:` guards all three on
|
|
225
|
+
# `publish` actually having published -- `select-mode` only decides
|
|
226
|
+
# the job should run, not that a version was new.
|
|
227
|
+
# `continue-on-error: true` on all three: `npm stage publish` has
|
|
228
|
+
# already succeeded by this point, so a failure here (a transient
|
|
229
|
+
# download error, say) must never fail the whole job and leave a
|
|
230
|
+
# staged, tagged, unreleased version behind -- this block is
|
|
231
|
+
# best-effort supplementary evidence, the same status as the
|
|
232
|
+
# "List pending stages" step below.
|
|
233
|
+
- name: Fetch the packed tarball back out of the pack job's artifact
|
|
234
|
+
if: steps.publish.outputs.published == 'true'
|
|
235
|
+
continue-on-error: true
|
|
236
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
237
|
+
with:
|
|
238
|
+
artifact-ids: ${{ needs.pack.outputs.pack-dir-artifact-id }}
|
|
239
|
+
path: .release-artifacts
|
|
240
|
+
merge-multiple: true
|
|
241
|
+
- name: Attest build provenance for the published tarball
|
|
242
|
+
if: steps.publish.outputs.published == 'true'
|
|
243
|
+
continue-on-error: true
|
|
244
|
+
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
|
245
|
+
with:
|
|
246
|
+
# actions/attest-build-provenance's own glob support (not the
|
|
247
|
+
# shell's), so `**` resolves correctly regardless of `globstar`.
|
|
248
|
+
subject-path: .release-artifacts/**/*.tgz
|
|
249
|
+
- name: Attach the attested tarball to the GitHub Release
|
|
250
|
+
if: steps.publish.outputs.published == 'true'
|
|
251
|
+
continue-on-error: true
|
|
252
|
+
env:
|
|
253
|
+
GH_TOKEN: ${{ github.token }}
|
|
254
|
+
run: |
|
|
255
|
+
# `changeset pack` writes into <outDir>/packages/*.tgz, not
|
|
256
|
+
# <outDir>/*.tgz -- found by path, not a bare shell glob the
|
|
257
|
+
# default (non-globstar) bash would pass through unexpanded on
|
|
258
|
+
# no match.
|
|
259
|
+
tarball="$(find .release-artifacts -name '*.tgz' -print -quit)"
|
|
260
|
+
if [ -z "$tarball" ]; then
|
|
261
|
+
echo "no packed tarball found under .release-artifacts/ -- skipping the release-asset upload" >&2
|
|
262
|
+
exit 0
|
|
263
|
+
fi
|
|
264
|
+
name="$(node -p "require('./package.json').name")"
|
|
265
|
+
version="$(node -p "require('./package.json').version")"
|
|
266
|
+
# changesets names the tag `v<version>` for a single-package
|
|
267
|
+
# repository (its "root" tool, which is what this baseline is: no
|
|
268
|
+
# `packages:` in pnpm-workspace.yaml) and `<name>@<version>` once
|
|
269
|
+
# the repository becomes a workspace. Upload to whichever one
|
|
270
|
+
# actually has a Release rather than guessing.
|
|
271
|
+
tag=""
|
|
272
|
+
for candidate in "v${version}" "${name}@${version}"; do
|
|
273
|
+
if gh release view "$candidate" --repo "${{ github.repository }}" >/dev/null 2>&1; then
|
|
274
|
+
tag="$candidate"
|
|
275
|
+
break
|
|
276
|
+
fi
|
|
277
|
+
done
|
|
278
|
+
if [ -z "$tag" ]; then
|
|
279
|
+
echo "cannot attach the tarball: no GitHub Release found for v${version} or ${name}@${version} -- failing this best-effort step" >&2
|
|
280
|
+
exit 1
|
|
281
|
+
fi
|
|
282
|
+
gh release upload "$tag" \
|
|
283
|
+
"$tarball" --clobber --repo "${{ github.repository }}"
|
|
284
|
+
# Staging succeeds unattended; going live needs a maintainer's 2FA
|
|
285
|
+
# (`npm stage approve <id>`, on the CLI or npmjs.com -- never
|
|
286
|
+
# automatable, by design). Best-effort convenience only: this job
|
|
287
|
+
# never logs in with a token (it publishes purely via the OIDC
|
|
288
|
+
# exchange scoped to the publish call itself), so `npm stage list` may
|
|
289
|
+
# print nothing useful -- `|| true` means that possibility never fails
|
|
290
|
+
# the job.
|
|
291
|
+
- name: List pending stages (best effort)
|
|
292
|
+
if: always()
|
|
293
|
+
run: npm stage list --package "$(node -p "require('./package.json').name")" || true
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# REUSE Software (https://reuse.software) annotations for files that cannot,
|
|
2
|
+
# or by this project's own convention should not, carry an inline SPDX
|
|
3
|
+
# header -- see bin/check-license-headers.mjs for the gate that gap-checks
|
|
4
|
+
# this file's globs against that expectation.
|
|
5
|
+
version = 1
|
|
6
|
+
SPDX-PackageName = "__PROJECT_NAME__"
|
|
7
|
+
|
|
8
|
+
# Data files, prose, and dotfiles with no comment syntax of their own to
|
|
9
|
+
# carry a header in. SPDX-License-Identifier is left as NOASSERTION -- edit
|
|
10
|
+
# both this block and bin/check-license-headers.mjs's headerLines() once
|
|
11
|
+
# this project's actual license is decided (a LICENSE file, if one exists,
|
|
12
|
+
# is the source of truth).
|
|
13
|
+
[[annotations]]
|
|
14
|
+
path = [
|
|
15
|
+
"**/*.json",
|
|
16
|
+
"**/*.md",
|
|
17
|
+
"pnpm-lock.yaml",
|
|
18
|
+
".node-version",
|
|
19
|
+
".npmrc",
|
|
20
|
+
".gitignore",
|
|
21
|
+
".prettierignore",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"REUSE.toml",
|
|
24
|
+
".gitleaks.toml",
|
|
25
|
+
".worktreeinclude",
|
|
26
|
+
]
|
|
27
|
+
SPDX-FileCopyrightText = "Copyright the __PROJECT_NAME__ contributors"
|
|
28
|
+
SPDX-License-Identifier = "NOASSERTION"
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-FileCopyrightText: Copyright the __PROJECT_NAME__ contributors
|
|
3
|
+
// SPDX-License-Identifier: NOASSERTION
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks that every package a published `.d.ts` file imports by bare
|
|
7
|
+
* specifier is declared in `package.json`'s `dependencies` (or
|
|
8
|
+
* `peerDependencies`) -- not only `devDependencies`, and not left
|
|
9
|
+
* undeclared entirely. A type import from a `devDependencies`-only package
|
|
10
|
+
* type-checks fine inside this repo (the dev dependency is installed here)
|
|
11
|
+
* but breaks a consumer's own `tsc`, since npm never installs
|
|
12
|
+
* `devDependencies` for a package's dependents. `publint`/`attw` don't catch
|
|
13
|
+
* this specific case, which is why it's a separate gate.
|
|
14
|
+
*
|
|
15
|
+
* Also honors `/// <reference types="x" />` directives (a common way a
|
|
16
|
+
* `.d.ts` pulls in ambient types), which the plain import/export scan below
|
|
17
|
+
* would miss entirely since triple-slash directives are stripped as
|
|
18
|
+
* comments before that scan runs. `types="node"` is exempt: `@types/node`
|
|
19
|
+
* is a near-universal ambient dev convenience most consumers already carry
|
|
20
|
+
* on their own, not something this package should have to declare as a
|
|
21
|
+
* real dependency just because it uses a Node builtin type.
|
|
22
|
+
*
|
|
23
|
+
* A declared package satisfies either its own name or its `@types/<name>`
|
|
24
|
+
* counterpart (`@types/<scope>__<name>` for a scoped `@scope/<name>`) --
|
|
25
|
+
* many packages only ship (or only need) the `@types/*` package, not the
|
|
26
|
+
* runtime one.
|
|
27
|
+
*
|
|
28
|
+
* A no-op (`checked: 0`, exit 0), not a failure, when:
|
|
29
|
+
* - `package.json` has `private: true` (the baseline's own default) -- a
|
|
30
|
+
* private package publishes no `.d.ts` files to anyone.
|
|
31
|
+
* - `dist/` does not exist yet (this step runs in the `build` verify group,
|
|
32
|
+
* after the `build` step itself -- see bin/lib/verify-steps.mjs).
|
|
33
|
+
* - `dist/` exists but contains no `.d.ts`/`.d.mts`/`.d.cts` file -- if this
|
|
34
|
+
* package is NOT private and really does mean to publish, this is worth a
|
|
35
|
+
* second look (a build that emits nothing, or a `tsconfig`/`exports`
|
|
36
|
+
* pointing declarations somewhere other than `dist/`), which the log
|
|
37
|
+
* message below says explicitly rather than only "skipping".
|
|
38
|
+
*/
|
|
39
|
+
import process from "node:process";
|
|
40
|
+
import { isBuiltin } from "node:module";
|
|
41
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
42
|
+
import { join } from "node:path";
|
|
43
|
+
import { fileURLToPath } from "node:url";
|
|
44
|
+
|
|
45
|
+
const repoRoot = join(fileURLToPath(import.meta.url), "..", "..");
|
|
46
|
+
|
|
47
|
+
const DTS_EXTENSIONS = new Set([".d.ts", ".d.mts", ".d.cts"]);
|
|
48
|
+
|
|
49
|
+
function dtsExtensionOf(fileName) {
|
|
50
|
+
for (const ext of DTS_EXTENSIONS) {
|
|
51
|
+
if (fileName.endsWith(ext)) return ext;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Every `.d.ts`/`.d.mts`/`.d.cts` file under `dir`, recursively. */
|
|
57
|
+
function listDtsFiles(dir) {
|
|
58
|
+
if (!existsSync(dir)) return [];
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
61
|
+
const full = join(dir, entry.name);
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
out.push(...listDtsFiles(full));
|
|
64
|
+
} else if (dtsExtensionOf(entry.name) !== undefined) {
|
|
65
|
+
out.push(full);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Every `/// <reference types="x" />` directive's `x`, extracted BEFORE
|
|
73
|
+
* comment-stripping (a triple-slash directive is syntactically a line
|
|
74
|
+
* comment, so it would otherwise vanish along with ordinary comments).
|
|
75
|
+
*/
|
|
76
|
+
function referenceTypeNamesIn(content) {
|
|
77
|
+
const names = new Set();
|
|
78
|
+
const pattern = /^\/\/\/\s*<reference\s+types\s*=\s*["']([^"']+)["']\s*\/>/gm;
|
|
79
|
+
for (const match of content.matchAll(pattern)) {
|
|
80
|
+
const name = match[1];
|
|
81
|
+
if (name !== undefined) names.add(name);
|
|
82
|
+
}
|
|
83
|
+
return names;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A conservative comment strip: block comments unconditionally, and a line
|
|
88
|
+
* comment only when the `//` isn't immediately preceded by `:` (so a
|
|
89
|
+
* `"https://example.com"` string literal survives). Not a full tokenizer --
|
|
90
|
+
* `.d.ts` output is machine-generated by `tsc`, not hand-written prose, so
|
|
91
|
+
* this heuristic is proportionate to what actually shows up there.
|
|
92
|
+
*/
|
|
93
|
+
function stripComments(content) {
|
|
94
|
+
return content
|
|
95
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
96
|
+
.replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Every bare-specifier import/export/`import()` target in `content` --
|
|
101
|
+
* a deliberately simple regex scan, not a TypeScript AST parse. A relative
|
|
102
|
+
* specifier (starting with `.` or `/`) is never a package dependency, so
|
|
103
|
+
* only a bare specifier is returned. `content` should already have comments
|
|
104
|
+
* stripped (see {@link stripComments}), or a specifier-shaped string inside
|
|
105
|
+
* a doc comment (an `@example` importing from some other package, say)
|
|
106
|
+
* would be flagged as a real dependency.
|
|
107
|
+
*/
|
|
108
|
+
function bareSpecifiersIn(content) {
|
|
109
|
+
const specifiers = new Set();
|
|
110
|
+
const pattern =
|
|
111
|
+
/(?:from|import)\s*\(?\s*["']([^"']+)["']|require\(\s*["']([^"']+)["']\s*\)/g;
|
|
112
|
+
for (const match of content.matchAll(pattern)) {
|
|
113
|
+
const specifier = match[1] ?? match[2];
|
|
114
|
+
if (
|
|
115
|
+
specifier !== undefined &&
|
|
116
|
+
!specifier.startsWith(".") &&
|
|
117
|
+
!specifier.startsWith("/")
|
|
118
|
+
) {
|
|
119
|
+
specifiers.add(specifier);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return specifiers;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A bare specifier's package name: `@scope/name` keeps its scope, `name/sub/path` is trimmed to `name`. */
|
|
126
|
+
export function packageNameOf(specifier) {
|
|
127
|
+
const parts = specifier.split("/");
|
|
128
|
+
if (specifier.startsWith("@")) {
|
|
129
|
+
return parts.slice(0, 2).join("/");
|
|
130
|
+
}
|
|
131
|
+
return parts[0];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The `@types/*` package name that would satisfy a declared-dependency check for `packageName`. */
|
|
135
|
+
function typesPackageNameFor(packageName) {
|
|
136
|
+
if (packageName.startsWith("@")) {
|
|
137
|
+
const scoped = packageName.slice(1).split("/");
|
|
138
|
+
return `@types/${scoped[0]}__${scoped[1] ?? ""}`;
|
|
139
|
+
}
|
|
140
|
+
return `@types/${packageName}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {Map<string, string>} dtsFiles path -> content
|
|
145
|
+
* @param {{ dependencies?: Record<string, string>, peerDependencies?: Record<string, string> }} pkg
|
|
146
|
+
* @returns {{ packageName: string, files: string[] }[]} every package name
|
|
147
|
+
* referenced by a `.d.ts` import (or `/// <reference types="..." />`) that
|
|
148
|
+
* isn't declared as a real dependency (by its own name or its `@types/*`
|
|
149
|
+
* counterpart), each with the file(s) that reference it.
|
|
150
|
+
*/
|
|
151
|
+
export function findUndeclaredTypeDeps(dtsFiles, pkg) {
|
|
152
|
+
const declared = new Set([
|
|
153
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
154
|
+
...Object.keys(pkg.peerDependencies ?? {}),
|
|
155
|
+
]);
|
|
156
|
+
const byPackage = new Map();
|
|
157
|
+
|
|
158
|
+
const record = (path, packageName) => {
|
|
159
|
+
if (packageName === pkg.name) return;
|
|
160
|
+
if (isBuiltin(packageName)) return;
|
|
161
|
+
if (
|
|
162
|
+
declared.has(packageName) ||
|
|
163
|
+
declared.has(typesPackageNameFor(packageName))
|
|
164
|
+
)
|
|
165
|
+
return;
|
|
166
|
+
const files = byPackage.get(packageName) ?? [];
|
|
167
|
+
files.push(path);
|
|
168
|
+
byPackage.set(packageName, files);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
for (const [path, rawContent] of dtsFiles) {
|
|
172
|
+
for (const referenceName of referenceTypeNamesIn(rawContent)) {
|
|
173
|
+
if (referenceName === "node") continue;
|
|
174
|
+
record(path, referenceName);
|
|
175
|
+
}
|
|
176
|
+
for (const specifier of bareSpecifiersIn(stripComments(rawContent))) {
|
|
177
|
+
record(path, packageNameOf(specifier));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return [...byPackage.entries()].map(([packageName, files]) => ({
|
|
182
|
+
packageName,
|
|
183
|
+
files,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function main() {
|
|
188
|
+
const pkgPath = join(repoRoot, "package.json");
|
|
189
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
190
|
+
|
|
191
|
+
if (pkg?.private === true) {
|
|
192
|
+
console.log("check-dts-deps: package.json is private -- skipping");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const distDir = join(repoRoot, "dist");
|
|
197
|
+
const dtsPaths = listDtsFiles(distDir);
|
|
198
|
+
if (dtsPaths.length === 0) {
|
|
199
|
+
console.log(
|
|
200
|
+
"check-dts-deps: no dist/**/*.d.ts found -- skipping (if this package means to publish " +
|
|
201
|
+
'type declarations, check the build output path and package.json\'s "types"/"exports")',
|
|
202
|
+
);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const dtsFiles = new Map(
|
|
207
|
+
dtsPaths.map((path) => [path, readFileSync(path, "utf8")]),
|
|
208
|
+
);
|
|
209
|
+
const undeclared = findUndeclaredTypeDeps(dtsFiles, pkg);
|
|
210
|
+
|
|
211
|
+
if (undeclared.length > 0) {
|
|
212
|
+
console.error(
|
|
213
|
+
`check-dts-deps: ${undeclared.length} package(s) are referenced by a published .d.ts file ` +
|
|
214
|
+
"but are not declared in dependencies/peerDependencies (a devDependencies-only or " +
|
|
215
|
+
"undeclared package breaks a consumer's own typecheck):",
|
|
216
|
+
);
|
|
217
|
+
for (const { packageName, files } of undeclared) {
|
|
218
|
+
console.error(` ${packageName} (from ${files.join(", ")})`);
|
|
219
|
+
}
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(
|
|
225
|
+
`check-dts-deps: ok -- checked ${dtsPaths.length} declaration file(s)`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (
|
|
230
|
+
process.argv[1] !== undefined &&
|
|
231
|
+
fileURLToPath(import.meta.url) === realpathSync(process.argv[1])
|
|
232
|
+
) {
|
|
233
|
+
main();
|
|
234
|
+
}
|