zx-semrel
July 23, 2026 Β· View on GitHub
A zx-based release script β a semantic-release alternative (PoC)
Sometimes bloody enterprise forbids you from using any third-party solutions for sensitive operations (like release, deploy, and so on). Good old copy-paste comes to the rescue!
Btw, here's an adaptation for monorepos: zx-bulk-release
Requirements
- macOS / linux
- Node.js >= 14.13.1
- git >= 2.0
- zx >= 1.6.0
Key features
- Zero dependencies
- Zero configuration
- Pretty fast
- Tiny, less than 140 lines with comments
- Reliability, safety, simplicity and maintainability (sarcasm)
Functionality
- Poor conventional commits analysis
CHANGELOG.mdgenerationpackage.jsonversion bumping- Git release commit creation (optionally SSH-signed)
- GitHub Release
- Package publishing to both npmjs and gh registries
π Usage
- Copy
- Tweak it, inject tokens, etc.
- Run
curl https://raw.githubusercontent.com/semrel-extra/zx-semrel/master/release.mjs > ./release.mjs
zx ./release.mjs
or like this if zx is not installed:
# Just replace GIT* env values with your own
GIT_COMMITTER_NAME=antongolub GIT_COMMITTER_EMAIL=mailbox@antongolub.ru GITHUB_TOKEN=token npx zx ./release.mjs
or just run it without any edits through npx:
# Cross your fingers for luck
GIT_COMMITTER_NAME=antongolub GIT_COMMITTER_EMAIL=mailbox@antongolub.ru GITHUB_TOKEN=token npx zx-semrel
See also the gh-actions usage example
Environment variables
Config is entirely env-driven. Booleans are on for any non-empty value. Only a GitHub token is strictly required β in GitHub Actions with id-token: write, npm OIDC self-enables, so a token pair is often the whole config.
Auth
| Variable | Required | Default | Controls |
|---|---|---|---|
GITHUB_TOKEN / GH_TOKEN | yes | β | GitHub API (release), commit push, GitHub Packages auth |
NPM_TOKEN | one of | β | npm auth β legacy token mode |
NPM_OIDC | one of | auto in Actions | npm OIDC trusted publishing instead of a token |
Publishing needs one of NPM_TOKEN / NPM_OIDC; in GitHub Actions with id-token: write OIDC self-enables (via the runner-provided ACTIONS_ID_TOKEN_REQUEST_URL).
Git commit / tag
| Variable | Required | Default | Controls |
|---|---|---|---|
GIT_COMMITTER_NAME | no | Semrel Extra Bot | Committer name |
GIT_COMMITTER_EMAIL | no | semrel-extra-bot@hotmail.com | Committer email β match the signing account for Verified |
GIT_SIGN_KEY | no | β | SSH private key β sign the release commit & tag |
GIT_BRANCH | no | current β master | Branch the release is pushed to |
GH_USER | no | β | Username in the token push URL (e.g. x-access-token) |
npm publish β skipped entirely when package.json has "private": true
| Variable | Required | Default | Controls |
|---|---|---|---|
PKG_ALIAS | no | pkg alias field | Extra name to also publish under |
NPM_PROVENANCE | no | on with OIDC | Force --provenance |
Behaviour
| Variable | Required | Default | Controls |
|---|---|---|---|
PUSH_MAJOR_TAG | no | off | Also move & force-push the major tag (v1) |
DRY_RUN | no | off | Stop before commit/push/publish β same as --dry-run |
DEBUG | no | off | Extra logging β same as --debug |
VERBOSE | no | off | Verbose shell output |
npm publishing: OIDC vs legacy tokens
Since npm revoked classic tokens, the recommended way to publish from CI/CD is OIDC Trusted Publishing.
OIDC mode (priority) β set NPM_OIDC=true or omit NPM_TOKEN in a GitHub Actions environment with id-token: write permission. The npm CLI obtains a short-lived credential automatically; --provenance is enforced.
Legacy mode β provide NPM_TOKEN (granular access token, 90-day max lifetime). Used as fallback when NPM_OIDC is not set.
Auto-detection: if NPM_OIDC is not set and NPM_TOKEN is absent, OIDC is used automatically when ACTIONS_ID_TOKEN_REQUEST_URL is available (GitHub Actions with id-token: write).
OIDC limitations
- First publish of a package cannot use OIDC β the initial version must be published with a token or locally, then configure trusted publishing on npmjs.com
- Each package supports one trusted publisher at a time β configure it per package (and per alias) at npmjs.com β Settings β Trusted publishing
- The workflow filename in trusted publisher config must match exactly (case-sensitive,
.ymlvs.yaml) - Requires npm >= 11.5.1 and Node.js >= 22.14.0
- An existing project
.npmrcwith an_authTokenforregistry.npmjs.orgwill override OIDC β remove it to use trusted publishing - OIDC applies to npmjs.org only; GitHub Packages still uses
GITHUB_TOKEN/GH_TOKEN
Signed release commits (opt-in)
Branch rulesets with Require signed commits reject the release commit, because zx-semrel creates it with a plain, unsigned local git commit. Set GIT_SIGN_KEY to sign the release commit and tag with SSH.
GIT_SIGN_KEY β the private SSH key of the committer identity (the full multi-line key, e.g. the contents of an id_ed25519 file). When set, zx-semrel writes it to a temporary 0600 file and enables SSH signing via local git config only (gpg.format ssh, user.signingkey, commit.gpgsign, tag.gpgsign) β it never touches your global git config. When unset, behaviour is unchanged (unsigned commit).
# .github/workflows/release.yml
env:
GIT_COMMITTER_NAME: Semrel Extra Bot
GIT_COMMITTER_EMAIL: bot@example.com # must be a verified email on the account below
GIT_SIGN_KEY: ${{ secrets.GIT_SIGN_KEY }} # PEM-format private key, incl. the BEGIN/END lines
For GitHub to show Verified (rather than Unverified):
- Add the matching public key to the committing account at Settings β SSH and GPG keys β New SSH key, choosing Key type: Signing Key (not the default Authentication Key).
- That account must have a verified email equal to
GIT_COMMITTER_EMAILβ GitHub matches the signature to the identity by the committer email.
Notes:
- SSH format only for now; GPG signing is out of scope.
- Generate a dedicated key, e.g.
ssh-keygen -t ed25519 -C bot@example.com -f ./sign_key, store the private half as theGIT_SIGN_KEYsecret, and registersign_key.pubas a Signing Key. - The bot account still needs push access to the protected branch (rulesets apply to everyone unless bypassed).
π οΈ Extras
π License
π Refs
- npm Trusted Publishing docs
- npm classic tokens revoked
- Actually you donβt need 'semantic-release' for semantic release
- stackoverflow.com/github-oauth2-token-how-to-restrict-access-to-read-a-single-private-repo
- npmjs.com/using-private-packages-in-a-ci-cd-workflow
- https://github.com/cli/cli/issues/1425
- https://gist.github.com/Kovrinic/ea5e7123ab5c97d451804ea222ecd78a
- https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token
- https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/
- Not invented here