Skip to content

Drop main, module, browser, and types from package.json, publish on version tags, and harden workflows - #4211

Merged
nstepien merged 11 commits into
mainfrom
publish-on-tag
Oct 19, 2026
Merged

nstepien merged 11 commits into
mainfrom
publish-on-tag

Conversation

@nstepien

@nstepien nstepien commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

The Publish workflow ran npm version itself but could only push the tag, as the default branch ruleset blocks its push to main, so the 7.0.0-beta.61 version commit never landed on main. Now npm version runs locally, and pushing the version commit and its tag runs the workflow.

How to release

vp run version <prerelease|patch|minor|major|x.y.z>
git push --atomic --no-follow-tags origin main "v$(npm pkg get version)"
  • The version task in vite.config.ts runs git switch main, git fetch --prune --prune-tags origin, which also deletes local tags that aren't on GitHub, and git reset --keep origin/main, which moves main to GitHub's and keeps uncommitted changes, which npm version then refuses to run with. It stops at the first failure.
  • Versioning: vp run appends the arguments to the task's last command, npm version, which commits the new version and creates an annotated v<version> tag. During the beta, use prerelease to go from 7.0.0-beta.61 to 7.0.0-beta.62, as patch, minor, and major all produce 7.0.0.
  • Pushing: the push stays a separate command, as Vite Task doesn't run commands through a shell on Windows, so $(...) doesn't work in a task. Naming the tag makes the push fail if the tag is missing, and --no-follow-tags leaves other local tags out, even with push.followTags set. --atomic pushes main and the tag together, or neither. With --follow-tags, a missing tag only pushes main, so nothing is published.
  • A missing tag: until it's pushed, any fetch that prunes tags deletes the new tag, for example an editor's auto-fetch with fetch.pruneTags enabled. If the push can't find the tag, recreate it, then push again:
    git tag --annotate "v$(npm pkg get version)" --message "$(npm pkg get version)"

Only nstepien and amanmahajan7 can push to main without a pull request and create tags, so only they can release (see Repository settings). The same steps are in the header of publish.yml. vpr version is short for vp run version, and the commands work in both Bash and PowerShell.

Changes

  • Publish runs on v* tags instead of workflow_dispatch. Before installing anything, it checks that the tagged commit is on main and that the tag matches the version from npm pkg get version. Tags can't be pushed twice, and npm publish rejects a version that already exists.
  • Publish is split into three jobs. In a job with id-token: write, every step can request an OIDC token, so any dev dependency that vp install or vp run ci ran could have published to npm as react-data-grid.
    • The read-only build job runs the checks and CI, and uploads lib/.
    • The publish job (id-token: write) downloads lib/ and publishes it without installing the project's dependencies. It still sets up Vite+, so npm is the version that devEngines asks for, and setup-vp downloads the Vite+ install script when it runs. npm publish --ignore-scripts skips the prepare script (vp config), so no project code runs either.
    • The release job (contents: write) creates the GitHub release with gh release create --verify-tag, which fails instead of creating a tag if the tag is missing. As a separate job, the job that can publish to npm has no write token, and a failed release can be re-run on its own once the version is already on npm.
    • Checkouts no longer persist credentials, as nothing is pushed.
  • Update Screenshots is split into three jobs, for the same reason:
    • remove-label (pull-requests: write) only runs gh, without checking out the repository.
    • screenshots (read-only) installs the dependencies, regenerates the screenshots, and uploads them if they changed.
    • push (contents: write) checks out the commit the screenshots were taken on, swaps in the new ones, commits, and pushes. The push fails if the branch moved since, as before. When nothing changed, this job is skipped, so no job with write access runs.
    • Only PNGs under test/**/screenshots/, at any depth, are handed from screenshots to push, so the job that pushes can only commit images.
  • publint: vp pack now lints the tarball that npm pack --ignore-scripts produces, in strict mode, so warnings fail the build like errors do. It reports no issues.
  • Breaking: main, module, browser, and types are removed from package.json, as exports covers every entry point. publint suggested replacing browser with an exports condition. Tools that ignore exports no longer resolve the package, like TypeScript's node10 module resolution (removed in TypeScript 7), webpack 4, and Jest before 28.
  • package.json metadata: repository uses the github:Comcast/react-data-grid shorthand, and homepage and bugs are removed, as npm derives all three when publishing. publishConfig.provenance is enabled, which trusted publishing already does, so publishing from outside CI fails by default (npm publish --no-provenance overrides it). npm publish logs that it converted the repository string to an object, which is expected.
  • skipLibCheck is removed from tsconfig.vite.json and tsconfig.website.json.
  • AGENTS.md documents the workflow conventions, publint, and where to find the release steps.

The Publish workflow's file name is unchanged, so npm's trusted publisher config still matches.

Repository settings

These changed in the repository settings alongside this PR, so they're not in the diff.

  • Actions: the default GITHUB_TOKEN is now read-only, and Actions can no longer create or approve pull requests. Every workflow sets its own permissions, so this doesn't affect them, only a workflow that would omit them. "Require actions to be pinned to a full-length commit SHA" was already on.
  • Rulesets:
    • Default branch requires a pull request with a code owner approval, and the test and CodeQL checks. Only nstepien and amanmahajan7 can bypass it, to push the version commit to main. The Admin role's bypass for pull requests is removed, so other admins need an approval and passing checks to merge.
    • Default branch history is new, with no bypass: nobody can delete main, force-push it, or add merge commits. These rules moved out of "Default branch", whose bypass would otherwise cover them.
    • Tag creation is new: only nstepien and amanmahajan7 can create tags, so only they can start a release.
    • Tags no longer has a bypass: nobody can delete, move, or force-push a tag.
    • gh-pages is unchanged, and the branch is kept as an archive, as Pages deploys from the CI workflow since Adopt Vite+ #4028.

Verification

  • vp run ci passes, and vp run typecheck passes without the cache.
  • With a local bare remote, npm version created an annotated tag and git push --atomic --no-follow-tags origin main "v$(npm pkg get version)" pushed both, without another local tag on main, which --follow-tags would have pushed too. With push.followTags set, the same push without --no-follow-tags sent that other tag as well. After a fetch with fetch.pruneTags deleted the new tag, the push failed and pushed nothing, while --follow-tags pushed main alone. The recreated tag matched the one npm version creates. The checks passed for the new tag, and failed for a tag that doesn't match package.json and a tag off main. With npm 12, npm pkg get version prints the bare version.
  • In a scratch repository with a stand-in remote, vp run version prerelease switched to main, deleted a local-only tag, reset main, and created the 7.0.0-beta.62 commit and tag, which the push then sent. The task stopped before npm version when git reset --keep refused to overwrite a local change to a file that GitHub's main had changed, and npm version refused to run with an uncommitted change. A pre-commit hook running a cached vp run task worked inside the task.
  • In npm 12's @npmcli/package-json, which npm publish uses to read package.json, the repository shorthand is normalized to git+https://github.com/Comcast/react-data-grid.git, the URL that provenance is verified against, and homepage and bugs are derived from it.
  • In a fresh clone with only lib/ and no node_modules, npm publish --dry-run --ignore-scripts packed the 4 lib/ files plus LICENSE, README.md, and package.json. Without --ignore-scripts, it ran prepare.
  • With a local bare remote, the steps of the push job committed a modified, a deleted, and an added screenshot, including in a directory two levels below test/, and the push was rejected as non-fast-forward when the PR branch had moved after the screenshots were taken. gh auth setup-git hands GH_TOKEN to git. With the *.png pattern, a planted test/browser/screenshots/x.test.tsx/evil.test.tsx was neither detected as a change nor staged, while a new PNG in a new directory was.
  • In gh 2.102.0's source, gh release create --repo skips its local git lookups, and --verify-tag checks the tag through the API, so the release job doesn't need a checkout.
  • With planted package.json problems, a missing file (an error) and an invalid repository (a warning) each fail vp pack, including in a local run outside CI.
  • A one-off zizmor run, not added to the repository, only reports Dependabot's cooldown.

The next release will be Publish's first run. Update Screenshots runs a PR's own workflow file, and its remove-label and screenshots jobs passed on this PR. push was skipped, as no screenshot changed, so its first run will be the next label that changes screenshots.

🤖 Generated with Claude Code

nstepien and others added 4 commits October 19, 2026 05:37
…publint

- Publish runs when a `v*` tag is pushed, after admins run `npm version`
  and push the commit and tag together, so version commits land on `main`.
  It checks that the tag matches `package.json`, is on `main`, and isn't
  published yet.
- Publish builds in a read-only job and publishes `lib/` from a separate
  job, so no dependency code runs with `id-token: write`.
- Update Screenshots regenerates screenshots in a read-only job, and
  removes the label and pushes from separate jobs that run no dependencies.
- `vp pack` lints the packed package with publint.
- Remove `skipLibCheck` from `tsconfig.vite.json` and `tsconfig.website.json`.
- Document the workflow conventions and the release steps in AGENTS.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`exports` covers every entry point, and publint suggested replacing
`browser` with an `exports` condition. Tools that ignore `exports`,
like TypeScript's `node10` module resolution, webpack 4, and Jest
before 28, no longer resolve the package.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nstepien and others added 2 commits October 19, 2026 16:10
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`rm -r test/**/screenshots/**` only reached screenshot directories one level
under `test/`, as bash's `globstar` is off. Tracked screenshots are now
deleted with `git ls-files`, and a single `SCREENSHOTS` pattern is shared by
the git commands and the upload.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Check that the tag is on main and matches the package version in
  separate steps, and drop the npm check: tags can't be pushed twice,
  and `npm publish` rejects an existing version.
- List `prerelease` in the release steps, as `patch`, `minor`, and
  `major` all turn a beta into a stable release.
- Use the `github:` shorthand for `repository`, and drop `homepage` and
  `bugs`, which npm derives from it when publishing.
- Enable provenance in `publishConfig`, so publishing outside of CI fails.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@nstepien
nstepien marked this pull request as ready for review October 19, 2026 16:53
@nstepien nstepien changed the title Publish on version tags, isolate write permissions in workflows, add publint Drop main, module, browser, and types from package.json, publish on version tags, and harden workflows Oct 19, 2026
nstepien and others added 4 commits October 19, 2026 18:47
- Publish: create the GitHub release in its own job, so the job with
  `id-token: write` has no write token, and a failed release can be
  re-run on its own once the version is already on npm.
- Publish: read the version with `npm pkg get version`.
- Update Screenshots: only match PNGs, so the job that pushes can only
  commit images, not files the read-only job could have planted.
- AGENTS.md: the write jobs never install the project's dependencies,
  which is what they guarantee, as the publish job still sets up Vite+.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Start from GitHub's `main` and tags, and push the version commit with the
tag named explicitly. With `--follow-tags`, a tag that a fetch pruned
before the push left only `main` to push, so nothing was published; now
the push fails instead, and other local tags stay out of it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With `push.followTags` set, naming the tag isn't enough to keep other
local annotated tags reachable from `main` out of the release push.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`vp run version <prerelease|patch|minor|major|x.y.z>` switches to `main`,
matches it and the local tags to GitHub's, then runs `npm version` with
the arguments, as `vp run` appends them to the last command. The push
stays a separate command, as Vite Task doesn't run commands through a
shell on Windows, so the tag name can't come from `$(...)` in a task.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@nstepien
nstepien merged commit 3f8a93e into main Oct 19, 2026
5 checks passed
@nstepien
nstepien deleted the publish-on-tag branch October 19, 2026 20:24
Comment thread package.json
Comment on lines -24 to 33
"main": "./lib/index.js",
"module": "./lib/index.js",
"browser": "./lib/index.js",
"types": "./lib/index.d.ts",
"exports": {
"./lib/styles.css": "./lib/styles.css",
".": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

image

Maybe not ideal with the default settings, would need to have a deeper look

@amanmahajan7

Copy link
Copy Markdown
Collaborator

Need a simple summary to understand the change and benefit 😄

@nstepien

Copy link
Copy Markdown
Collaborator Author

a simple summary

Well... it's more secure now!
Also we won't forget to merge release commits onto main.
To release a new version, just check the docs at the top of publish.yml

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.

2 participants