-
Notifications
You must be signed in to change notification settings - Fork 0
feat: initial version of the pnpm/update action #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
17326a2
feat: initial version of the pnpm/update action
zkochan c47e161
fix: base updates on the base branch, keep the token off the command …
zkochan 1ecbc4f
feat: update pnpm and the pinned runtime by default, within their cur…
zkochan 9e5973c
fix: unset persisted credentials on GitHub Enterprise Server too
zkochan d8cc10c
feat: refresh the lockfile by default, add update-deps and post-updat…
zkochan 70533cf
feat: generate changesets for packages whose production dependencies …
zkochan eac38c2
feat: update GitHub Actions too, with pnpm update --include-github-ac…
zkochan d814d12
refactor: make GitHub Actions updates opt-in (default off)
zkochan d9dc264
test: extract pure logic into scripts/lib.sh with bats unit tests + CI
zkochan cb81a1c
feat: generate changesets via native pnpm update --changeset
zkochan f7f2d4e
test: end-to-end tests for the update step against a stubbed pnpm
zkochan dc7b47b
ci: run bats via npx to avoid global-install permission errors
zkochan cab2c6a
ci: run tests with pnpm's pnx instead of npx
zkochan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| name: Test | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: {} | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-24.04 | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: latest | ||
| # ubuntu runners ship shellcheck; lint the real bash scripts (the action's | ||
| # thin ${{ }} wiring steps aren't shell to check). | ||
| - name: Shellcheck the scripts | ||
| run: shellcheck scripts/*.sh | ||
| # Fetch and run bats with pnpm's own `pnx` (= `pnpm dlx`), dogfooding pnpm | ||
| # instead of npm's npx. | ||
| - name: Run unit and integration tests | ||
| run: pnx bats test/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| # pnpm/update | ||
|
|
||
| Updates the dependencies of your project with pnpm, keeps the pinned | ||
| pnpm (`packageManager` / `devEngines.packageManager`) and Node.js | ||
| (`devEngines.runtime`) versions fresh — by default within their current major | ||
| versions — and opens a pull request with the result. It can optionally update | ||
| the GitHub Actions pinned in your workflow files too. By default the lockfile | ||
| is regenerated from scratch, so transitive dependencies of unchanged packages | ||
| are refreshed too. | ||
|
|
||
| The default setup needs no secrets: the built-in `GITHUB_TOKEN` is enough to | ||
| update dependencies, pnpm, and Node.js, validate them with your own `verify` | ||
| commands, and open a pull request. | ||
|
|
||
| Unlike external dependency bots, this action runs pnpm itself, so it supports | ||
| every feature of your workspace: catalogs, patched dependencies, config | ||
| dependencies, overrides, and anything pnpm learns in the future. | ||
|
|
||
| The action expects pnpm (and a runtime, if your project needs one for | ||
| verification) to already be set up — pair it with [`pnpm/setup`]. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```yaml | ||
| name: Update Dependencies | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: '0 0 * * 1' # Every Monday at midnight UTC | ||
| workflow_dispatch: {} | ||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
|
|
||
| concurrency: | ||
| group: update-dependencies | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| update-dependencies: | ||
| if: github.repository == 'your-org/your-repo' # Don't run on forks | ||
| runs-on: ubuntu-24.04 | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| # Installs the pnpm version from `packageManager` and the runtime | ||
| # from `devEngines.runtime`. | ||
| - uses: pnpm/setup@v1 | ||
| - uses: pnpm/update@v0 | ||
| with: | ||
| verify: | | ||
| pnpm build | ||
| pnpm test | ||
| ``` | ||
|
|
||
| [`pnpm/setup`]: https://github.com/pnpm/setup | ||
|
|
||
| ## Updating GitHub Actions | ||
|
|
||
| The action can also bump the GitHub Actions pinned in `.github/workflows/*.yml` | ||
| and `action.yml` (via `pnpm update --include-github-actions`). This is **opt-in** | ||
| (`github-actions: true`) because GitHub does not let the default `GITHUB_TOKEN` | ||
| push changes to workflow files — you must pass a `token` that carries the | ||
| `workflow` scope (a PAT) or `workflows: write` (a GitHub App). Without such a | ||
| token the push fails as soon as an action needs updating. | ||
|
|
||
| ```yaml | ||
| - uses: pnpm/update@v0 | ||
| with: | ||
| github-actions: true | ||
| token: ${{ secrets.UPDATE_TOKEN }} # PAT with `repo` + `workflow` | ||
| ``` | ||
|
|
||
| GitHub Actions updates ride along with the dependency update, so they only | ||
| happen when `update-deps` is `latest` or `ranges` (not `false`). | ||
|
|
||
| ## Refreshing the lockfile only | ||
|
|
||
| To refresh the lockfile to the latest versions matching your `package.json` | ||
| ranges without touching any manifests (and, in this example, follow pnpm's | ||
| prereleases while propagating updated versions into other files): | ||
|
|
||
| ```yaml | ||
| - uses: pnpm/update@v0 | ||
| with: | ||
| update-deps: false | ||
| update-pnpm: next-12 | ||
| post-update: pnpm update-manifests | ||
| token: ${{ secrets.UPDATE_TOKEN }} | ||
| ``` | ||
|
|
||
| ## Development | ||
|
|
||
| The action's logic lives in `scripts/` so it can be tested outside of a live | ||
| workflow: | ||
|
|
||
| - `scripts/lib.sh` — pure helpers (update-argument construction, `update-deps` | ||
| validation, Node.js-major extraction), unit-tested in `test/lib.bats`. | ||
| - `scripts/update.sh` — the whole "Update dependencies" step, driven end-to-end | ||
| in `test/update.bats` against a stubbed `pnpm` (`test/stubs/pnpm`) that | ||
| records the commands it would run. | ||
|
|
||
| Run the checks with [shellcheck](https://www.shellcheck.net) and | ||
| [bats](https://github.com/bats-core/bats-core): | ||
|
|
||
| ```sh | ||
| shellcheck scripts/*.sh | ||
| bats test/ | ||
| ``` | ||
|
|
||
| CI runs both on every push and pull request. | ||
|
|
||
| ## Inputs | ||
|
|
||
| | Input | Default | Description | | ||
| |---|---|---| | ||
| | `token` | `github.token` | Token used to push the branch and create the PR. PRs created with the default `GITHUB_TOKEN` don't trigger other workflows; pass a GitHub App token or PAT if you want CI to run on the PR. With `github-actions` enabled, the token must also carry the `workflow` scope (PAT) or `workflows: write` (App) to push workflow-file changes. | | ||
| | `branch` | `chore/update-dependencies` | Branch the updates are pushed to (force-pushed on every run, so at most one update PR stays open). | | ||
| | `base` | repository default branch | Branch the updates are based on and the pull request targets. | | ||
| | `update-deps` | `latest` | How to update dependencies: `latest` ignores `package.json` ranges, `ranges` stays within them, `false` skips manifest updates entirely. | | ||
| | `refresh-lockfile` | `true` | Delete `pnpm-lock.yaml` and `node_modules` before updating, so the whole graph — including transitive dependencies — is freshly resolved. Set to `false` to keep existing resolutions where possible. | | ||
| | `exclude` | — | Whitespace-separated package name patterns whose ranges should not be updated, e.g. `typescript @types/*`. With `refresh-lockfile`, excluded packages are still re-resolved within their kept ranges. | | ||
| | `github-actions` | `false` | Set to `true` to also update the GitHub Actions pinned in `.github/workflows/*.yml` and `action.yml`. Only applies when `update-deps` is `latest` or `ranges`. Requires a `token` with the `workflow` scope (see above). | | ||
| | `post-update` | — | Shell commands run after the updates, before verification; their changes are included in the PR. | | ||
| | `changesets` | `true` | In repositories that use changesets: generate a changeset for the updated dependencies via `pnpm update --changeset` (patch for production deps, major for peer deps, and the same for packages consuming a changed `catalog:` entry). Private, ignored, and dev-only changes are skipped. Only applies in `latest`/`ranges` mode and when the installed pnpm supports `--changeset`. Set to `false` to disable. | | ||
| | `update-pnpm` | pinned major | Bump pnpm itself via `pnpm self-update`. Defaults to the latest release of the currently pinned major; set a version, range, or dist-tag (`latest`, `12`, `next-12`) to move onto it, or `false` to skip. | | ||
| | `node` | pinned major | Bump the Node.js version pinned in `devEngines.runtime`. Defaults to the latest release of the currently pinned major (skipped when nothing is pinned); set `24`, `lts`, or `latest` to move onto it, or `false` to skip. | | ||
| | `verify` | — | Shell commands run after updating (build, tests). If they fail, no PR is created. | | ||
| | `commit-message` | `chore: update dependencies` | Message of the update commit. | | ||
| | `pr-title` | `chore: update dependencies` | Title of the pull request. | | ||
| | `pr-body` | Automated dependency updates… | Body of the pull request. | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| name: 'pnpm update' | ||
| description: 'Update dependencies (and optionally pnpm and the runtime) with pnpm, then open a pull request' | ||
| branding: | ||
| icon: 'refresh-cw' | ||
| color: 'orange' | ||
| inputs: | ||
| token: | ||
| description: >- | ||
| Token used to push the update branch and create the pull request. | ||
| Pull requests created with the default GITHUB_TOKEN do not trigger other | ||
| workflows; pass a GitHub App token or PAT if you want CI to run on the PR. | ||
| When github-actions is enabled, the token must additionally carry the | ||
| `workflow` scope (PAT) or `workflows: write` (App) to push changes to | ||
| .github/workflows files. | ||
| default: ${{ github.token }} | ||
| branch: | ||
| description: >- | ||
| Branch the updates are pushed to. It is force-pushed on every run, so at | ||
| most one update PR stays open at a time. | ||
| default: 'chore/update-dependencies' | ||
| base: | ||
| description: 'Branch the updates are based on and the pull request targets.' | ||
| default: ${{ github.event.repository.default_branch }} | ||
| update-deps: | ||
| description: >- | ||
| How to update dependencies with `pnpm update`. "latest" updates them to | ||
| their latest versions, ignoring the ranges declared in package.json; | ||
| "ranges" updates them within those ranges; "false" skips manifest | ||
| updates entirely (combined with refresh-lockfile, this refreshes the | ||
| lockfile without touching any package.json). | ||
| default: 'latest' | ||
| refresh-lockfile: | ||
| description: >- | ||
| Delete pnpm-lock.yaml and node_modules before updating, so the whole | ||
| dependency graph — including transitive dependencies of unchanged | ||
| packages — is freshly resolved instead of reused from the existing | ||
| lockfile. Set to "false" to keep existing resolutions where possible. | ||
| default: 'true' | ||
| exclude: | ||
| description: >- | ||
| Whitespace-separated package name patterns whose package.json ranges | ||
| should not be updated. Example: "typescript @types/*". Note that with | ||
| refresh-lockfile, excluded packages are still re-resolved within their | ||
| kept ranges. | ||
| default: '' | ||
| github-actions: | ||
| description: >- | ||
| Set to "true" to also update the GitHub Actions pinned in | ||
| .github/workflows/*.yml and action.yml (via | ||
| `pnpm update --include-github-actions`). Opt-in because pushing | ||
| workflow-file changes requires a token with the `workflow` scope (a PAT) | ||
| or `workflows: write` (a GitHub App); the default GITHUB_TOKEN cannot, so | ||
| enabling it without such a token fails the push. Only applies when | ||
| update-deps is "latest" or "ranges". | ||
| default: 'false' | ||
| post-update: | ||
| description: >- | ||
| Shell commands run after the updates, before verification and the | ||
| commit. Useful for propagating updated versions into other files; | ||
| their changes are included in the pull request. | ||
| default: '' | ||
| changesets: | ||
| description: >- | ||
| Generate a changeset for the updated dependencies via | ||
| `pnpm update --changeset`, so the next release ships them: a patch bump | ||
| for changed production dependencies, a major bump for changed peer | ||
| dependencies, and the same for packages consuming a changed `catalog:` | ||
| entry. Only applies when update-deps is "latest" or "ranges", the | ||
| repository uses changesets (.changeset/config.json exists), and the | ||
| installed pnpm supports `--changeset`. Set to "false" to disable (which | ||
| also passes `--no-changeset`, overriding a repo-level `update.changeset`). | ||
| default: 'true' | ||
| update-pnpm: | ||
| description: >- | ||
| How to update the pinned pnpm version (packageManager and | ||
| devEngines.packageManager) via `pnpm self-update`. By default, updates | ||
| to the latest release of the currently pinned major version. Set to a | ||
| version, range, or dist-tag (e.g. "latest", "12", "next-12") to move | ||
| onto that instead, or "false" to skip. | ||
| default: '' | ||
| node: | ||
| description: >- | ||
| How to update the Node.js version pinned in devEngines.runtime. By | ||
| default, updates to the latest release of the currently pinned major | ||
| version (skipped when no Node.js version is pinned). Set to a spec | ||
| accepted by `pnpm runtime set node` (e.g. "24", "lts", "latest") to | ||
| move onto that instead, or "false" to skip. | ||
| default: '' | ||
| verify: | ||
| description: >- | ||
| Shell commands run after updating (e.g. build and tests). If they fail, | ||
| no pull request is created. | ||
| default: '' | ||
| commit-message: | ||
| description: 'Message of the update commit.' | ||
| default: 'chore: update dependencies' | ||
| pr-title: | ||
| description: 'Title of the pull request.' | ||
| default: 'chore: update dependencies' | ||
| pr-body: | ||
| description: 'Body of the pull request.' | ||
| default: 'Automated dependency updates generated with `pnpm update`.' | ||
| runs: | ||
| using: 'composite' | ||
| steps: | ||
| - name: Prepare the update branch | ||
| shell: bash | ||
| env: | ||
| BRANCH: ${{ inputs.branch }} | ||
| BASE: ${{ inputs.base }} | ||
| run: | | ||
| set -euo pipefail | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| # Base the update on the latest base branch, even when the workflow | ||
| # was dispatched from another ref or the checkout is shallow. | ||
| git fetch origin "$BASE" | ||
| git checkout -B "$BRANCH" FETCH_HEAD | ||
|
|
||
| - name: Update dependencies | ||
| shell: bash | ||
| env: | ||
| UPDATE_DEPS: ${{ inputs.update-deps }} | ||
| REFRESH_LOCKFILE: ${{ inputs.refresh-lockfile }} | ||
| EXCLUDE: ${{ inputs.exclude }} | ||
| # Not GITHUB_ACTIONS: the runner already sets that to "true". | ||
| INCLUDE_GITHUB_ACTIONS: ${{ inputs.github-actions }} | ||
| CHANGESETS: ${{ inputs.changesets }} | ||
| UPDATE_PNPM: ${{ inputs.update-pnpm }} | ||
| NODE: ${{ inputs.node }} | ||
| run: bash "$GITHUB_ACTION_PATH/scripts/update.sh" | ||
|
|
||
| - name: Run post-update commands | ||
| if: ${{ inputs.post-update != '' }} | ||
| shell: bash | ||
| run: ${{ inputs.post-update }} | ||
|
|
||
| - name: Verify the updated project | ||
| if: ${{ inputs.verify != '' }} | ||
| shell: bash | ||
| run: ${{ inputs.verify }} | ||
|
|
||
| - name: Commit, push, and create the pull request | ||
| shell: bash | ||
| env: | ||
| GH_TOKEN: ${{ inputs.token }} | ||
| BRANCH: ${{ inputs.branch }} | ||
| BASE: ${{ inputs.base }} | ||
| COMMIT_MESSAGE: ${{ inputs.commit-message }} | ||
| PR_TITLE: ${{ inputs.pr-title }} | ||
| PR_BODY: ${{ inputs.pr-body }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| if [ -z "$(git status --porcelain)" ]; then | ||
| echo "Everything is up to date." | ||
| exit 0 | ||
| fi | ||
|
|
||
| git add -A | ||
| git commit -m "$COMMIT_MESSAGE" | ||
|
|
||
| # Remove any credentials persisted by actions/checkout: they would | ||
| # take precedence over the token this action was given, silently | ||
| # downgrading a user-supplied PAT or App token to GITHUB_TOKEN. | ||
| git config --local --unset-all "http.${GITHUB_SERVER_URL:-https://github.com}/.extraheader" || true | ||
| # Supply the token through a credential helper (it reads GH_TOKEN | ||
| # from the environment) so it never appears on a command line. | ||
| git -c credential.helper= \ | ||
| -c credential.helper='!f() { echo username=x-access-token; echo "password=${GH_TOKEN}"; }; f' \ | ||
| push --force origin "$BRANCH" | ||
|
|
||
| # A PR left open by a previous run already points at the branch we | ||
| # just force-pushed, so there is nothing more to do. | ||
| if [ -z "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then | ||
| gh pr create \ | ||
| --title "$PR_TITLE" \ | ||
| --body "$PR_BODY" \ | ||
| --base "$BASE" \ | ||
| --head "$BRANCH" | ||
| fi | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| #!/usr/bin/env bash | ||
| # Pure helpers for the pnpm/update action, kept in a sourceable library so they | ||
| # can be unit-tested (see test/lib.bats) without running the whole action. | ||
| # Nothing here has side effects or calls pnpm/git — the action wires these into | ||
| # its steps. | ||
|
|
||
| # Validate the `update-deps` input. Prints a GitHub error annotation and returns | ||
| # non-zero on an unknown value. | ||
| validate_update_deps() { | ||
| case "$1" in | ||
| latest | ranges | false) return 0 ;; | ||
| *) | ||
| echo "::error::Invalid value for the update-deps input: ${1}. Expected latest, ranges, or false." | ||
| return 1 | ||
| ;; | ||
| esac | ||
| } | ||
|
|
||
| # Print the arguments for `pnpm update`, one per line, given: | ||
| # $1 update-deps (latest|ranges — the caller handles "false") | ||
| # $2 include-github-actions (true|false) | ||
| # $3 exclude (whitespace-separated name patterns) | ||
| # $4 changeset-arg (--changeset, --no-changeset, or empty) | ||
| # The exclude string is deliberately word-split; the caller runs with `set -f` | ||
| # so patterns like "@types/*" reach pnpm as negation selectors rather than | ||
| # globbing against the working tree. | ||
| pnpm_update_args() { | ||
| local update_deps="$1" include_actions="$2" exclude="$3" changeset_arg="${4:-}" pattern | ||
| printf '%s\n' --recursive | ||
| [ "$update_deps" = latest ] && printf '%s\n' --latest | ||
| [ "$include_actions" = true ] && printf '%s\n' --include-github-actions | ||
| [ -n "$changeset_arg" ] && printf '%s\n' "$changeset_arg" | ||
| # shellcheck disable=SC2086 # intentional word splitting; caller sets -f | ||
| for pattern in $exclude; do | ||
| printf '!%s\n' "$pattern" | ||
| done | ||
| return 0 | ||
| } | ||
|
|
||
| # Print the pinned Node.js major from a package.json's `devEngines.runtime`, | ||
| # handling both the single-object and array forms. Prints nothing (and returns | ||
| # 0) when no Node.js runtime is pinned or the file is unreadable. | ||
| node_major_from_manifest() { | ||
| local file="${1:-package.json}" pinned | ||
| pinned="$(jq -r ' | ||
| .devEngines.runtime // empty | ||
| | if type == "array" then .[] else . end | ||
| | select(.name == "node") | .version // empty | ||
| ' "$file" 2>/dev/null | head -n 1)" | ||
| [ -n "$pinned" ] || return 0 | ||
| printf '%s' "$pinned" | grep -oE '[0-9]+' | head -n 1 | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.