Skip to content

feat(release): carry the library build on the packaged branch and publish to npm - #150

Merged
Vivswan merged 5 commits into
mainfrom
feat/npm-publish
Sep 12, 2026
Merged

Vivswan merged 5 commits into
mainfrom
feat/npm-publish

Conversation

@Vivswan

@Vivswan Vivswan commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Two publish paths, one package:

green push to main   ->  @vivswan/github-settings-as-code@2.0.1-main.412.gb8df084   (npm dist-tag next)
release v2.1.0       ->  @vivswan/github-settings-as-code@2.1.0                     (npm dist-tag latest)
  • Every packaged commit on the build branch now carries lib/pkg/ (the library build) beside lib/index.js, so npm install github:Vivswan/github-settings-as-code#<packaged sha> works with no build step.
  • post-green.yml gains publish-next: each green main commit's library build goes to npm under the next dist-tag through trusted publishing (OIDC). No registry token exists anywhere.
  • update-release.yml gains publish-npm: the release's build goes to npm as the stable version, behind the ref verification.
  • Neither dist-tag ever moves backward: the new npm-verdict subcommand reads the registry and skips a version already there, a stale retry after newer runs published, and a rerun of an older release after a newer one.
  • Until the managed ci.yml grants its post-green call id-token: write, publish-next warns and skips instead of failing the hook.
  • docs/reference/library.md now carries the three install forms, the versioning rules, and the owner's one-time setup (bootstrap publish, trusted publisher on npmjs.com, tokens disallowed).
Technical details

Packaged paths. release-pipeline.ts replaces the single BUNDLE_FILE with PACKAGED_PATHS = ["lib/index.js", "lib/pkg/"]; a directory entry stages every file under it. The tree-identity check rebuilds the expected tree from a packaged commit's own entries under those paths, so the chain commits minted before this change (bundle only) stay valid parents and valid latest targets. What this run mints, and what the end-of-pipeline confirmation blesses, must carry both lib/index.js and lib/pkg/index.js as non-empty regular files; a rerun's tag verification compares the whole file set and every file's bytes with the fresh build.

Versions. prerelease-version prints the manifest's next patch plus -main.<run number>.g<sha7>. The g prefix keeps npm from rewriting an all-digit sha7 with a leading zero (npm reads 0123456 as the number 123456). npm-verdict <next|stable> fetches the packument (404 is an unpublished package; any other non-2xx stops the job rather than publishing blind) and prints publish <version> or skip <reason>; the workflow steps branch on that.

Provenance. npm publish runs with GITHUB_SHA set to the release source, because a newer push's run can recover an older release and npm's provenance names the commit in GITHUB_SHA.

Ceiling dependency. A called job asking for more than its caller's ceiling fails the whole call, and the managed ci.yml grants the post-green call contents: write alone, so publish-next declares no permissions and probes ACTIONS_ID_TOKEN_REQUEST_URL instead: absent means no id-token: write arrived by inheritance, and the job warns naming the missing grant and skips every later step. The stable job's ceiling (contents: read, id-token: write) sits inside the caller's existing update-release grant.

No semver dependency. The script stays Node-builtins-only: the anchor and boundary-check jobs run it with no install step.

Tests. test/scripts/release-pipeline.test.ts routes every fixture build through the three built files, pins the widened diff, the legacy-tip transition, the per-file drift and extra-file refusals, the version ordering, both verdict tables, the registry adapter over a local server, and the subcommands' stdout. test/docs/apply-settings-workflow.test.ts pins the whole publish-next job; the new test/docs/npm-publish-workflows.test.ts pins the cross-workflow invariants of both publish jobs (OIDC only, no token inputs, same guard and build text, dist-tag per channel, source sha to provenance) and runs the probe, the npm floor guard, and both publish blocks under bash against stubs.

Reviewer note (Copilot): report a finding only with a concrete input and the wrong output it produces.

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

File size check

2 over a hard cap (fails), 8 warning(s).

File Size Tier Cap
.github/scripts/release-pipeline.ts:362 262 chars hard 256
test/scripts/release-pipeline.test.ts:716 265 chars hard 256
.github/workflows/post-green.yml:54 205 chars warn 150
.github/workflows/post-green.yml:55 193 chars warn 150
.github/workflows/post-green.yml:110 179 chars warn 150
.github/workflows/post-green.yml:76 19 comment lines warn 10
test/docs/npm-publish-workflows.test.ts:88 176 chars warn 150
test/docs/post-green-workflow.test.ts:1 158 chars warn 150
test/docs/post-green-workflow.test.ts:69 151 chars warn 150
test/scripts/release-pipeline.test.ts 3180 lines warn 2560

Split the file, wrap the line, shorten or exempt the comment, or list the path in .file-size-allow.local with a # reason.

5 managed file(s) skipped; repo-platform owns them.

@Vivswan
Vivswan force-pushed the feat/library-package branch from 11757a2 to d45b6fa Compare September 12, 2026 01:20
@Vivswan
Vivswan force-pushed the feat/library-package branch from d45b6fa to a3d764a Compare September 12, 2026 01:39
@Vivswan
Vivswan force-pushed the feat/library-package branch from a18dc70 to 379c9b1 Compare September 12, 2026 01:54
@Vivswan
Vivswan force-pushed the feat/npm-publish branch 2 times, most recently from 8f45a24 to f4e1ef0 Compare September 12, 2026 02:20
@Vivswan Vivswan closed this Sep 12, 2026
@Vivswan Vivswan reopened this Sep 12, 2026
@Vivswan
Vivswan force-pushed the feat/library-package branch from 379c9b1 to 6ea8aa1 Compare September 12, 2026 03:28
Base automatically changed from feat/library-package to main September 12, 2026 04:23
Copilot AI balanced review requested due to automatic review settings September 12, 2026 05:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Legacy package validation can accept an unstripped manifest, and the bootstrap instructions incorrectly assume npm creates the latest tag.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds packaged library artifacts and OIDC-based npm publishing for prerelease and stable channels.

Changes:

  • Packages lib/pkg/ alongside the action bundle.
  • Adds serialized npm publishing with version safeguards.
  • Documents publishing, installation, and provenance.
File summaries
File Description
.github/actionlint.yaml Allows the new concurrency queue syntax.
.github/scripts/release-pipeline.ts Adds package assembly and npm version decisions.
.github/workflows/post-green.yml Publishes green builds under next.
.github/workflows/update-release.yml Publishes stable releases to npm.
.github/SECURITY.md Documents npm provenance.
AGENTS.md Updates release guidance.
README.md Advertises npm installation.
docs/reference/library.md Documents installation and publishing setup.
docs/upgrading/README.md Describes packaged library artifacts.
test/docs/npm-publish-workflows.test.ts Tests publishing workflow contracts.
test/docs/post-green-workflow.test.ts Covers the new prerelease job.
test/scripts/release-pipeline.test.ts Tests packaging and npm decisions.
Review details

Suppressed comments (1)

docs/reference/library.md:177

  • This bootstrap does not create the promised latest dist-tag. On an empty package, npm publish --tag next creates only next, so plain npm install @vivswan/github-settings-as-code fails until the first stable release instead of installing the bootstrap pre-release described above. Explicitly assign latest after the first publish, or remove that promise from the documentation.
npm version "$version" --no-git-tag-version && npm publish --access public --tag next
  • Files reviewed: 12/12 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/scripts/release-pipeline.ts Resolved
Comment thread docs/reference/library.md Resolved
Copilot AI review requested due to automatic review settings September 12, 2026 05:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Packaged-tree validation can bless an unstripped modern manifest, and bootstrap and historical-tag documentation is inaccurate.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

.github/scripts/release-pipeline.ts:318

  • A current-shaped packaged commit that retains scripts.prepare is accepted as a legacy commit. For example, a commit with lib/index.js, lib/pkg/index.js, and the source package.json satisfies the second tree comparison, so verify can bless it even though a github: install will run prepare. Restrict the source-manifest fallback to the old bundle-only shape.
  // The chain commits minted before the prepare strip carry the source's package.json as is; they stay valid parents
  // and latest targets.
  if (actual !== expected && actual !== treePlusBundle(cwd, source, stage, "source")) {
  • Files reviewed: 12/12 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread docs/reference/library.md Resolved
Comment thread docs/reference/library.md Resolved
Comment thread docs/upgrading/README.md Resolved
Copilot AI review requested due to automatic review settings September 12, 2026 05:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Packaged-tree validation permits an unstripped prepare script, and the bootstrap procedure does not create the promised latest tag.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

docs/reference/library.md:177

  • This bootstrap publishes only the next dist-tag, so an unqualified install fails until the first stable release. npm publish --tag next adds the selected tag only; it does not also create latest for a new package. Explicitly assign latest to the bootstrap version before tokens are disabled.
npm version "$version" --no-git-tag-version && npm publish --access public --tag next

docs/reference/library.md:157

  • npm does not automatically create latest when the first publish explicitly uses --tag next. With the bootstrap command below, the packument contains next only, so this explanation is incorrect even though latest can be added explicitly after publishing.
| `latest` | Every release cut | The released version: `2.1.0`. Until the first release it names the bootstrap pre-release, which npm tags `latest` as a package's first publish | `npm install @vivswan/github-settings-as-code` |
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread .github/scripts/release-pipeline.ts Resolved
Copilot AI review requested due to automatic review settings September 12, 2026 06:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The bootstrap command does not create the promised latest tag, and the direct managed-file edit will be overwritten.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

AGENTS.md:48

  • This edit will be overwritten by the next repo-platform sync. The file header marks lines inside the managed region as generated, so the new npm pipeline guidance will disappear even though repository behavior now depends on it. Make this change in Vivswan/repo-platform and bring it here through the sync instead.

docs/reference/library.md:177

  • The bootstrap command creates only the next dist-tag, so an unqualified install fails before the first stable release. After this command, the registry has next: <version> but no latest; npm install @vivswan/github-settings-as-code therefore cannot resolve the bootstrap version promised above. Add latest explicitly after publishing.
npm version "$version" --no-git-tag-version && npm publish --access public --tag next
  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings September 12, 2026 06:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The documentation incorrectly says the next bootstrap publish also creates the latest dist-tag.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

docs/reference/library.md:166

  • The bootstrap command below does not create latest. npm documents that publishing sets latest unless --tag is used, so npm publish --tag next leaves only next; the plain install in this row is unavailable until the first stable release.
| `latest` | Every release cut | The released version: `2.1.0`. Until the first release it names the bootstrap pre-release, which npm tags `latest` as a package's first publish | `npm install @vivswan/github-settings-as-code` |
  • Files reviewed: 12/12 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread .github/scripts/release-pipeline.ts Resolved
Comment thread README.md Resolved
Copilot AI review requested due to automatic review settings September 12, 2026 06:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The setup guide omits enabling direct npm publishing, causing newly configured trusted publishers to reject both jobs.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

docs/reference/library.md:199

  • The one-time setup omits the permission both CI jobs need to call npm publish. For a trusted publisher created with npm's post-September 3 defaults, only npm stage publish is allowed, so following these steps makes both publish-next and publish-npm fail authentication instead of publishing. Tell the owner to enable direct npm publish in the publisher's Allowed actions.
  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

…lish to npm

Every chain commit on the build branch carries lib/pkg/ (the tsdown library build) beside lib/index.js, minus package.json's prepare script (npm's git fetcher would run it on a github: install), so a packaged sha installs as an npm package straight from GitHub. The two release hooks publish the library through npm trusted publishing: post-green.yml's publish-next publishes each green main commit's build under the next dist-tag as the manifest's next patch plus -main.<run number>.g<sha7>; update-release.yml's publish-npm publishes the release's build as the stable version behind the ref verification. No registry token exists anywhere; npm attaches provenance.

release-pipeline.ts replaces BUNDLE_FILE with PACKAGED_PATHS. The tree-identity check rebuilds from a packaged commit's own entries under those paths, so the bundle-only chain commits minted before this change stay valid parents and latest targets, while what a run mints and what the confirmation blesses must carry lib/index.js and lib/pkg/index.js. Two subcommands feed the workflows: prerelease-version names the pre-release, and npm-verdict reads the registry's packument (404 is an unpublished package; any other answer, or a body that is not a packument, stops the job) and prints publish or skip: a version already there, a stale retry after newer runs published, or an older release rerun after a newer release never moves next or latest backward; a pre-release latest, the bootstrap's, is taken over by the first release. Both publish jobs share a lossless lane (the managed ci.yml runs main pushes concurrently and the registry has no compare-and-set) and drop scripts.prepare before publishing; npm publish runs with GITHUB_SHA set to the release source so the SLSA predicate names the built commit.

publish-next declares no permissions (a called job asking beyond the caller's ceiling fails the call) and skips with a warning when the runner minted no OIDC token. The script stays Node-builtins-only, so the bump is a validated regex. The g prefix on the sha7 keeps npm from rewriting an all-digit sha7 with a leading zero. docs/reference/library.md carries the install forms and the versioning rules for both channels.
…writing the checkout's config

configureIdentity wrote user.name and user.email into the checkout's repository config, which outlives the run: run once against a real checkout, it stamped every later commit made from that repository and from every worktree sharing its config as the release bot (this is what happened locally). The identity now travels as GIT_AUTHOR_* and GIT_COMMITTER_* in the environment of the two git invocations that mint commits (commit-tree for chain commits, commit for the anchor); the major tag is lightweight and carries none. Tests pin the minted commits' author and committer and that a fixture clone's own config is untouched after each command; the config-writing form fails them.
…est strip, which now covers pacote's preparation scripts

The pre-strip manifest shape was accepted for any packaged commit, so a hand-planted commit carrying lib/pkg/ beside the source's unchanged package.json passed the tree check and a github: install of it would run prepare. Only the bundle-only chain commits predate the strip, so the fallback now applies to commits with no lib/pkg/ entry; the refusal names kept preparation scripts, which a path diff against the source cannot show.

The strip itself widens from scripts.prepare to every script npm's git fetcher (pacote) takes as a reason to run npm install --include=dev and a prepare step in a github: dependency's checkout: prepare, prepack, build, and the install hooks. With prepare alone removed the repository's build script still triggered the dev install. The docs place the tags up to v2.0.0 where they are, on release commits on main, not on packaged commits.
…atest dist-tag

npm's documentation says publishing sets latest unless --tag is used and is silent on a package's first publish, so neither "latest stays unset" nor "the registry assigns latest" has a primary source. The setup step now tells the owner to read npm dist-tag ls after the bootstrap and gives the plain-install behaviour for each state; the README line, the Versioning row, and the stable verdict's comment say the same. The stable publish job handles both states: it yields only to a newer release.
… definition, and pin every preparation trigger

A packument's dist-tags are an object with at least one key, latest (npm/registry REGISTRY-API.md), so a first publish carries latest whatever --tag asked for; the docs, the setup step, and the stable verdict's comment say so with that citation, replacing the two-outcome hedge. The bootstrap step gains the dist-tag check and drops the prepare script from the manifest as both CI publishers do.

PREPARATION_SCRIPTS is exported and the test fixture carries one of each trigger, so dropping any of the six from the list fails the suite; the planter predicate is named for what it decides. AGENTS.md's bundle sentence places the release's packaged commit on the build chain (a child of the previous tip or a root), never as a child of the release commit.
Copilot AI review requested due to automatic review settings September 12, 2026 07:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The supply-chain, immutable-tag, and registry-publishing changes warrant final human validation despite comprehensive automated coverage.

Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@Vivswan
Vivswan marked this pull request as ready for review September 12, 2026 07:25
@Vivswan
Vivswan merged commit 8eb5897 into main Sep 12, 2026
38 checks passed
@Vivswan
Vivswan deleted the feat/npm-publish branch September 12, 2026 07:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants