Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
v3.0.0 is a rewrite of the action. It is a composite action that runs on the runner, on Linux, macOS and Windows, with Node.js 24 by default. Its modes are commit (the default, the original behavior), check, pr and build. Your build runs without the write token. v2 keeps working through the v2 tag. The v2 input names still work in v3, with a warning. Read the migration guide before you change @v2 to @v3.
Breaking changes from v2
- The default Node.js version is 24, and
node-versionhas an effect. - The action does not guess the build directory. Set
paths, which isdistby default. - A detached HEAD (a tag push or a
pull_requestevent) is an error in modecommit. - The pull request branch is fixed (
auto-build/<branch>), so a pull request is updated and not opened again. - The action runs on the host and not in an Alpine container.
build-onlyismode: buildandcreate-prismode: pr. The old names work for now, with a warning.- Two lockfiles of different package managers are an error, and a checkout that kept its credentials is refused in modes
commitandpr. - v2 support ends with this release (see
SECURITY.md).
Added
mode: pr, one pull request kept up to date (issue #5). For a base branch that does not accept direct pushes. After the build the action stages thepaths, makes one commit on top of the checked out branch (withgit commit-tree, so the base branch and the working tree do not move) and force-pushes it to the bot branch, thebranchinput orauto-build/<base branch>. Then it opens a pull request to the base withpr-titleandpr-labels, or, when one is already open from that branch, sets the title again and adds the labels. A second run never opens another pull request, and the same build on the same base pushes nothing. No difference from the base means no push and no pull request. The outputspull-request-urlandpull-request-numbercome from this step. It uses theghCLI, which GitHub-hosted runners have, and not a third-party action.auto-mergerunsgh pr merge --autowith the first merge method that the repository allows (squash, merge, rebase). It fails the step, after the pull request and the outputs exist, with the repository setting to change when GitHub refuses.docs/testing.mddocuments what PR mode needs: the repository setting “Allow GitHub Actions to create and approve pull requests”,permissions: contents: writeandpull-requests: write, and that a pull request made withGITHUB_TOKENstarts no other workflows, so required checks need a personal access token or a GitHub App token.- Tests:
tests/pr.bats(a local bare remote and a fakegh), and the CI jobself-test-pr, which runs mode pr against a local bare remote with a stand-ingh. It never pushes to this repository and never opens a real pull request. mode: commit, the auto build (the default, issue #3). After the build, the action runsgit add --forceon eachpathsentry (so ignored build folders work), and when something is staged it commits withgit-user-nameandgit-user-emailand thecommit-message, then pushes to the checked out branch. When nothing changed it exits 0 without a commit and setschanged=false. The outputschangedandcommit-shacome from this step.- A notice when
commit-messagehas no[skip ci]: a push with a personal access token or a GitHub App token starts the workflow again and can loop. The message is never changed. - Composite action (
runs: using: composite), so it runs on Linux, macOS and Windows runners, on the host and not in an Alpine container. - The full v3 contract is declared: the inputs
mode,working-directory,command,install-command,package-manager,node-version,node-version-file,paths,token,commit-message,git-user-name,git-user-email,branch,pr-title,pr-labels,auto-merge,ignore-scriptsandupload-diff, and the outputschanged,stale,commit-sha,pull-request-urlandpull-request-number. The outputs of a mode that did not run are empty. - A real
node-version, default24, installed withactions/setup-node(pinned by commit SHA).node-version-fileis supported and wins overnode-version. ignore-scriptsinput: installs with--ignore-scripts. Yarn berry has no such flag, so the action setsYARN_ENABLE_SCRIPTS=falsefor that one command.- pnpm and yarn berry. pnpm runs
pnpm install --frozen-lockfile, yarn classic (1.x)yarn install --frozen-lockfileand yarn berry (2+)yarn install --immutable. Berry is detected from thepackageManagerfield, a.yarnrc.ymlfile, a berryyarn.lockoryarn --version. The build runs with the same manager (pnpm run,yarn run,npm run). - Corepack.
corepack enableruns when thepackageManagerfield names pnpm or yarn, or when the chosen pnpm or yarn is not installed. The action never runsnpm install -g. Without corepack, a manager already onPATHis used with a warning, and the step fails only when the manager is missing too. The manager is checked with--versionbefore the install. - Private registries. The new optional inputs
registry-urlandscope(default empty) go toactions/setup-node, which writes an.npmrcwith a${NODE_AUTH_TOKEN}placeholder. They are additions to the v3 contract of issue #10. The documented and tested contract: the install and the build inheritNODE_AUTH_TOKENand theNPM_CONFIG_*,YARN_*andPNPM_*variables of the job, a project.npmrcwith${NODE_AUTH_TOKEN}works,NODE_AUTH_TOKENis kept even when its value equals the GitHub token, while the GitHub token variables are removed (see the risk indocs/testing.md), and the scripts never write an.npmrc. Aregistry-urlwith credentials, a query, a fragment or a setting such as_authTokenin it is rejected, and anhttp://URL (other than localhost) is accepted with a warning that the token travels in clear text. - Tests:
tests/package-managers.batsandtests/registries.batswith fake package managers, a realnpm installagainst a local registry that needs a token, the fixturestests/fixtures/pnpm,yarn-classicandyarn-berry, and the CI jobself-test-pm. - bats tests (
tests/) with a fixture project, and CI jobsbatsandself-test. The self-test runs the action against the fixture on Ubuntu, macOS and Windows. .gitattributeskeeps LF line endings in shell scripts, so a Windows checkout does not break them.mode: check(issue #4): runs the build, then compares thepathswithHEADand fails the job when they differ. A modified file, a deleted file, a new file and a file that.gitignoreignores are all stale. A line ending difference alone (carriage returns,git diff --ignore-cr-at-eol, git 2.16 or newer) is not. The::error::names the working directory and up to 50 files (then “and N more”), and says to run<package manager> run <command>and commit the changed paths. A path that resolves outside the repository is refused. It only reads, never reads a token, so it works onpull_requestevents from forks.- Output
staleistrueorfalsein modecheck, and the step exits 1 when it istrue. Withupload-diff: truethe action uploadsdiff.patchandfiles.txtas the artifactnpm-auto-build-stale-diff(7 days), when the check found stale output.actions/upload-artifactis pinned by commit SHA. scripts/check.shworks without GitHub: it needs only bash, git and theNAB_PATHSvariable, and printsstale=trueorstale=falsewhenGITHUB_OUTPUTis not set, so Bitbucket Pipelines can use it with one line. Seedocs/testing.md.- The validate step has a
modeoutput, and the CI jobself-test-checkruns the action on the fixture, once fresh and once stale. docs/migration-v2-to-v3.md: the renamed inputs with what each warning says, the breaking changes, and how to stay on@v2.docs/recipes.md: protected branches, tag andpull_requestevents, matrix and monorepo, a private registry, pnpm and yarn, and a Bitbucket Pipelines step forscripts/check.sh.examples/rewritten for v3:complete-workflow.yml,monorepo.yml,protected-branch-pr.ymlandprotected-branch-token.yml. They replaceprotected-branches.ymland the v2 examples.- The examples cannot rot unnoticed.
tests/docs-examples.pyreads the YAML snippets of the README, the examples, the recipes and the other docs, and checks each one againstaction.yml: the@v3,@mainor SHA reference, only inputs that exist (and no v2 names), a validmode, the checkout withpersist-credentials: falseand the permissions that the mode needs. It runs intests/docs-examples.bats, with a test that adds a wrong input to a copy of the README and expects a failure, and in the CI jobdocs-examples. It does not run the action. A test also fails once av3tag exists while the docs still say that@v3is not released. - Release tooling (issue #9).
docs/releasing.mdis the release procedure: the checklist, the commands in order, who does each step and the rollback.scripts/release-check.shis a read only check that a release is ready. With--tagit checks the tagged commit instead..github/workflows/release.ymlruns only on a tagv3.x.y. It checks the tag, runs the tests and makes a source archive with a build provenance attestation. Then it creates the GitHub release as a draft, with the CHANGELOG section as the notes, and moves the floating tagv3.tests/release-check.batstests the script with each rule broken on purpose, andtests/architecture.batshas a list of maintainer tools for it.
Deprecated
- v2 input names are accepted as aliases with a warning that names the new input:
build-only(mode: build),create-pr(mode: pr),directory(working-directory),build-dir(paths) andgithub-token(token).github-tokenprints the warning and its value is used as the token of the commit and pr steps, where it wins overtoken, so a v2 workflow keeps pushing.
Changed
- The
workflowsjob of CI parses the YAML files with Ruby, which comes with the runner, and no longer installs PyYAML withpip. That closes the Scorecard alert Pinned-Dependencies (pipCommand not pinned by hash), andactions/setup-pythonis gone from the workflow. - The git helpers that mode commit and mode pr share (the credential header, the git version check, the branch and path checks) moved from
scripts/commit.shtoscripts/git-lib.sh. Nothing changed in how mode commit behaves. The detached HEAD and protected branch messages of mode commit now point tomode: prwithout saying it is not available. - A detached HEAD is an error in mode commit (a tag push, or a
pull_requestevent), with a message that namesmode: check, a branch push andmode: pr. There is no moreHEADused as a branch name. - The self-test of the mode gate runs an unknown mode (
deploy), now that every documented mode works. A new CI jobself-test-commitrunsmode: commitagainst a local bare remote.docs/testing.mdexplains the workflow requirements:persist-credentials: false,permissions: contents: writeand aconcurrencygroup. - The default Node.js version is 24 and
node-versionhas an effect now. - The scripts live in
scripts/(lib.sh,validate-inputs.sh,install.sh,build.sh), one responsibility each, withset -euo pipefail. The logic is no longer in the YAML. - A missing
pathsentry after the build is an error that lists the missing paths. - Package manager detection (
package-manager: auto). The order is thepackageManagerfield ofpackage.json, then the lockfile (pnpm-lock.yaml,yarn.lock,package-lock.jsonornpm-shrinkwrap.json), then npm without a lockfile (npm installand a warning). v2 always preferredpackage-lock.json. Now lockfiles of two different managers and nopackageManagerfield fail and name both files, instead of picking one. ApackageManagerfield ofbun, or a bun lockfile alone, fails with the list of supported managers. An explicitpackage-managerinput wins over everything.install-commandoverrides the install command, and the build still runs with the detected manager. - New warnings and errors: without corepack, a pnpm or yarn that is already on
PATHis used with a warning, and the step fails only when the manager is missing too. The build step fails with a message that namesinstall-commandwhen the manager is not onPATH. A missing lockfile warns. - pnpm and yarn without a lockfile install without the frozen flags and print a warning, like npm does, instead of failing on the missing lockfile.
npm testruns shellcheck and, when bats is installed,bats tests/. Dependabot no longer watches Docker.- The CI and housekeeping from #13 and the push fix of the Docker action are in no release: the Docker code they changed is removed here.
- README rewritten for v3 (issue #7). The premise first, the four modes with one example each, the inputs and outputs tables (checked against
action.ymlbytests/docs-examples.py), the permissions andpersist-credentials: falsethat modescommitandprneed, and the token model. The badges for CI, CodeQL and the OpenSSF Scorecard were added, and the license badge is labeled “License”. The release badge shows the latest tag. - Stale v2 text removed from the README,
docs/README.md,.github/ACTION_DESCRIPTION.mdand.github/copilot-instructions.md: the old Node.js version and checkout version in examples, the@v2references, the claim of fewer merge conflicts, the auto detected build directory, and the description of how the action runs. - The description of
action.yml, the text that the Marketplace shows, is shorter and says what the action does:Build your project and commit the output, so you do not commit dist/ by hand. Can also check it is current, or open a PR.(issue #9).
Removed
Dockerfileandentrypoint.sh, and the Docker only scriptstest.sh,test-build-only.sh,scripts/test-local.sh,scripts/test-debug.shandscripts/Dockerfile.debug. They stay available through thev2tag.- The guessing of the build directory (
dist,build,lib,public,out,.next).
Security
- The force-push is limited to a branch the action owns. It uses
--force-with-lease=<branch>:<tip it looked at>, never a plain force, so a push that races with a person or another run is refused and looked at again (three attempts). The destination is alwaysrefs/heads/<branch>. The branch is refused when it is the base branch, the default branch of the remote, a reserved name (main,master,develop,release/*and similar, in any case), or when it already exists and its tip commit has noGenerated-by: npm-auto-buildline, so a branch that a person made is never overwritten. A checked out branch that starts withauto-build/is refused, so a pull request branch cannot get a pull request of its own. The name is checked by the validate step, before the build. - The token in mode pr follows the rules of mode commit: only the pr step receives it,
git ls-remote,git fetchandgit pushget it as anhttp.extraheaderinGIT_CONFIG_*for that one command, andghgets it asGH_TOKENfor that one command. It is in no argument list, URL, git config, file or output, and install and build never see it. - The token is used only by the commit step (and the pr step in mode pr). It is removed from the step environment, masked together with its base64 form, and given to
git fetchandgit pushonly, as anhttp.extraheaderin theGIT_CONFIG_*variables of that one command. It is not in a command line (git -cwould show it inps), a URL,.git/config,git config --globalor a file. Tests check each of these places. Needs git 2.31 or newer.actions/checkoutshould usepersist-credentials: false, and the action warns when it finds credentials in.git/config. - Push safety. A rejected push is retried up to three times: the action fetches the branch, rebases its commit on top and pushes again. Mode commit never force-pushes. A rebase conflict is aborted and fails with a clear message. A protected branch fails with the two options that work: a personal access token or a GitHub App token that may bypass the protection, or
mode: pr. If a parallel run already pushed the same output, nothing is pushed andchanged=false. - Safe
pathsfor commit. An entry is resolved against the repository root.., the repository root,.git(and anything inside it) and a path outside the repository are refused. A name is never read as a glob or a pathspec magic. Anything already staged before the step is refused, so it cannot end up in the build commit. Repository hooks do not run. - The install and build steps run without the token. The token is not passed to them, and the scripts remove the known token variables (
GITHUB_TOKEN,GH_TOKEN,INPUT_TOKEN,INPUT_GITHUB_TOKEN,ACTIONS_RUNTIME_TOKENandACTIONS_ID_TOKEN_REQUEST_TOKEN) from their environment even when the workflow exports one. A test prints the environment of an install script and a build script and finds no token. - Input validation. Inputs reach the scripts only through environment variables. A
pathsentry that is empty, starts with-, contains..or is absolute is rejected, and so is aworking-directorythat leaves the workspace. Values with a line break, or longer than 1024 characters, are rejected. Spaces, quotes and$()are kept as text and never run. - Workflow checks for this repository, in
.github/workflows/workflow-security.yml:actionlint,zizmor, and a check that everyuses:is pinned to a commit SHA. - OpenSSF Scorecard (
.github/workflows/scorecard.yml) and a dependency review on pull requests (.github/workflows/dependency-review.yml). docs/threat-model.md: the assets, actors, trust boundaries and threats of the action, and the known v2 findings with the v3 fix for each.SECURITY.md: how to report a vulnerability, which versions are supported, and the response times.docs/architecture.mdandtests/architecture.bats(issue #11): the two trust zones, what each script may do, and tests that fail when a script outside the allowlist reads a token variable, runsgit pushorgit fetch, calls a network tool, or when arun:block holds a${{ }}expression.tests/failure-paths.batsadds tests for long values in every input, a leading dash, a fork pull request, a failed build and two runs at the same time. The last one found that a push that loses a ref lock race (cannot lock ref) was not retried, socommit.shnow treats it like a rejected push.- A checkout that kept its credentials is refused before the install in mode
commitand modepr(issue #26). The check runs invalidate-inputs.sh, reads only the key names ofhttp.*.extraheaderin the local git config, and tells you to setpersist-credentials: falseonactions/checkout. Modecheckand modebuildprint a warning. Before, the warning came after the build, when the token had already been readable. - Build provenance for the release (issue #9, audit finding F4). The release workflow attests the build provenance of the source archive of each release, and
gh attestation verifychecks it. Each job of the workflow has the permissions it needs and no more, and only the release job can write to the repository.
[2.1.2] - 2025-11-10
Fixed
- Enhanced .gitignore detection for build directories
- Build directories with trailing slash (e.g.,
dist/) are now properly detected - Multiple pattern checks:
dist,dist/, anddist/* - Fallback grep validation for direct .gitignore file check
- Ensures proper use of
git add --forcefor ignored directories
- Build directories with trailing slash (e.g.,
Technical Details
The action now performs comprehensive checks to detect if a build directory is in .gitignore:
git check-ignore $BUILD_DIR- Check exact directory namegit check-ignore $BUILD_DIR/- Check with trailing slashgit check-ignore $BUILD_DIR/*- Check files within directory- Direct grep in
.gitignorefile - Fallback validation
This fixes a critical issue where dist/ (with trailing slash) in .gitignore wasn’t being detected, causing the action to fail when attempting to commit build files.
Impact
Users who have their build directories in .gitignore (common best practice) will now have their builds properly committed by the action without errors.
[2.1.1] - 2025-11-04
Fixed
- CREATE_PR parameter debugging: Added comprehensive debug logging to identify why
create-pr: trueparameter wasn’t working properly - Enhanced troubleshooting: Added variable state logging and comparison result logging for better issue diagnosis
- Protected branch debugging: Improved debugging capabilities for protected branch configuration issues
Debug Features
- Added debug output showing
CREATE_PRparameter value at startup - Added comparison result logging before push/PR decision
- Enhanced error diagnostics for protected branch scenarios
[2.1.0] - 2025-11-04
Added
- Protected branch support: New
create-prparameter for handling protected branches - Automatic PR creation: Creates pull requests when direct push fails due to branch protection
- GitHub CLI integration: Installs and configures GitHub CLI for seamless PR creation
- Enhanced error handling: Detailed error messages with solutions for protected branch issues
Features
- New Input:
create-pr: true- Creates PR instead of direct push - Smart detection: Automatically detects protected branch push failures
- Unique branch naming: Creates timestamped branches for each build
- Comprehensive logging: Shows PR creation process and branch information
[2.0.2] - 2025-11-04
Fixed
- Git configuration error: Fixed “Author identity unknown” error when git user inputs are not provided
- Input validation: Added proper validation for git user name and email
- Default values: Added fallback default values for all input parameters
- Debug logging: Enhanced logging to show git configuration values for troubleshooting
[2.0.1] - 2025-11-04
Fixed
- Critical .gitignore handling: Build directories in .gitignore (like
dist/) are now properly committed using--forceflag - Enhanced logging: Added detailed git status logging to help diagnose commit issues
- Improved detection: Better detection of ignored directories and files
Technical Details
This fixes a critical issue where build files weren’t committed if the build directory was in .gitignore. This is common practice where developers ignore dist/ locally but want the action to commit official builds.
[2.0.0] - 2025-11-04
Note: This is the first fully functional release. Previous v1.x versions contained a critical Docker configuration issue that prevented execution.
Added
- 🚀 NPM Auto Build GitHub Action: Complete functional GitHub Action for automating npm build and commit workflows
- 🔧 Build-only mode: New
build-onlyinput parameter for testing builds without committing changes - 🐳 Docker-based execution: Containerized environment with Node.js 20, Git, and Bash
- 📁 Flexible build detection: Auto-detects build directories (dist/, build/, public/, out/, lib/)
- 📦 Package manager support: Automatic detection and support for both npm and yarn
- 📚 Comprehensive examples: Real-world usage patterns for different scenarios
- Complete CI/CD workflow with PR testing and main branch deployment
- Monorepo setup with matrix strategy for multiple packages
- Protected branches configuration with Personal Access Token
- 🧪 Local testing scripts: Enhanced development workflow with Docker-based test scripts
- 🏪 GitHub Marketplace compliance: Full compliance with marketplace requirements
- 📖 Complete documentation: Detailed README with troubleshooting and permission guides
Core Features
- Input parameters:
command: NPM script to build your package (default: “build”)directory: Directory containing package.json (default: ”.”)commit-message: Custom commit message (default: “chore: update build files”)build-dir: Build output directory (default: “dist”, auto-detected)github-token: GitHub token for authentication (required)git-user-name: Git user name (default: “github-actions[bot]”)git-user-email: Git user email (default: “github-actions[bot]@users.noreply.github.com”)node-version: Node.js version (default: “18”)build-only: Test mode without committing (default: “false”)
Technical Implementation
- Base image: node:20-alpine with bash, git, and npm/yarn
- Shell script: entrypoint.sh with comprehensive error handling and logging
- Action metadata: Properly formatted action.yml with all input definitions
- Docker support: Multi-architecture container support
- Permission handling: Documented requirements for
contents: write
Breaking Changes
- Version 2.0.0: Complete rewrite from v1.x (which were non-functional)
- Node.js 20: Updated from Node.js 18 for better performance and security
- Alpine Linux: Switched to Alpine base image with proper bash support
- Enhanced validation: Stricter input validation and error reporting
Usage
See the README for the current usage of the action, and the examples for complete workflows. The usage of v2 is in the v2 tag.