mirror of
https://github.com/changesets/action
synced 2026-10-26 16:39:56 +08:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79d401e0bd | ||
|
|
5b307d3df2 | ||
|
|
84d78c68f9 | ||
|
|
c6cc0d84cb | ||
|
|
e3e2663c0e | ||
|
|
e43c867727 | ||
|
|
4b6e1f1099 | ||
|
|
cf45e93c21 | ||
|
|
73591071e6 | ||
|
|
f71ae043ed | ||
|
|
ca57073900 | ||
|
|
0eae789230 | ||
|
|
164652bdd6 | ||
|
|
a35a66bd1e | ||
|
|
823cf741ca | ||
|
|
b12c36a7bf | ||
|
|
5a8b9b721b | ||
|
|
0490518932 | ||
|
|
dc29b738b5 | ||
|
|
ccb38113ea | ||
|
|
cf37561cd9 | ||
|
|
5c88881fff |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@changesets/action": patch
|
||||
---
|
||||
|
||||
Fix the computed publish plan path passed internally to `changeset pack` by the `/pack` subaction.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@changesets/action": patch
|
||||
---
|
||||
|
||||
Validate that projects use Changesets CLI v3 and direct Changesets CLI v2 users to `changesets/action@v1`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@changesets/action": patch
|
||||
---
|
||||
|
||||
Allow custom publish scripts to complete without a Changesets output file, warning that GitHub releases and git tags cannot be created when that file is missing.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@changesets/action": major
|
||||
---
|
||||
|
||||
Add a new `push-git-tags` option that complements `create-github-releases` to control specifically if git tags should be created but not GitHub releases.
|
||||
|
||||
If `create-github-releases` was previously set to `false`, which also indirectly disabled git tag creation, git tags will now be created instead by default. If this is not desired, set `push-git-tags` to `false` explicitly.
|
||||
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@1.6.2/schema.json",
|
||||
"changelog": [
|
||||
"@changesets/changelog-github",
|
||||
{ "repo": "changesets/action" }
|
||||
],
|
||||
"$schema": "https://unpkg.com/@changesets/config@4.0.0-next.6/schema.json",
|
||||
"changelog": "@changesets/changelog-github",
|
||||
"commit": false,
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
"ignore": [],
|
||||
"privatePackages": {
|
||||
"version": true,
|
||||
"tag": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
"@changesets/action": major
|
||||
---
|
||||
|
||||
Rename the root action inputs and outputs to better match the sub-actions' conventions.
|
||||
|
||||
Inputs:
|
||||
|
||||
- `version` -> `version-script`
|
||||
- `publish` -> `publish-script`
|
||||
- `commit` -> `commit-message`
|
||||
- `title` -> `pr-title`
|
||||
- `branch` -> `pr-base-branch`
|
||||
|
||||
Outputs:
|
||||
|
||||
- `pull-request-number` -> `pr-number`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@changesets/action": major
|
||||
---
|
||||
|
||||
Remove support for passing custom GitHub token through the GITHUB_TOKEN environment variable. It should be passed to the `github-token` input instead.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@changesets/action": patch
|
||||
---
|
||||
|
||||
Authenticate git CLI pushes with the configured GitHub token using Git extra headers instead of writing to a global `.netrc` file.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@changesets/action": patch
|
||||
---
|
||||
|
||||
Derive the Git server URL from the GitHub Actions context when configuring git CLI authentication to support GitHub Enterprise Server setups.
|
||||
+16
-4
@@ -1,8 +1,20 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"tag": "next",
|
||||
"initialVersions": {
|
||||
"@changesets/action": "1.9.0"
|
||||
},
|
||||
"changesets": ["eight-clocks-pull", "eighty-doors-swim", "lemon-turtles-itch"]
|
||||
"changesets": [
|
||||
"all-cars-grin",
|
||||
"chubby-geese-sink",
|
||||
"eight-clocks-pull",
|
||||
"eighty-doors-swim",
|
||||
"eighty-parks-tickle",
|
||||
"fiery-toes-vanish",
|
||||
"git-cli-auth-header",
|
||||
"git-cli-server-url",
|
||||
"lemon-turtles-itch",
|
||||
"ready-mangos-boil",
|
||||
"shaky-rings-type",
|
||||
"shy-months-crash",
|
||||
"silent-tips-dance",
|
||||
"yellow-roses-fix"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@changesets/action": patch
|
||||
---
|
||||
|
||||
Fixed usage of `--from-publish-plan` flag used by the `/pack` subaction
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@changesets/action": major
|
||||
---
|
||||
|
||||
Update to Changesets v3 packages
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@changesets/action": minor
|
||||
---
|
||||
|
||||
Published packages detection done through stdout parsing was replaced with one based on the shared output file using `CHANGESETS_OUTPUT` environment variable. When using custom scripts this environment variable should always be passed down to the Changesets CLI invocations.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@changesets/action": patch
|
||||
---
|
||||
|
||||
Fix the `/version` subaction to not crash on missing `pr-base-branch` input. This input is meant to be optional.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@changesets/action": minor
|
||||
---
|
||||
|
||||
Add staged publishing support to the publish action and a GitHub release action
|
||||
that validates the staged handoff, verifies approved packages, and creates tags
|
||||
and releases idempotently.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@changesets/action": major
|
||||
---
|
||||
|
||||
Rename the input and output names to kebab-case instead of camelCase to match the official GitHub actions pattern
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/.github/workflows/** @Andarist @bluwy @emmatown
|
||||
/.github/** @Andarist @bluwy @emmatown
|
||||
/action.yml @Andarist @bluwy @emmatown
|
||||
/scripts/bump.ts @Andarist @bluwy @emmatown
|
||||
/scripts/release.ts @Andarist @bluwy @emmatown
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Setup bot auth
|
||||
description: Create a GitHub App token and configure matching git identity
|
||||
|
||||
inputs:
|
||||
client-id:
|
||||
description: GitHub App client ID
|
||||
required: true
|
||||
private-key:
|
||||
description: GitHub App private key
|
||||
required: true
|
||||
permission-contents:
|
||||
description: Contents permission for the GitHub App token
|
||||
required: false
|
||||
default: write
|
||||
permission-pull-requests:
|
||||
description: Pull requests permission for the GitHub App token
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
token:
|
||||
description: GitHub App installation access token
|
||||
value: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
id: app-token
|
||||
with:
|
||||
client-id: ${{ inputs.client-id }}
|
||||
private-key: ${{ inputs.private-key }}
|
||||
permission-contents: ${{ inputs.permission-contents }}
|
||||
permission-pull-requests: ${{ inputs.permission-pull-requests }}
|
||||
|
||||
- name: Setup Git user
|
||||
shell: bash
|
||||
run: |
|
||||
app_user="$APP_SLUG[bot]"
|
||||
app_id="$(gh api "/users/$app_user" --jq .id)"
|
||||
git config user.name "$app_user"
|
||||
git config user.email "$app_id+$app_user@users.noreply.github.com"
|
||||
env:
|
||||
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
@@ -15,13 +15,12 @@ concurrency:
|
||||
permissions: {} # each job should define its own permission explicitly
|
||||
|
||||
jobs:
|
||||
version:
|
||||
name: Version
|
||||
select-mode:
|
||||
name: Select mode
|
||||
runs-on: ubuntu-latest
|
||||
environment: version
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
hasChangesets: ${{ steps.changesets.outputs.hasChangesets }}
|
||||
mode: ${{ steps.select-mode.outputs.mode }}
|
||||
permissions:
|
||||
contents: read # to check out repo (actions/checkout)
|
||||
steps:
|
||||
@@ -37,38 +36,20 @@ jobs:
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
id: app-token
|
||||
with:
|
||||
client-id: ${{ vars.APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
permission-contents: write # to create version commits (changesets/action)
|
||||
permission-pull-requests: write # to create pull request (changesets/action)
|
||||
- name: Select mode
|
||||
id: select-mode
|
||||
uses: ./select-mode
|
||||
|
||||
- name: Create or update release pull request
|
||||
id: changesets
|
||||
uses: ./
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
version: pnpm bump
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
if: needs.version.outputs.hasChangesets == 'false'
|
||||
needs: version
|
||||
version:
|
||||
name: Version
|
||||
if: needs.select-mode.outputs.mode == 'version'
|
||||
needs: select-mode
|
||||
runs-on: ubuntu-latest
|
||||
environment: marketplace
|
||||
environment: version
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: write # to create release and have it authored by github-actions (changesets/action)
|
||||
contents: read # to check out repo (actions/checkout)
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
id: app-token
|
||||
with:
|
||||
client-id: ${{ vars.APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
permission-contents: write # to bypass branch and tag protection rules (release.ts)
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
@@ -81,8 +62,53 @@ jobs:
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish to marketplace
|
||||
uses: ./
|
||||
- uses: ./.github/actions/setup-bot-auth
|
||||
id: bot-auth
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
publish: pnpm release
|
||||
client-id: ${{ vars.APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
permission-contents: write # to create version commits (changesets/action)
|
||||
permission-pull-requests: write # to create pull request (changesets/action)
|
||||
|
||||
- name: Create or update release pull request
|
||||
id: changesets
|
||||
uses: ./version
|
||||
with:
|
||||
github-token: ${{ steps.bot-auth.outputs.token }}
|
||||
setup-git-user: false
|
||||
script: pnpm bump
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
if: needs.select-mode.outputs.mode == 'publish'
|
||||
needs: select-mode
|
||||
runs-on: ubuntu-latest
|
||||
environment: marketplace
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: write # to create release and have it authored by github-actions (changesets/action)
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/ci-setup
|
||||
with:
|
||||
skip-cache: true # avoid cache poisoning attacks
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- uses: ./.github/actions/setup-bot-auth
|
||||
id: bot-auth
|
||||
with:
|
||||
client-id: ${{ vars.APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
permission-contents: write # to bypass branch and tag protection rules (release.ts)
|
||||
|
||||
- name: Publish to marketplace
|
||||
uses: ./publish
|
||||
with:
|
||||
github-token: ${{ steps.bot-auth.outputs.token }}
|
||||
script: pnpm release
|
||||
|
||||
@@ -122,13 +122,6 @@ jobs:
|
||||
pull-requests: write # to comment on the pull request with the release result (changesets/action)
|
||||
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
id: app-token
|
||||
with:
|
||||
client-id: ${{ vars.APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
permission-contents: write # to push the release snapshot branch (release-pr.ts)
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
@@ -173,15 +166,17 @@ jobs:
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Setup Git user
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
- uses: ./.github/actions/setup-bot-auth
|
||||
id: bot-auth
|
||||
with:
|
||||
client-id: ${{ vars.APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
permission-contents: write # to push the release snapshot branch (release-pr.ts)
|
||||
|
||||
- name: Release snapshot version
|
||||
run: pnpm release:pr
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
GITHUB_TOKEN: ${{ steps.bot-auth.outputs.token }}
|
||||
|
||||
- name: Add success reaction
|
||||
run: gh api "/repos/$GH_REPO/issues/comments/$COMMENT_ID/reactions" -f content='rocket'
|
||||
|
||||
@@ -1,5 +1,53 @@
|
||||
# @changesets/action
|
||||
|
||||
## 2.0.0-next.3
|
||||
|
||||
### Major Changes
|
||||
|
||||
- [#680](https://github.com/changesets/action/pull/680) [`ca57073`](https://github.com/changesets/action/commit/ca57073900dc678254406a052a03c0c8824e319d) Thanks [@bluwy](https://github.com/bluwy)! - Add a new `push-git-tags` option that complements `create-github-releases` to control specifically if git tags should be created but not GitHub releases.
|
||||
|
||||
If `create-github-releases` was previously set to `false`, which also indirectly disabled git tag creation, git tags will now be created instead by default. If this is not desired, set `push-git-tags` to `false` explicitly.
|
||||
|
||||
- [#681](https://github.com/changesets/action/pull/681) [`7359107`](https://github.com/changesets/action/commit/73591071e61c7f61cf322dd7a6341dc29a8e1d4e) Thanks [@bluwy](https://github.com/bluwy)! - Rename the root action inputs and outputs to better match the sub-actions' conventions.
|
||||
|
||||
Inputs:
|
||||
- `version` -> `version-script`
|
||||
- `publish` -> `publish-script`
|
||||
- `commit` -> `commit-message`
|
||||
- `title` -> `pr-title`
|
||||
- `branch` -> `pr-base-branch`
|
||||
|
||||
Outputs:
|
||||
- `pull-request-number` -> `pr-number`
|
||||
|
||||
- [#674](https://github.com/changesets/action/pull/674) [`164652b`](https://github.com/changesets/action/commit/164652bdd60525670d95291addb8c6f92833ac60) Thanks [@bluwy](https://github.com/bluwy)! - Remove support for passing custom GitHub token through the GITHUB_TOKEN environment variable. It should be passed to the `github-token` input instead.
|
||||
|
||||
- [#673](https://github.com/changesets/action/pull/673) [`823cf74`](https://github.com/changesets/action/commit/823cf741ca57c5e22652123d3a847dfafcd77ca0) Thanks [@bluwy](https://github.com/bluwy)! - Update to Changesets v3 packages
|
||||
|
||||
- [#668](https://github.com/changesets/action/pull/668) [`0eae789`](https://github.com/changesets/action/commit/0eae789230defbc4ad287e9b476aba7e842e34e0) Thanks [@bluwy](https://github.com/bluwy)! - Rename the input and output names to kebab-case instead of camelCase to match the official GitHub actions pattern
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#678](https://github.com/changesets/action/pull/678) [`f71ae04`](https://github.com/changesets/action/commit/f71ae043ed5deb17952900091e009a60edef9507) Thanks [@Andarist](https://github.com/Andarist)! - Published packages detection done through stdout parsing was replaced with one based on the shared output file using `CHANGESETS_OUTPUT` environment variable. When using custom scripts this environment variable should always be passed down to the Changesets CLI invocations.
|
||||
|
||||
## 2.0.0-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#670](https://github.com/changesets/action/pull/670) [`5a8b9b7`](https://github.com/changesets/action/commit/5a8b9b721bf683e3bf9bd72ea92a685dc024147b) Thanks [@Andarist](https://github.com/Andarist)! - Authenticate git CLI pushes with the configured GitHub token using Git extra headers instead of writing to a global `.netrc` file.
|
||||
|
||||
- [#670](https://github.com/changesets/action/pull/670) [`5a8b9b7`](https://github.com/changesets/action/commit/5a8b9b721bf683e3bf9bd72ea92a685dc024147b) Thanks [@Andarist](https://github.com/Andarist)! - Derive the Git server URL from the GitHub Actions context when configuring git CLI authentication to support GitHub Enterprise Server setups.
|
||||
|
||||
## 2.0.0-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#663](https://github.com/changesets/action/pull/663) [`ccb3811`](https://github.com/changesets/action/commit/ccb38113eaaa4a13eb6aabe09520e49cc8519b84) Thanks [@Andarist](https://github.com/Andarist)! - Fix the computed publish plan path passed internally to `changeset pack` by the `/pack` subaction.
|
||||
|
||||
- [#662](https://github.com/changesets/action/pull/662) [`5c88881`](https://github.com/changesets/action/commit/5c88881fff1f0d8b055d4a2d3a74a8ce843db6f3) Thanks [@Andarist](https://github.com/Andarist)! - Fixed usage of `--from-publish-plan` flag used by the `/pack` subaction
|
||||
|
||||
- [#666](https://github.com/changesets/action/pull/666) [`dc29b73`](https://github.com/changesets/action/commit/dc29b738b532d9e95f46b9bf493e2e3fdf48a7ed) Thanks [@Andarist](https://github.com/Andarist)! - Fix the `/version` subaction to not crash on missing `pr-base-branch` input. This input is meant to be optional.
|
||||
|
||||
## 2.0.0-next.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -14,20 +14,23 @@ There are also sub-actions hosted in this repository. Check out their respective
|
||||
|
||||
### Inputs
|
||||
|
||||
- publish - The command to use to build and publish packages
|
||||
- version - The command to update version, edit CHANGELOG, read and delete changesets. Default to `changeset version` if not provided
|
||||
- commit - The commit message to use. Default to `Version Packages`
|
||||
- title - The pull request title. Default to `Version Packages`
|
||||
- setupGitUser - Sets up the git user for commits as `"github-actions[bot]"`. Default to `true`
|
||||
- createGithubReleases - A boolean value to indicate whether to create Github releases after `publish` or not. Default to `true`
|
||||
- commitMode - Specifies the commit mode. Use `"git-cli"` to push changes using the Git CLI, or `"github-api"` to push changes via the GitHub API. When using `"github-api"`, all commits and tags are GPG-signed and attributed to the user or app who owns the `GITHUB_TOKEN`. Default to `git-cli`.
|
||||
- publish-script - The command to use to build and publish packages
|
||||
- version-script - The command to update version, edit CHANGELOG, read and delete changesets. Default to `changeset version` if not provided
|
||||
- commit-message - The commit message to use. Default to `Version Packages`
|
||||
- pr-title - The pull request title. Default to `Version Packages`
|
||||
- setup-git-user - Sets up the git user for commits as `"github-actions[bot]"`. Default to `true`
|
||||
- create-github-releases - A boolean value to indicate whether to create Github releases after `publish` or not. Default to `true`
|
||||
- push-git-tags - A boolean value to indicate whether to create git tags after `publish` or not. Default to `true`
|
||||
- commit-mode - Specifies the commit mode. Use `"git-cli"` to push changes using the Git CLI, or `"github-api"` to push changes via the GitHub API. When using `"github-api"`, all commits and tags are GPG-signed and attributed to the user or app who owns the `GITHUB_TOKEN`. Default to `git-cli`
|
||||
- cwd - Changes node's `process.cwd()` if the project is not located on the root. Default to `process.cwd()`
|
||||
- prDraft - Controls draft PR behavior. Use `create` to create new version PRs as draft, or `always` to also convert existing version PRs back to draft when updating them. By default, version PRs are not forced into draft mode.
|
||||
- pr-draft - Controls draft PR behavior. Use `create` to create new version PRs as draft, or `always` to also convert existing version PRs back to draft when updating them. By default, version PRs are not forced into draft mode
|
||||
- github-token - Passes a custom GitHub token
|
||||
|
||||
### Outputs
|
||||
|
||||
- published - A boolean value to indicate whether a publishing has happened or not
|
||||
- publishedPackages - A JSON array to present the published packages. The format is `[{"name": "@xx/xx", "version": "1.2.0"}, {"name": "@xx/xy", "version": "0.8.9"}]`
|
||||
- published-packages - A JSON array to present the published packages. The format is `[{"name": "@xx/xx", "version": "1.2.0"}, {"name": "@xx/xy", "version": "0.8.9"}]`
|
||||
- staged-release-artifact-id - The exact artifact id for staged packages emitted by a configured or custom publish command
|
||||
|
||||
### Example workflow
|
||||
|
||||
@@ -65,7 +68,7 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Create Release Pull Request
|
||||
uses: changesets/action@v1
|
||||
uses: changesets/action@v2
|
||||
```
|
||||
|
||||
#### With Publishing
|
||||
@@ -103,10 +106,10 @@ jobs:
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
uses: changesets/action@v2
|
||||
with:
|
||||
# This expects you to have a script called release which does a build for your packages and calls changeset publish
|
||||
publish: pnpm release
|
||||
publish-script: pnpm release
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -170,7 +173,7 @@ jobs:
|
||||
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
uses: changesets/action@v2
|
||||
|
||||
- name: Publish
|
||||
if: steps.changesets.outputs.hasChangesets == 'false'
|
||||
@@ -214,10 +217,10 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Create Release Pull Request
|
||||
uses: changesets/action@v1
|
||||
uses: changesets/action@v2
|
||||
with:
|
||||
# this expects you to have a npm script called version that runs some logic and then calls `changeset version`.
|
||||
version: pnpm version
|
||||
version-script: pnpm version
|
||||
```
|
||||
|
||||
#### With Yarn 2 / Plug'n'Play
|
||||
@@ -225,8 +228,8 @@ jobs:
|
||||
If you are using [Yarn Plug'n'Play](https://yarnpkg.com/features/pnp), you should use a custom `version` command so that the action can resolve the `changeset` CLI:
|
||||
|
||||
```yaml
|
||||
- uses: changesets/action@v1
|
||||
- uses: changesets/action@v2
|
||||
with:
|
||||
version: yarn changeset version
|
||||
version-script: yarn changeset version
|
||||
# ...
|
||||
```
|
||||
|
||||
+29
-20
@@ -8,28 +8,37 @@ inputs:
|
||||
description: "The GitHub token to use for authentication. Defaults to the GitHub-provided token."
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
publish:
|
||||
publish-script:
|
||||
description: "The command to use to build and publish packages"
|
||||
required: false
|
||||
version:
|
||||
version-script:
|
||||
description: "The command to update version, edit CHANGELOG, read and delete changesets. Default to `changeset version` if not provided"
|
||||
required: false
|
||||
commit:
|
||||
description: |
|
||||
The commit message. Default to `Version Packages`
|
||||
commit-message:
|
||||
description: "The commit message. Default to `Version Packages`"
|
||||
required: false
|
||||
title:
|
||||
description: The pull request title. Default to `Version Packages`
|
||||
default: "Version Packages"
|
||||
pr-title:
|
||||
description: "The pull request title. Default to `Version Packages`"
|
||||
required: false
|
||||
setupGitUser:
|
||||
description: Sets up the git user for commits as `"github-actions[bot]"`. Default to `true`
|
||||
default: "Version Packages"
|
||||
pr-draft:
|
||||
description: "Controls draft PR behavior. Use 'create' to create new version PRs as draft, or 'always' to also convert existing version PRs back to draft when updating them."
|
||||
required: false
|
||||
pr-base-branch:
|
||||
description: "Sets the base branch of the PR. Defaults to `github.ref_name`."
|
||||
required: false
|
||||
create-github-releases:
|
||||
description: "Whether to create Github releases after publish"
|
||||
required: false
|
||||
default: true
|
||||
createGithubReleases:
|
||||
description: "A boolean value to indicate whether to create Github releases after `publish` or not"
|
||||
push-git-tags:
|
||||
description: >
|
||||
Whether to create git tags after publish. If `create-github-releases` is set to `true`,
|
||||
this option will also always be `true`.
|
||||
required: false
|
||||
default: true
|
||||
commitMode:
|
||||
commit-mode:
|
||||
description: >
|
||||
An enum to specify the commit mode. Use "git-cli" to push changes using the Git CLI,
|
||||
or "github-api" to push changes via the GitHub API. When using "github-api",
|
||||
@@ -37,21 +46,21 @@ inputs:
|
||||
or app who owns the GITHUB_TOKEN.
|
||||
required: false
|
||||
default: "git-cli"
|
||||
branch:
|
||||
description: Sets the branch in which the action will run. Default to `github.ref_name` if not provided
|
||||
required: false
|
||||
prDraft:
|
||||
description: 'Controls draft PR behavior. Use "create" to create new version PRs as draft, or "always" to also convert existing version PRs back to draft when updating them.'
|
||||
setup-git-user:
|
||||
description: Sets up the git user for commits as `"github-actions[bot]"`. Default to `true`
|
||||
required: false
|
||||
default: true
|
||||
outputs:
|
||||
published:
|
||||
description: A boolean value to indicate whether a publishing is happened or not
|
||||
publishedPackages:
|
||||
published-packages:
|
||||
description: >
|
||||
A JSON array to present the published packages. The format is `[{"name": "@xx/xx", "version": "1.2.0"}, {"name": "@xx/xy", "version": "0.8.9"}]`
|
||||
hasChangesets:
|
||||
staged-release-artifact-id:
|
||||
description: Artifact id for a staged release handoff emitted by the publish script
|
||||
has-changesets:
|
||||
description: A boolean about whether there were changesets. Useful if you want to create your own publishing functionality.
|
||||
pullRequestNumber:
|
||||
pr-number:
|
||||
description: The pull request number that was created or updated
|
||||
branding:
|
||||
icon: "package"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# changesets/action/github-release
|
||||
|
||||
Finalizes packages produced by staged publishing. Pass the exact
|
||||
`staged-release-artifact-id` output from `changesets/action/publish`.
|
||||
|
||||
By default the action waits up to two minutes for every approved package
|
||||
version to become visible before creating any Git tag or GitHub Release.
|
||||
Private registries therefore need read authentication configured before this
|
||||
step. Set `verify-published: false` to skip that check.
|
||||
|
||||
The handoff is bound to the repository, workflow run, and commit. Tags and
|
||||
releases are created idempotently; a tag that already points at another commit
|
||||
is rejected.
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Changesets - GitHub Release
|
||||
description: Create Git tags and GitHub Releases after approving a staged publish
|
||||
runs:
|
||||
using: node24
|
||||
main: ../dist/github-release.js
|
||||
inputs:
|
||||
github-token:
|
||||
description: "The GitHub token to use for authentication. Defaults to the GitHub-provided token."
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
staged-release-artifact-id:
|
||||
description: "The exact artifact id emitted by the publish action"
|
||||
required: true
|
||||
verify-published:
|
||||
description: "Verify that every approved package is visible in its configured registry before creating tags and releases"
|
||||
required: false
|
||||
default: true
|
||||
outputs:
|
||||
released-packages:
|
||||
description: "A JSON array of packages finalized by this action"
|
||||
branding:
|
||||
icon: package
|
||||
color: blue
|
||||
+12
-11
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@changesets/action",
|
||||
"version": "2.0.0-next.0",
|
||||
"version": "2.0.0-next.3",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
@@ -21,12 +22,12 @@
|
||||
"@actions/core": "^3.0.1",
|
||||
"@actions/exec": "^3.0.0",
|
||||
"@actions/github": "^9.1.1",
|
||||
"@changesets/get-release-plan": "^4.0.16",
|
||||
"@changesets/ghcommit": "^2.1.1",
|
||||
"@changesets/git": "^3.0.4",
|
||||
"@changesets/pre": "^2.0.2",
|
||||
"@changesets/read": "^0.6.7",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"@changesets/get-release-plan": "^5.0.0-next.7",
|
||||
"@changesets/ghcommit": "^3.0.0-next.1",
|
||||
"@changesets/git": "^4.0.0-next.6",
|
||||
"@changesets/pre": "^3.0.0-next.6",
|
||||
"@changesets/read": "^1.0.0-next.7",
|
||||
"@manypkg/get-packages": "^3.1.0",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-throttling": "^11.0.3",
|
||||
"human-id": "^4.1.3",
|
||||
@@ -35,10 +36,10 @@
|
||||
"tinyexec": "^1.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/changelog-github": "^0.7.0",
|
||||
"@changesets/cli": "^2.31.0",
|
||||
"@changesets/types": "^6.1.0",
|
||||
"@changesets/write": "^0.4.0",
|
||||
"@changesets/changelog-github": "^1.0.0-next.6",
|
||||
"@changesets/cli": "^3.0.0-next.8",
|
||||
"@changesets/types": "^7.0.0-next.6",
|
||||
"@changesets/write": "^1.0.0-next.6",
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@types/node": "^24.12.4",
|
||||
"@types/semver": "^7.7.1",
|
||||
|
||||
Generated
+275
-1046
File diff suppressed because it is too large
Load Diff
+17
-1
@@ -3,7 +3,23 @@ dedupePeers: true
|
||||
dedupePeerDependents: true
|
||||
minimumReleaseAge: 10080
|
||||
minimumReleaseAgeExclude:
|
||||
- "@changesets/ghcommit@2.1.1"
|
||||
- "@changesets/apply-release-plan@8.0.0-next.7"
|
||||
- "@changesets/assemble-release-plan@7.0.0-next.7"
|
||||
- "@changesets/changelog-git@1.0.0-next.6"
|
||||
- "@changesets/changelog-github@1.0.0-next.6"
|
||||
- "@changesets/cli@3.0.0-next.8"
|
||||
- "@changesets/config@4.0.0-next.6"
|
||||
- "@changesets/get-dependents-graph@3.0.0-next.6"
|
||||
- "@changesets/get-github-info@1.0.0-next.3"
|
||||
- "@changesets/get-release-plan@5.0.0-next.7"
|
||||
- "@changesets/ghcommit@3.0.0-next.1"
|
||||
- "@changesets/git@4.0.0-next.6"
|
||||
- "@changesets/parse@1.0.0-next.7"
|
||||
- "@changesets/pre@3.0.0-next.6"
|
||||
- "@changesets/read@1.0.0-next.7"
|
||||
- "@changesets/should-skip-package@1.0.0-next.6"
|
||||
- "@changesets/types@7.0.0-next.6"
|
||||
- "@changesets/write@1.0.0-next.6"
|
||||
|
||||
overrides:
|
||||
lightningcss: "-" # we do not bundle any css
|
||||
|
||||
+22
-1
@@ -1,3 +1,24 @@
|
||||
# changesets/action/publish
|
||||
|
||||
TODO
|
||||
Publishes packages with Changesets.
|
||||
|
||||
Set the optional `stage` input to `true` or `false` to override
|
||||
`stagedPublishing` from the Changesets config. The input is tri-state: omitting
|
||||
it leaves the config in control. An explicit `stage` cannot be combined with a
|
||||
custom `script`; configure the script itself instead.
|
||||
|
||||
When packages are staged, the action uploads a 30-day handoff and returns its
|
||||
exact id as `staged-release-artifact-id`. It also prints a topologically
|
||||
ordered approval command:
|
||||
|
||||
```sh
|
||||
changeset stage approve <id...>
|
||||
```
|
||||
|
||||
That command is only a convenience and uses the default registry. For custom
|
||||
or multiple registries, split the IDs into correctly scoped commands and pass
|
||||
`--registry` to each.
|
||||
|
||||
After approval, pass the artifact id to
|
||||
`changesets/action/github-release` to verify publication and create Git tags
|
||||
and GitHub Releases.
|
||||
|
||||
@@ -11,6 +11,12 @@ inputs:
|
||||
script:
|
||||
description: "The command to use to publish packages"
|
||||
required: false
|
||||
stage:
|
||||
description: >
|
||||
Whether to use staged publishing. Omit to use the Changesets config,
|
||||
set to true for --stage, or false for --no-stage. Cannot be combined
|
||||
with a custom script.
|
||||
required: false
|
||||
pack-dir-artifact-id:
|
||||
description: "Artifact id for packed publish output generated by the pack subaction"
|
||||
required: false
|
||||
@@ -18,12 +24,20 @@ inputs:
|
||||
description: "Whether to create Github releases after publish"
|
||||
required: false
|
||||
default: true
|
||||
push-git-tags:
|
||||
description: >
|
||||
Whether to create git tags after publish. If `create-github-releases` is set to `true`,
|
||||
this option will also always be `true`.
|
||||
required: false
|
||||
default: true
|
||||
outputs:
|
||||
published:
|
||||
description: "A boolean value to indicate whether a publishing has happened or not"
|
||||
published-packages:
|
||||
description: >
|
||||
A JSON array to present the published packages. The format is `[{"name": "@xx/xx", "version": "1.2.0"}, {"name": "@xx/xy", "version": "0.8.9"}]`
|
||||
staged-release-artifact-id:
|
||||
description: "Artifact id for the staged release handoff"
|
||||
branding:
|
||||
icon: package
|
||||
color: blue
|
||||
|
||||
@@ -9,6 +9,7 @@ export default defineConfig({
|
||||
"select-mode": "src/select-mode/index.ts",
|
||||
version: "src/version/index.ts",
|
||||
publish: "src/publish/index.ts",
|
||||
"github-release": "src/github-release/index.ts",
|
||||
},
|
||||
output: {
|
||||
dir: "dist",
|
||||
|
||||
+3
-1
@@ -1,12 +1,14 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { exec } from "@actions/exec";
|
||||
import pkgJson from "../package.json" with { type: "json" };
|
||||
|
||||
process.chdir(path.join(import.meta.dirname, ".."));
|
||||
|
||||
await exec("changeset", ["version"]);
|
||||
|
||||
// read after versioning to get the new version
|
||||
const pkgJson = (await import("../package.json", { with: { type: "json" } }))
|
||||
.default;
|
||||
const releaseLine = `v${pkgJson.version.split(".")[0]}`;
|
||||
|
||||
const readmePath = path.join(import.meta.dirname, "..", "README.md");
|
||||
|
||||
+11
-31
@@ -1,6 +1,6 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import path from "node:path";
|
||||
import { exec, getExecOutput } from "@actions/exec";
|
||||
import { exec } from "@actions/exec";
|
||||
import pkgJson from "../package.json" with { type: "json" };
|
||||
|
||||
const tag = `v${pkgJson.version}`;
|
||||
@@ -20,37 +20,17 @@ const gitEnv = {
|
||||
|
||||
process.chdir(path.join(import.meta.dirname, ".."));
|
||||
|
||||
(async () => {
|
||||
const { exitCode, stderr } = await getExecOutput(
|
||||
`git`,
|
||||
["ls-remote", "--exit-code", "origin", "--tags", `refs/tags/${tag}`],
|
||||
{
|
||||
ignoreReturnCode: true,
|
||||
},
|
||||
);
|
||||
if (exitCode === 0) {
|
||||
console.log(
|
||||
`Action is not being published because version ${tag} is already published`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (exitCode !== 2) {
|
||||
throw new Error(`git ls-remote exited with ${exitCode}:\n${stderr}`);
|
||||
}
|
||||
await exec("git", ["checkout", "--detach"]);
|
||||
await exec("git", ["add", "--force", "dist"]);
|
||||
await exec("git", ["commit", "-m", tag]);
|
||||
|
||||
await exec("git", ["checkout", "--detach"]);
|
||||
await exec("git", ["add", "--force", "dist"]);
|
||||
await exec("git", ["commit", "-m", tag]);
|
||||
|
||||
await exec("changeset", ["tag"]);
|
||||
|
||||
if (isPrerelease) {
|
||||
await exec("git", ["push", "origin", `refs/tags/${tag}`], {
|
||||
env: gitEnv,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await exec("changeset", ["git-tag"]);
|
||||
|
||||
if (isPrerelease) {
|
||||
await exec("git", ["push", "origin", `refs/tags/${tag}`], {
|
||||
env: gitEnv,
|
||||
});
|
||||
} else {
|
||||
await exec(
|
||||
"git",
|
||||
[
|
||||
@@ -64,4 +44,4 @@ process.chdir(path.join(import.meta.dirname, ".."));
|
||||
env: gitEnv,
|
||||
},
|
||||
);
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as core from "@actions/core";
|
||||
import { GitHub } from "../github.ts";
|
||||
import {
|
||||
downloadAndValidateStagedRelease,
|
||||
finalizeStagedRelease,
|
||||
} from "../staged-release.ts";
|
||||
import { getOptionalBooleanInput, getRequiredInput } from "../utils.ts";
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (error) {
|
||||
core.setFailed((error as Error).message);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const cwd = process.cwd();
|
||||
const github = new GitHub({
|
||||
cwd,
|
||||
githubToken: getRequiredInput("github-token"),
|
||||
commitMode: "github-api",
|
||||
});
|
||||
const artifactId = Number(getRequiredInput("staged-release-artifact-id"));
|
||||
const verify = getOptionalBooleanInput("verify-published") ?? true;
|
||||
const { handoff, packagesByName } = await downloadAndValidateStagedRelease(
|
||||
artifactId,
|
||||
cwd,
|
||||
);
|
||||
|
||||
await finalizeStagedRelease({
|
||||
github,
|
||||
handoff,
|
||||
packagesByName,
|
||||
cwd,
|
||||
verify,
|
||||
});
|
||||
|
||||
core.setOutput(
|
||||
"released-packages",
|
||||
JSON.stringify(
|
||||
handoff.releases.map((release) => ({
|
||||
name: release.packageName,
|
||||
version: release.version,
|
||||
})),
|
||||
),
|
||||
);
|
||||
}
|
||||
+71
-28
@@ -1,11 +1,15 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import * as core from "@actions/core";
|
||||
import { exec, getExecOutput } from "@actions/exec";
|
||||
import * as github from "@actions/github";
|
||||
import { commitChangesFromRepo } from "@changesets/ghcommit/git";
|
||||
import type { Octokit } from "./octokit.ts";
|
||||
import { context } from "@actions/github";
|
||||
import { commitChangesSinceBase } from "@changesets/ghcommit";
|
||||
import { setupOctokit, type Octokit } from "./octokit.ts";
|
||||
|
||||
export type CommitMode = "git-cli" | "github-api";
|
||||
|
||||
type GitOptions = {
|
||||
cwd: string;
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
|
||||
const push = async (branch: string, options: GitOptions) => {
|
||||
@@ -46,17 +50,51 @@ const checkIfClean = async (options: GitOptions): Promise<boolean> => {
|
||||
return !stdout.length;
|
||||
};
|
||||
|
||||
export class Git {
|
||||
readonly octokit: Octokit | null;
|
||||
export class GitHub {
|
||||
readonly #githubToken: string;
|
||||
readonly octokit: Octokit;
|
||||
readonly cwd: string;
|
||||
readonly commitMode: CommitMode;
|
||||
|
||||
constructor(args: { octokit?: Octokit; cwd: string }) {
|
||||
this.octokit = args.octokit ?? null;
|
||||
this.cwd = args.cwd;
|
||||
constructor(options: {
|
||||
githubToken: string;
|
||||
cwd: string;
|
||||
commitMode?: CommitMode;
|
||||
}) {
|
||||
this.#githubToken = options.githubToken;
|
||||
this.cwd = options.cwd;
|
||||
this.commitMode = options.commitMode ?? "git-cli";
|
||||
this.octokit = setupOctokit(options.githubToken);
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return this.#githubToken;
|
||||
}
|
||||
|
||||
#getCliAuthEnv(): Record<string, string> {
|
||||
const basic = Buffer.from(`x-access-token:${this.#githubToken}`).toString(
|
||||
"base64",
|
||||
);
|
||||
const serverUrl = (
|
||||
context.serverUrl ??
|
||||
process.env.GITHUB_SERVER_URL ??
|
||||
"https://github.com"
|
||||
).replace(/\/+$/, "");
|
||||
const gitConfigCount = Number(process.env.GIT_CONFIG_COUNT ?? 0);
|
||||
if (!Number.isInteger(gitConfigCount) || gitConfigCount < 0) {
|
||||
throw new Error(
|
||||
`Invalid GIT_CONFIG_COUNT value: ${process.env.GIT_CONFIG_COUNT}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
GIT_CONFIG_COUNT: String(gitConfigCount + 1),
|
||||
[`GIT_CONFIG_KEY_${gitConfigCount}`]: `http.${serverUrl}/.extraheader`,
|
||||
[`GIT_CONFIG_VALUE_${gitConfigCount}`]: `AUTHORIZATION: basic ${basic}`,
|
||||
};
|
||||
}
|
||||
|
||||
async setupUser() {
|
||||
if (this.octokit) {
|
||||
if (this.commitMode === "github-api") {
|
||||
return;
|
||||
}
|
||||
await exec("git", ["config", "user.name", `"github-actions[bot]"`], {
|
||||
@@ -76,54 +114,59 @@ export class Git {
|
||||
}
|
||||
|
||||
async pushTag(tag: string) {
|
||||
if (this.octokit) {
|
||||
if (this.commitMode === "github-api") {
|
||||
return this.octokit.rest.git
|
||||
.createRef({
|
||||
...github.context.repo,
|
||||
...context.repo,
|
||||
ref: `refs/tags/${tag}`,
|
||||
sha: github.context.sha,
|
||||
sha: context.sha,
|
||||
})
|
||||
.catch((err) => {
|
||||
// Assuming tag was manually pushed in custom publish script
|
||||
core.warning(`Failed to create tag ${tag}: ${err.message}`);
|
||||
});
|
||||
}
|
||||
await exec("git", ["push", "origin", tag], { cwd: this.cwd });
|
||||
await exec("git", ["push", "origin", tag], {
|
||||
cwd: this.cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
...this.#getCliAuthEnv(),
|
||||
} as Record<string, string>,
|
||||
});
|
||||
}
|
||||
|
||||
async prepareBranch(branch: string) {
|
||||
if (this.octokit) {
|
||||
if (this.commitMode === "github-api") {
|
||||
// Preparing a new local branch is not necessary when using the API
|
||||
return;
|
||||
}
|
||||
await switchToMaybeExistingBranch(branch, { cwd: this.cwd });
|
||||
await reset(github.context.sha, { cwd: this.cwd });
|
||||
await reset(context.sha, { cwd: this.cwd });
|
||||
}
|
||||
|
||||
async pushChanges({ branch, message }: { branch: string; message: string }) {
|
||||
if (this.octokit) {
|
||||
/**
|
||||
* Only add files form the current working directory
|
||||
*
|
||||
* This will emulate the behavior of `git add .`,
|
||||
* used in {@link commitAll}.
|
||||
*/
|
||||
const addFromDirectory = this.cwd;
|
||||
return commitChangesFromRepo({
|
||||
if (this.commitMode === "github-api") {
|
||||
await commitChangesSinceBase({
|
||||
octokit: this.octokit,
|
||||
...github.context.repo,
|
||||
...context.repo,
|
||||
branch,
|
||||
message,
|
||||
base: {
|
||||
commit: github.context.sha,
|
||||
commit: context.sha,
|
||||
},
|
||||
cwd: this.cwd,
|
||||
force: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!(await checkIfClean({ cwd: this.cwd }))) {
|
||||
await commitAll(message, { cwd: this.cwd });
|
||||
}
|
||||
await push(branch, { cwd: this.cwd });
|
||||
await push(branch, {
|
||||
cwd: this.cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
...this.#getCliAuthEnv(),
|
||||
} as Record<string, string>,
|
||||
});
|
||||
}
|
||||
}
|
||||
+71
-46
@@ -1,59 +1,69 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import { Git } from "./git.ts";
|
||||
import { setupOctokit } from "./octokit.ts";
|
||||
import { GitHub } from "./github.ts";
|
||||
import readChangesetState from "./readChangesetState.ts";
|
||||
import { runPublish, runVersion } from "./run.ts";
|
||||
import { fileExists } from "./utils.ts";
|
||||
|
||||
const getOptionalInput = (name: string) => core.getInput(name) || undefined;
|
||||
import { uploadStagedRelease } from "./staged-release.ts";
|
||||
import {
|
||||
fileExists,
|
||||
getOptionalInput,
|
||||
getRequiredInput,
|
||||
throwOnRenamedInputs,
|
||||
validateChangesetsCliVersion,
|
||||
} from "./utils.ts";
|
||||
|
||||
(async () => {
|
||||
// to maintain compatibility with workflows created before github-token input was introduced
|
||||
// it's important to prefer the explicitly set GITHUB_TOKEN over the default token coming from github.token
|
||||
let githubToken = process.env.GITHUB_TOKEN || core.getInput("github-token");
|
||||
|
||||
if (!githubToken) {
|
||||
core.setFailed("Please add the GITHUB_TOKEN to the changesets action");
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||
const cwd = process.cwd();
|
||||
await validateChangesetsCliVersion(cwd);
|
||||
|
||||
const octokit = setupOctokit(githubToken);
|
||||
const commitMode = getOptionalInput("commitMode") ?? "git-cli";
|
||||
const prDraft = getOptionalInput("prDraft");
|
||||
throwOnRenamedInputs({
|
||||
publish: "publish-script",
|
||||
version: "version-script",
|
||||
commit: "commit-mesage",
|
||||
title: "pr-title",
|
||||
branch: "pr-base-branch",
|
||||
prDraft: "pr-draft",
|
||||
createGithubReleases: "create-github-releases",
|
||||
commitMode: "commit-mode",
|
||||
setupGitUser: "setup-git-user",
|
||||
});
|
||||
|
||||
const githubToken = getRequiredInput("github-token");
|
||||
if (process.env.GITHUB_TOKEN && process.env.GITHUB_TOKEN !== githubToken) {
|
||||
throw new Error(
|
||||
'The GITHUB_TOKEN environment variable is set and does not match the "github-token" input. ' +
|
||||
'Please pass the custom GitHub token to the "github-token" input and ' +
|
||||
"remove the GITHUB_TOKEN environment variable to avoid conflicts.",
|
||||
);
|
||||
}
|
||||
|
||||
const commitMode = getOptionalInput("commit-mode") ?? "git-cli";
|
||||
const prDraft = getOptionalInput("pr-draft");
|
||||
if (commitMode !== "git-cli" && commitMode !== "github-api") {
|
||||
core.setFailed(`Invalid commit mode: ${commitMode}`);
|
||||
return;
|
||||
}
|
||||
if (prDraft !== undefined && prDraft !== "always" && prDraft !== "create") {
|
||||
core.setFailed(`Invalid prDraft: ${prDraft}`);
|
||||
core.setFailed(`Invalid pr-draft: ${prDraft}`);
|
||||
return;
|
||||
}
|
||||
const git = new Git({
|
||||
octokit: commitMode === "github-api" ? octokit : undefined,
|
||||
const github = new GitHub({
|
||||
cwd,
|
||||
githubToken,
|
||||
commitMode,
|
||||
});
|
||||
|
||||
let setupGitUser = core.getBooleanInput("setupGitUser");
|
||||
let setupGitUser = core.getBooleanInput("setup-git-user");
|
||||
|
||||
if (setupGitUser) {
|
||||
core.info("setting git user");
|
||||
await git.setupUser();
|
||||
await github.setupUser();
|
||||
}
|
||||
|
||||
core.info("setting GitHub credentials");
|
||||
await fs.writeFile(
|
||||
`${process.env.HOME}/.netrc`,
|
||||
`machine github.com\nlogin github-actions[bot]\npassword ${githubToken}`,
|
||||
);
|
||||
|
||||
let { changesets } = await readChangesetState(cwd);
|
||||
|
||||
let publishScript = core.getInput("publish");
|
||||
let publishScript = core.getInput("publish-script");
|
||||
let hasChangesets = changesets.length !== 0;
|
||||
const hasNonEmptyChangesets = changesets.some(
|
||||
(changeset) => changeset.releases.length > 0,
|
||||
@@ -61,8 +71,8 @@ const getOptionalInput = (name: string) => core.getInput(name) || undefined;
|
||||
let hasPublishScript = !!publishScript;
|
||||
|
||||
core.setOutput("published", "false");
|
||||
core.setOutput("publishedPackages", "[]");
|
||||
core.setOutput("hasChangesets", String(hasChangesets));
|
||||
core.setOutput("published-packages", "[]");
|
||||
core.setOutput("has-changesets", String(hasChangesets));
|
||||
|
||||
switch (true) {
|
||||
case !hasChangesets && !hasPublishScript:
|
||||
@@ -120,19 +130,37 @@ const getOptionalInput = (name: string) => core.getInput(name) || undefined;
|
||||
);
|
||||
}
|
||||
|
||||
const createGithubReleases = core.getBooleanInput(
|
||||
"create-github-releases",
|
||||
);
|
||||
const pushGitTags = core.getBooleanInput("push-git-tags");
|
||||
if (createGithubReleases && !pushGitTags) {
|
||||
throw new Error(
|
||||
"The input 'create-github-releases' is set to true, but 'push-git-tags' is set to false. " +
|
||||
"Creating GitHub releases requires pushing git tags. Please set 'push-git-tags' to true " +
|
||||
"or set 'create-github-releases' to false.",
|
||||
);
|
||||
}
|
||||
const result = await runPublish({
|
||||
script: publishScript,
|
||||
githubToken,
|
||||
git,
|
||||
octokit,
|
||||
createGithubReleases: core.getBooleanInput("createGithubReleases"),
|
||||
github,
|
||||
createGithubReleases,
|
||||
pushGitTags,
|
||||
cwd,
|
||||
});
|
||||
|
||||
if (result.stagedPackages?.length) {
|
||||
const artifactId = await uploadStagedRelease(
|
||||
result.stagedPackages,
|
||||
result.exitCode === 0 ? "approve" : "reject",
|
||||
);
|
||||
core.setOutput("staged-release-artifact-id", String(artifactId));
|
||||
}
|
||||
|
||||
if (result.published) {
|
||||
core.setOutput("published", "true");
|
||||
core.setOutput(
|
||||
"publishedPackages",
|
||||
"published-packages",
|
||||
JSON.stringify(result.publishedPackages),
|
||||
);
|
||||
}
|
||||
@@ -155,21 +183,18 @@ const getOptionalInput = (name: string) => core.getInput(name) || undefined;
|
||||
core.info("All changesets are empty; not creating PR");
|
||||
return;
|
||||
case hasChangesets: {
|
||||
const octokit = setupOctokit(githubToken);
|
||||
const { pullRequestNumber } = await runVersion({
|
||||
script: getOptionalInput("version"),
|
||||
githubToken,
|
||||
git,
|
||||
octokit,
|
||||
script: getOptionalInput("version-script"),
|
||||
github,
|
||||
cwd,
|
||||
prTitle: getOptionalInput("title"),
|
||||
commitMessage: getOptionalInput("commit"),
|
||||
prTitle: getOptionalInput("pr-title"),
|
||||
commitMessage: getOptionalInput("commit-message"),
|
||||
hasPublishScript,
|
||||
prDraft,
|
||||
branch: getOptionalInput("branch"),
|
||||
branch: getOptionalInput("pr-base-branch"),
|
||||
});
|
||||
|
||||
core.setOutput("pullRequestNumber", String(pullRequestNumber));
|
||||
core.setOutput("pr-number", String(pullRequestNumber));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
+11
-4
@@ -3,7 +3,12 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import artifact from "@actions/artifact";
|
||||
import * as core from "@actions/core";
|
||||
import { downloadArtifact, execChangesetsCli } from "../utils.ts";
|
||||
import {
|
||||
downloadArtifact,
|
||||
execChangesetsCli,
|
||||
getOptionalInput,
|
||||
validateChangesetsCliVersion,
|
||||
} from "../utils.ts";
|
||||
|
||||
try {
|
||||
await main();
|
||||
@@ -12,10 +17,12 @@ try {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const publishPlanArtifactId = core.getInput("publish-plan-artifact-id");
|
||||
|
||||
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||
const cwd = process.cwd();
|
||||
await validateChangesetsCliVersion(cwd);
|
||||
|
||||
const publishPlanArtifactId = getOptionalInput("publish-plan-artifact-id");
|
||||
|
||||
const tmpDir = process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir()));
|
||||
const outDir = path.join(tmpDir, `changeset-pack-${Date.now()}`);
|
||||
|
||||
@@ -49,7 +56,7 @@ async function pack(
|
||||
) {
|
||||
const cliArgs = ["pack", "--out-dir", args.outDir];
|
||||
if (args.publishPlanPath) {
|
||||
cliArgs.push("--from-plan", args.publishPlanPath);
|
||||
cliArgs.push("--from-publish-plan", args.publishPlanPath);
|
||||
}
|
||||
|
||||
await execChangesetsCli(cliArgs, {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as github from "@actions/github";
|
||||
import { getOptionalInput, getRequiredInput } from "../utils.ts";
|
||||
|
||||
type Octokit = ReturnType<typeof github.getOctokit>;
|
||||
type CreateCommentParams = NonNullable<
|
||||
@@ -23,9 +24,9 @@ async function main() {
|
||||
);
|
||||
}
|
||||
|
||||
const githubToken = core.getInput("github-token", { required: true });
|
||||
const body = core.getInput("body", { required: true });
|
||||
const updateId = core.getInput("update-id");
|
||||
const githubToken = getRequiredInput("github-token");
|
||||
const body = getRequiredInput("body");
|
||||
const updateId = getOptionalInput("update-id");
|
||||
|
||||
const commentMarker = updateId ? getCommentMarker(updateId) : null;
|
||||
const commentBody = commentMarker ? `${commentMarker}\n\n${body}` : body;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as github from "@actions/github";
|
||||
import getReleasePlan from "@changesets/get-release-plan";
|
||||
import { getReleasePlan } from "@changesets/get-release-plan";
|
||||
import type {
|
||||
ComprehensiveRelease,
|
||||
ReleasePlan,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { pathToFileURL } from "node:url";
|
||||
import type * as github from "@actions/github";
|
||||
import getReleasePlan from "@changesets/get-release-plan";
|
||||
import { getReleasePlan } from "@changesets/get-release-plan";
|
||||
import { createFixture } from "fs-fixture";
|
||||
import { exec } from "tinyexec";
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -28,6 +28,7 @@ describe("getPullRequestWorktree", () => {
|
||||
private: true,
|
||||
workspaces: ["packages/*"],
|
||||
}),
|
||||
"package-lock.json": "",
|
||||
"packages/pkg-a/package.json": JSON.stringify({
|
||||
name: "pkg-a",
|
||||
version: "1.0.0",
|
||||
|
||||
+41
-14
@@ -1,10 +1,17 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import * as core from "@actions/core";
|
||||
import { Git } from "../git.ts";
|
||||
import { setupOctokit } from "../octokit.ts";
|
||||
import { GitHub } from "../github.ts";
|
||||
import { runPublish } from "../run.ts";
|
||||
import { downloadArtifact } from "../utils.ts";
|
||||
import { uploadStagedRelease } from "../staged-release.ts";
|
||||
import {
|
||||
downloadArtifact,
|
||||
getOptionalBooleanInput,
|
||||
getOptionalInput,
|
||||
getRequiredInput,
|
||||
validateChangesetsCliVersion,
|
||||
} from "../utils.ts";
|
||||
import { assertValidStageInput } from "./options.ts";
|
||||
|
||||
try {
|
||||
await main();
|
||||
@@ -13,17 +20,29 @@ try {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const githubToken = core.getInput("github-token", { required: true });
|
||||
const script = core.getInput("script");
|
||||
const packDirArtifactId = core.getInput("pack-dir-artifact-id");
|
||||
const createGithubReleases = core.getBooleanInput("create-github-releases");
|
||||
|
||||
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||
const cwd = process.cwd();
|
||||
await validateChangesetsCliVersion(cwd);
|
||||
|
||||
const octokit = setupOctokit(githubToken);
|
||||
// NOTE: Always pass octokit here as publish does not need a commit-mode
|
||||
const git = new Git({ octokit, cwd });
|
||||
const githubToken = getRequiredInput("github-token");
|
||||
const script = getOptionalInput("script");
|
||||
const stage = getOptionalBooleanInput("stage");
|
||||
const packDirArtifactId = getOptionalInput("pack-dir-artifact-id");
|
||||
const createGithubReleases = core.getBooleanInput("create-github-releases");
|
||||
const pushGitTags = core.getBooleanInput("push-git-tags");
|
||||
|
||||
assertValidStageInput(stage, script);
|
||||
|
||||
if (createGithubReleases && !pushGitTags) {
|
||||
throw new Error(
|
||||
"The input 'create-github-releases' is set to true, but 'push-git-tags' is set to false. " +
|
||||
"Creating GitHub releases requires pushing git tags. Please set 'push-git-tags' to true " +
|
||||
"or set 'create-github-releases' to false.",
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: Always use API mode here as publish does not need a commit-mode.
|
||||
const github = new GitHub({ cwd, githubToken, commitMode: "github-api" });
|
||||
|
||||
const fromPackDir = packDirArtifactId
|
||||
? await downloadArtifact(
|
||||
@@ -35,14 +54,22 @@ async function main() {
|
||||
|
||||
const result = await runPublish({
|
||||
script,
|
||||
githubToken,
|
||||
git,
|
||||
octokit,
|
||||
stage,
|
||||
github,
|
||||
createGithubReleases,
|
||||
pushGitTags,
|
||||
cwd,
|
||||
fromPackDir,
|
||||
});
|
||||
|
||||
if (result.stagedPackages?.length) {
|
||||
const artifactId = await uploadStagedRelease(
|
||||
result.stagedPackages,
|
||||
result.exitCode === 0 ? "approve" : "reject",
|
||||
);
|
||||
core.setOutput("staged-release-artifact-id", String(artifactId));
|
||||
}
|
||||
|
||||
if (result.published) {
|
||||
core.setOutput("published", "true");
|
||||
core.setOutput(
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { assertValidStageInput } from "./options.ts";
|
||||
|
||||
describe("publish stage input", () => {
|
||||
it("rejects an explicit stage override with a custom script", () => {
|
||||
expect(() => assertValidStageInput(false, "pnpm release")).toThrow(
|
||||
"cannot be combined with a custom 'script'",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows an omitted stage input with a custom script", () => {
|
||||
expect(() =>
|
||||
assertValidStageInput(undefined, "pnpm release"),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
export function assertValidStageInput(
|
||||
stage: boolean | undefined,
|
||||
script: string | undefined,
|
||||
) {
|
||||
if (stage !== undefined && script) {
|
||||
throw new Error(
|
||||
"The 'stage' input cannot be combined with a custom 'script'. Configure staged publishing inside the script instead.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readPreState } from "@changesets/pre";
|
||||
import readChangesets from "@changesets/read";
|
||||
import { readChangesets } from "@changesets/read";
|
||||
import type { PreState, NewChangeset } from "@changesets/types";
|
||||
|
||||
export type ChangesetState = {
|
||||
|
||||
+174
-31
@@ -1,11 +1,11 @@
|
||||
import path from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import type { Changeset } from "@changesets/types";
|
||||
import writeChangeset from "@changesets/write";
|
||||
import { writeChangeset } from "@changesets/write";
|
||||
import { createFixture } from "fs-fixture";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { Git } from "./git.ts";
|
||||
import { setupOctokit } from "./octokit.ts";
|
||||
import { runVersion } from "./run.ts";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { GitHub } from "./github.ts";
|
||||
import { readChangesetsOutput, runPublish, runVersion } from "./run.ts";
|
||||
|
||||
vi.mock("@actions/github", () => ({
|
||||
context: {
|
||||
@@ -21,8 +21,9 @@ vi.mock("@actions/github", () => ({
|
||||
graphql: mockedGraphql,
|
||||
}),
|
||||
}));
|
||||
vi.mock("./git.ts");
|
||||
vi.mock("@changesets/ghcommit/git");
|
||||
vi.mock("@actions/core", { spy: true });
|
||||
const mockedWarning = vi.mocked(core.warning);
|
||||
vi.mock("@changesets/ghcommit");
|
||||
|
||||
let mockedGithubMethods = {
|
||||
pulls: {
|
||||
@@ -58,6 +59,7 @@ function createSimpleProjectFixture() {
|
||||
private: true,
|
||||
workspaces: ["packages/*"],
|
||||
}),
|
||||
"package-lock.json": "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,6 +86,7 @@ function createIgnoredPackageFixture() {
|
||||
private: true,
|
||||
workspaces: ["packages/*"],
|
||||
}),
|
||||
"package-lock.json": "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,10 +94,166 @@ const writeChangesets = (changesets: Changeset[], cwd: string) => {
|
||||
return Promise.all(changesets.map((commit) => writeChangeset(commit, cwd)));
|
||||
};
|
||||
|
||||
const createGithub = (cwd: string) =>
|
||||
new GitHub({
|
||||
cwd,
|
||||
githubToken: "@@GITHUB_TOKEN",
|
||||
commitMode: "github-api",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
describe("publish", () => {
|
||||
it("parses staged publish events in emitted order", async () => {
|
||||
await using fixture = await createFixture({
|
||||
"changesets-output.ndjson": [
|
||||
JSON.stringify({
|
||||
type: "npm-stage",
|
||||
packageName: "pkg-b",
|
||||
version: "1.0.0",
|
||||
tag: "latest",
|
||||
gitTag: "pkg-b@1.0.0",
|
||||
stageId: "stage-b",
|
||||
}),
|
||||
JSON.stringify({ type: "unknown" }),
|
||||
JSON.stringify({
|
||||
type: "npm-stage",
|
||||
packageName: "pkg-a",
|
||||
version: "1.0.0",
|
||||
tag: "latest",
|
||||
gitTag: "pkg-a@1.0.0",
|
||||
stageId: "stage-a",
|
||||
}),
|
||||
].join("\n"),
|
||||
});
|
||||
|
||||
await expect(
|
||||
readChangesetsOutput(path.join(fixture.path, "changesets-output.ndjson")),
|
||||
).resolves.toMatchObject([
|
||||
{ type: "npm-stage", stageId: "stage-b" },
|
||||
{ type: "npm-stage", stageId: "stage-a" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns staged packages from a custom publish script", async () => {
|
||||
await using fixture = await createSimpleProjectFixture();
|
||||
const cwd = fixture.path;
|
||||
vi.stubEnv("RUNNER_TEMP", cwd);
|
||||
await fixture.writeFile(
|
||||
"write-output.cjs",
|
||||
`require("node:fs").writeFileSync(process.env.CHANGESETS_OUTPUT, JSON.stringify({
|
||||
type: "npm-stage",
|
||||
packageName: "changesets-dev-simple-project-pkg-b",
|
||||
version: "1.0.0",
|
||||
tag: "latest",
|
||||
gitTag: "changesets-dev-simple-project-pkg-b@1.0.0",
|
||||
stageId: "stage-b"
|
||||
}) + "\\n")`,
|
||||
);
|
||||
|
||||
const result = await runPublish({
|
||||
script: "node write-output.cjs",
|
||||
github: createGithub(cwd),
|
||||
createGithubReleases: false,
|
||||
pushGitTags: false,
|
||||
cwd,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
published: false,
|
||||
exitCode: 0,
|
||||
stagedPackages: [{ type: "npm-stage", stageId: "stage-b" }],
|
||||
});
|
||||
expect(mockedGithubMethods.repos.createRelease).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes an explicit stage override to the built-in CLI", async () => {
|
||||
await using fixture = await createFixture({
|
||||
"node_modules/@changesets/cli/package.json": JSON.stringify({
|
||||
name: "@changesets/cli",
|
||||
type: "module",
|
||||
}),
|
||||
"node_modules/@changesets/cli/bin.js": `
|
||||
import fs from "node:fs";
|
||||
fs.writeFileSync("args.json", JSON.stringify(process.argv.slice(2)));
|
||||
fs.writeFileSync(process.env.CHANGESETS_OUTPUT, "");
|
||||
`,
|
||||
"package.json": JSON.stringify({
|
||||
name: "simple-project",
|
||||
version: "1.0.0",
|
||||
}),
|
||||
"package-lock.json": "",
|
||||
});
|
||||
const cwd = fixture.path;
|
||||
vi.stubEnv("RUNNER_TEMP", cwd);
|
||||
|
||||
await runPublish({
|
||||
stage: false,
|
||||
github: createGithub(cwd),
|
||||
createGithubReleases: false,
|
||||
pushGitTags: false,
|
||||
cwd,
|
||||
});
|
||||
|
||||
await expect(
|
||||
fixture.readFile("args.json", "utf8").then(JSON.parse),
|
||||
).resolves.toEqual(["publish", "--no-stage"]);
|
||||
});
|
||||
|
||||
it("warns when a custom publish script does not create the output file", async () => {
|
||||
await using fixture = await createSimpleProjectFixture();
|
||||
const cwd = fixture.path;
|
||||
vi.stubEnv("RUNNER_TEMP", cwd);
|
||||
|
||||
const result = await runPublish({
|
||||
script: 'node -e "void 0"',
|
||||
github: createGithub(cwd),
|
||||
createGithubReleases: true,
|
||||
pushGitTags: true,
|
||||
cwd,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ published: false, exitCode: 0 });
|
||||
expect(mockedWarning).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"GitHub releases and git tags cannot be created without this output",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when the built-in publish command does not create the output file", async () => {
|
||||
await using fixture = await createFixture({
|
||||
"node_modules/@changesets/cli/package.json": JSON.stringify({
|
||||
name: "@changesets/cli",
|
||||
type: "module",
|
||||
}),
|
||||
"node_modules/@changesets/cli/bin.js": "",
|
||||
"package.json": JSON.stringify({
|
||||
name: "simple-project",
|
||||
version: "1.0.0",
|
||||
}),
|
||||
"package-lock.json": "",
|
||||
});
|
||||
const cwd = fixture.path;
|
||||
vi.stubEnv("RUNNER_TEMP", cwd);
|
||||
|
||||
await expect(
|
||||
runPublish({
|
||||
github: createGithub(cwd),
|
||||
createGithubReleases: true,
|
||||
pushGitTags: true,
|
||||
cwd,
|
||||
}),
|
||||
).rejects.toThrow("Failed to read changesets output at");
|
||||
});
|
||||
});
|
||||
|
||||
describe("version", () => {
|
||||
it("creates simple PR", async () => {
|
||||
await using fixture = await createSimpleProjectFixture();
|
||||
@@ -126,9 +285,7 @@ describe("version", () => {
|
||||
);
|
||||
|
||||
await runVersion({
|
||||
octokit: setupOctokit("@@GITHUB_TOKEN"),
|
||||
githubToken: "@@GITHUB_TOKEN",
|
||||
git: new Git({ cwd }),
|
||||
github: createGithub(cwd),
|
||||
cwd,
|
||||
});
|
||||
|
||||
@@ -161,9 +318,7 @@ describe("version", () => {
|
||||
);
|
||||
|
||||
await runVersion({
|
||||
octokit: setupOctokit("@@GITHUB_TOKEN"),
|
||||
githubToken: "@@GITHUB_TOKEN",
|
||||
git: new Git({ cwd }),
|
||||
github: createGithub(cwd),
|
||||
cwd,
|
||||
prDraft: "create",
|
||||
});
|
||||
@@ -197,9 +352,7 @@ describe("version", () => {
|
||||
);
|
||||
|
||||
await runVersion({
|
||||
octokit: setupOctokit("@@GITHUB_TOKEN"),
|
||||
githubToken: "@@GITHUB_TOKEN",
|
||||
git: new Git({ cwd }),
|
||||
github: createGithub(cwd),
|
||||
cwd,
|
||||
});
|
||||
|
||||
@@ -232,9 +385,7 @@ describe("version", () => {
|
||||
);
|
||||
|
||||
await runVersion({
|
||||
octokit: setupOctokit("@@GITHUB_TOKEN"),
|
||||
githubToken: "@@GITHUB_TOKEN",
|
||||
git: new Git({ cwd }),
|
||||
github: createGithub(cwd),
|
||||
cwd,
|
||||
});
|
||||
|
||||
@@ -287,9 +438,7 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis.
|
||||
);
|
||||
|
||||
await runVersion({
|
||||
octokit: setupOctokit("@@GITHUB_TOKEN"),
|
||||
githubToken: "@@GITHUB_TOKEN",
|
||||
git: new Git({ cwd }),
|
||||
github: createGithub(cwd),
|
||||
cwd,
|
||||
prBodyMaxCharacters: 1000,
|
||||
});
|
||||
@@ -346,9 +495,7 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis.
|
||||
);
|
||||
|
||||
await runVersion({
|
||||
octokit: setupOctokit("@@GITHUB_TOKEN"),
|
||||
githubToken: "@@GITHUB_TOKEN",
|
||||
git: new Git({ cwd }),
|
||||
github: createGithub(cwd),
|
||||
cwd,
|
||||
prBodyMaxCharacters: 500,
|
||||
});
|
||||
@@ -383,9 +530,7 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis.
|
||||
);
|
||||
|
||||
await runVersion({
|
||||
octokit: setupOctokit("@@GITHUB_TOKEN"),
|
||||
githubToken: "@@GITHUB_TOKEN",
|
||||
git: new Git({ cwd }),
|
||||
github: createGithub(cwd),
|
||||
cwd,
|
||||
prDraft: "create",
|
||||
});
|
||||
@@ -417,9 +562,7 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis.
|
||||
);
|
||||
|
||||
await runVersion({
|
||||
octokit: setupOctokit("@@GITHUB_TOKEN"),
|
||||
githubToken: "@@GITHUB_TOKEN",
|
||||
git: new Git({ cwd }),
|
||||
github: createGithub(cwd),
|
||||
cwd,
|
||||
prDraft: "always",
|
||||
});
|
||||
|
||||
+196
-83
@@ -1,4 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import {
|
||||
@@ -7,10 +9,10 @@ import {
|
||||
type ExecOptions,
|
||||
type ExecOutput,
|
||||
} from "@actions/exec";
|
||||
import * as github from "@actions/github";
|
||||
import { context } from "@actions/github";
|
||||
import type { PreState } from "@changesets/types";
|
||||
import { type Package, getPackages } from "@manypkg/get-packages";
|
||||
import { Git } from "./git.ts";
|
||||
import type { GitHub } from "./github.ts";
|
||||
import type { Octokit } from "./octokit.ts";
|
||||
import readChangesetState from "./readChangesetState.ts";
|
||||
import {
|
||||
@@ -29,7 +31,7 @@ import {
|
||||
// To avoid that, we ensure to cap the message to 60k chars.
|
||||
const MAX_CHARACTERS_PER_MESSAGE = 60000;
|
||||
|
||||
const createRelease = async (
|
||||
export const createRelease = async (
|
||||
octokit: Octokit,
|
||||
{ pkg, tagName }: { pkg: Package; tagName: string },
|
||||
) => {
|
||||
@@ -57,47 +59,156 @@ const createRelease = async (
|
||||
tag_name: tagName,
|
||||
body: changelogEntry.content,
|
||||
prerelease: pkg.packageJson.version.includes("-"),
|
||||
...github.context.repo,
|
||||
...context.repo,
|
||||
});
|
||||
};
|
||||
|
||||
type PublishOptions = {
|
||||
script?: string;
|
||||
fromPackDir?: string;
|
||||
githubToken: string;
|
||||
octokit: Octokit;
|
||||
stage?: boolean;
|
||||
createGithubReleases: boolean;
|
||||
git: Git;
|
||||
pushGitTags: boolean;
|
||||
github: GitHub;
|
||||
cwd: string;
|
||||
};
|
||||
|
||||
type PublishedPackage = { name: string; version: string };
|
||||
export type ChangesetsOutputEvent =
|
||||
| {
|
||||
type: "git-tag";
|
||||
tag: string;
|
||||
packageName: string;
|
||||
}
|
||||
| {
|
||||
type: "npm-stage";
|
||||
packageName: string;
|
||||
version: string;
|
||||
tag: string;
|
||||
gitTag: string;
|
||||
stageId: string;
|
||||
};
|
||||
|
||||
export type NpmStageEvent = Extract<
|
||||
ChangesetsOutputEvent,
|
||||
{ type: "npm-stage" }
|
||||
>;
|
||||
|
||||
class ChangesetsOutputReadError extends Error {}
|
||||
|
||||
type PublishResult =
|
||||
| {
|
||||
published: true;
|
||||
publishedPackages: PublishedPackage[];
|
||||
stagedPackages?: NpmStageEvent[];
|
||||
exitCode: number;
|
||||
}
|
||||
| {
|
||||
published: false;
|
||||
stagedPackages?: NpmStageEvent[];
|
||||
exitCode: number;
|
||||
};
|
||||
|
||||
function isObject(value: unknown) {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isChangesetsOutputEvent(
|
||||
value: unknown,
|
||||
): value is ChangesetsOutputEvent {
|
||||
if (!isObject(value) || !("type" in value)) return false;
|
||||
if (
|
||||
value.type === "git-tag" &&
|
||||
"tag" in value &&
|
||||
typeof value.tag === "string" &&
|
||||
"packageName" in value &&
|
||||
typeof value.packageName === "string"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
value.type === "npm-stage" &&
|
||||
"packageName" in value &&
|
||||
typeof value.packageName === "string" &&
|
||||
"version" in value &&
|
||||
typeof value.version === "string" &&
|
||||
"tag" in value &&
|
||||
typeof value.tag === "string" &&
|
||||
"gitTag" in value &&
|
||||
typeof value.gitTag === "string" &&
|
||||
"stageId" in value &&
|
||||
typeof value.stageId === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export async function readChangesetsOutput(outputPath: string) {
|
||||
let rawOutput: string;
|
||||
try {
|
||||
rawOutput = await fs.readFile(outputPath, "utf8");
|
||||
} catch (err) {
|
||||
throw new ChangesetsOutputReadError(
|
||||
`Failed to read changesets output at ${outputPath}`,
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
|
||||
const events: ChangesetsOutputEvent[] = [];
|
||||
|
||||
let lineStart = 0;
|
||||
while (lineStart <= rawOutput.length) {
|
||||
let lineEnd = rawOutput.indexOf("\n", lineStart);
|
||||
if (lineEnd === -1) {
|
||||
lineEnd = rawOutput.length;
|
||||
}
|
||||
const line = rawOutput.slice(lineStart, lineEnd);
|
||||
lineStart = lineEnd + 1;
|
||||
|
||||
if (/^\s*$/.test(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let event: unknown;
|
||||
try {
|
||||
event = JSON.parse(line);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to parse changesets output event: ${line}`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isChangesetsOutputEvent(event)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
export async function runPublish({
|
||||
script,
|
||||
fromPackDir,
|
||||
githubToken,
|
||||
git,
|
||||
octokit,
|
||||
stage,
|
||||
github,
|
||||
createGithubReleases,
|
||||
pushGitTags,
|
||||
cwd,
|
||||
}: PublishOptions): Promise<PublishResult> {
|
||||
const { octokit } = github;
|
||||
let changesetPublishOutput: ExecOutput;
|
||||
const outputFile = path.join(
|
||||
process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir())),
|
||||
`changesets-output-${randomUUID()}.ndjson`,
|
||||
);
|
||||
const execOptions: ExecOptions = {
|
||||
cwd,
|
||||
ignoreReturnCode: true,
|
||||
env: { ...process.env, GITHUB_TOKEN: githubToken },
|
||||
env: {
|
||||
...process.env,
|
||||
GITHUB_TOKEN: github.getToken(),
|
||||
CHANGESETS_OUTPUT: outputFile,
|
||||
},
|
||||
};
|
||||
|
||||
if (script) {
|
||||
@@ -108,6 +219,9 @@ export async function runPublish({
|
||||
);
|
||||
} else {
|
||||
const args = ["publish"];
|
||||
if (stage !== undefined) {
|
||||
args.push(stage ? "--stage" : "--no-stage");
|
||||
}
|
||||
if (fromPackDir) {
|
||||
args.push("--from-pack-dir", fromPackDir);
|
||||
}
|
||||
@@ -117,75 +231,81 @@ export async function runPublish({
|
||||
);
|
||||
}
|
||||
|
||||
let { packages, tool } = await getPackages(cwd);
|
||||
let releasedPackages: Package[] = [];
|
||||
|
||||
if (tool !== "root") {
|
||||
let newTagRegex = /New tag:\s+(@[^/]+\/[^@]+|[^/]+)@([^\s]+)/;
|
||||
let packagesByName = new Map(packages.map((x) => [x.packageJson.name, x]));
|
||||
|
||||
for (let line of changesetPublishOutput.stdout.split("\n")) {
|
||||
let match = line.match(newTagRegex);
|
||||
if (match === null) {
|
||||
continue;
|
||||
}
|
||||
let pkgName = match[1];
|
||||
let pkg = packagesByName.get(pkgName);
|
||||
let { packages, rootPackage, tool } = await getPackages(cwd);
|
||||
let packagesByName = new Map(
|
||||
[...packages, ...(rootPackage ? [rootPackage] : [])].map((x) => [
|
||||
x.packageJson.name,
|
||||
x,
|
||||
]),
|
||||
);
|
||||
let output: ChangesetsOutputEvent[];
|
||||
try {
|
||||
output = await readChangesetsOutput(outputFile);
|
||||
} catch (err) {
|
||||
if (!script || !(err instanceof ChangesetsOutputReadError)) {
|
||||
throw err;
|
||||
}
|
||||
core.warning(
|
||||
`${err.message}. GitHub releases and git tags cannot be created without this output. Ensure the custom publish script passes CHANGESETS_OUTPUT to the Changesets CLI.`,
|
||||
);
|
||||
output = [];
|
||||
}
|
||||
const stagedPackages = output.filter(
|
||||
(event): event is NpmStageEvent => event.type === "npm-stage",
|
||||
);
|
||||
let releases = output
|
||||
.filter(
|
||||
(event): event is Extract<ChangesetsOutputEvent, { type: "git-tag" }> =>
|
||||
event.type === "git-tag",
|
||||
)
|
||||
.map((event) => {
|
||||
let pkg = packagesByName.get(event.packageName);
|
||||
if (pkg === undefined) {
|
||||
throw new Error(
|
||||
`Package "${pkgName}" not found.` +
|
||||
`Package "${event.packageName}" not found.` +
|
||||
"This is probably a bug in the action, please open an issue",
|
||||
);
|
||||
}
|
||||
releasedPackages.push(pkg);
|
||||
}
|
||||
return { pkg, tag: event.tag };
|
||||
});
|
||||
|
||||
if (createGithubReleases) {
|
||||
await Promise.all(
|
||||
releasedPackages.map(async (pkg) => {
|
||||
const tagName = `${pkg.packageJson.name}@${pkg.packageJson.version}`;
|
||||
await git.pushTag(tagName);
|
||||
await createRelease(octokit, { pkg, tagName });
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (packages.length === 0) {
|
||||
throw new Error(
|
||||
`No package found.` +
|
||||
"This is probably a bug in the action, please open an issue",
|
||||
);
|
||||
}
|
||||
let pkg = packages[0];
|
||||
let newTagRegex = /New tag:/;
|
||||
|
||||
for (let line of changesetPublishOutput.stdout.split("\n")) {
|
||||
let match = line.match(newTagRegex);
|
||||
|
||||
if (match) {
|
||||
releasedPackages.push(pkg);
|
||||
if (createGithubReleases) {
|
||||
const tagName = `v${pkg.packageJson.version}`;
|
||||
await git.pushTag(tagName);
|
||||
await createRelease(octokit, { pkg, tagName });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tool.type === "root" && packages.length === 0) {
|
||||
throw new Error(
|
||||
`No package found.` +
|
||||
"This is probably a bug in the action, please open an issue",
|
||||
);
|
||||
}
|
||||
|
||||
if (releasedPackages.length) {
|
||||
if (createGithubReleases || pushGitTags) {
|
||||
await Promise.all(
|
||||
releases.map(async ({ pkg, tag }) => {
|
||||
if (pushGitTags) {
|
||||
await github.pushTag(tag);
|
||||
}
|
||||
if (createGithubReleases) {
|
||||
await createRelease(octokit, { pkg, tagName: tag });
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (releases.length) {
|
||||
return {
|
||||
published: true,
|
||||
publishedPackages: releasedPackages.map((pkg) => ({
|
||||
publishedPackages: releases.map(({ pkg }) => ({
|
||||
name: pkg.packageJson.name,
|
||||
version: pkg.packageJson.version,
|
||||
})),
|
||||
...(stagedPackages.length > 0 ? { stagedPackages } : {}),
|
||||
exitCode: changesetPublishOutput.exitCode,
|
||||
};
|
||||
}
|
||||
|
||||
return { published: false, exitCode: changesetPublishOutput.exitCode };
|
||||
return {
|
||||
published: false,
|
||||
...(stagedPackages.length > 0 ? { stagedPackages } : {}),
|
||||
exitCode: changesetPublishOutput.exitCode,
|
||||
};
|
||||
}
|
||||
|
||||
type GetMessageOptions = {
|
||||
@@ -259,9 +379,7 @@ export async function getVersionPrBody({
|
||||
|
||||
type VersionOptions = {
|
||||
script?: string;
|
||||
githubToken: string;
|
||||
git: Git;
|
||||
octokit: Octokit;
|
||||
github: GitHub;
|
||||
cwd?: string;
|
||||
prTitle?: string;
|
||||
commitMessage?: string;
|
||||
@@ -277,26 +395,25 @@ type RunVersionResult = {
|
||||
|
||||
export async function runVersion({
|
||||
script,
|
||||
githubToken,
|
||||
git,
|
||||
octokit,
|
||||
github,
|
||||
cwd = process.cwd(),
|
||||
prTitle = "Version Packages",
|
||||
commitMessage = "Version Packages",
|
||||
hasPublishScript = false,
|
||||
prBodyMaxCharacters = MAX_CHARACTERS_PER_MESSAGE,
|
||||
branch = github.context.ref.replace("refs/heads/", ""),
|
||||
branch = context.ref.replace("refs/heads/", ""),
|
||||
prDraft,
|
||||
}: VersionOptions): Promise<RunVersionResult> {
|
||||
const { octokit } = github;
|
||||
let versionBranch = `changeset-release/${branch}`;
|
||||
|
||||
let { preState } = await readChangesetState(cwd);
|
||||
|
||||
await git.prepareBranch(versionBranch);
|
||||
await github.prepareBranch(versionBranch);
|
||||
|
||||
let versionsByDirectory = await getVersionsByDirectory(cwd);
|
||||
|
||||
const env = { ...process.env, GITHUB_TOKEN: githubToken };
|
||||
const env = { ...process.env, GITHUB_TOKEN: github.getToken() };
|
||||
|
||||
if (script) {
|
||||
await exec(script, undefined, { cwd, env });
|
||||
@@ -327,17 +444,10 @@ export async function runVersion({
|
||||
!!preState ? ` (${preState.tag})` : ""
|
||||
}`;
|
||||
|
||||
/**
|
||||
* Fetch any existing pull requests that are open against the branch,
|
||||
* before we push any changes that may inadvertently close the existing PRs.
|
||||
*
|
||||
* (`@changesets/ghcommit` has to reset the branch to the same commit as the base,
|
||||
* which GitHub will then react to by closing the PRs)
|
||||
*/
|
||||
const existingPullRequests = await octokit.rest.pulls.list({
|
||||
...github.context.repo,
|
||||
...context.repo,
|
||||
state: "open",
|
||||
head: `${github.context.repo.owner}:${versionBranch}`,
|
||||
head: `${context.repo.owner}:${versionBranch}`,
|
||||
base: branch,
|
||||
});
|
||||
core.info(
|
||||
@@ -348,7 +458,10 @@ export async function runVersion({
|
||||
)}`,
|
||||
);
|
||||
|
||||
await git.pushChanges({ branch: versionBranch, message: finalCommitMessage });
|
||||
await github.pushChanges({
|
||||
branch: versionBranch,
|
||||
message: finalCommitMessage,
|
||||
});
|
||||
|
||||
const changedPackagesInfo = (await changedPackagesInfoPromises)
|
||||
.filter((x) => x)
|
||||
@@ -370,7 +483,7 @@ export async function runVersion({
|
||||
title: finalPrTitle,
|
||||
body: prBody,
|
||||
draft: prDraft !== undefined,
|
||||
...github.context.repo,
|
||||
...context.repo,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from "node:path";
|
||||
import artifact from "@actions/artifact";
|
||||
import * as core from "@actions/core";
|
||||
import readChangesetState from "../readChangesetState.ts";
|
||||
import { execChangesetsCli } from "../utils.ts";
|
||||
import { execChangesetsCli, validateChangesetsCliVersion } from "../utils.ts";
|
||||
|
||||
type ModeResult =
|
||||
| {
|
||||
@@ -27,7 +27,10 @@ try {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const result = await getMode();
|
||||
const cwd = process.cwd();
|
||||
await validateChangesetsCliVersion(cwd);
|
||||
|
||||
const result = await getMode(cwd);
|
||||
core.setOutput("mode", result.mode);
|
||||
if (result.mode === "publish") {
|
||||
const publishPlanArtifact = await artifact.uploadArtifact(
|
||||
@@ -48,8 +51,8 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
async function getMode(): Promise<ModeResult> {
|
||||
const { changesets } = await readChangesetState();
|
||||
async function getMode(cwd: string): Promise<ModeResult> {
|
||||
const { changesets } = await readChangesetState(cwd);
|
||||
|
||||
if (changesets.length > 0) {
|
||||
const hasNonEmptyChangesets = changesets.some(
|
||||
@@ -61,10 +64,11 @@ async function getMode(): Promise<ModeResult> {
|
||||
return { mode: "none" };
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
const publishPlanPath = path.join(
|
||||
process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir())),
|
||||
`changeset-publish-plan-${Date.now()}.json`,
|
||||
`changeset-publish-plan-${Date.now()}`,
|
||||
// we need a stable filename here (in a unique dirname) so the artifact download can find this cleanly
|
||||
"publish-plan.json",
|
||||
);
|
||||
await execChangesetsCli(["publish-plan", "--output", publishPlanPath], {
|
||||
cwd,
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import * as path from "node:path";
|
||||
import { getExecOutput } from "@actions/exec";
|
||||
import type { Package } from "@manypkg/get-packages";
|
||||
import { createFixture } from "fs-fixture";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { GitHub } from "./github.ts";
|
||||
import {
|
||||
createStagedReleaseHandoff,
|
||||
finalizeStagedRelease,
|
||||
formatStageCommand,
|
||||
validateStagedReleaseHandoff,
|
||||
verifyPublished,
|
||||
type StagedReleaseEntry,
|
||||
} from "./staged-release.ts";
|
||||
|
||||
vi.mock("@actions/github", () => ({
|
||||
context: {
|
||||
repo: { owner: "changesets", repo: "action" },
|
||||
runId: 123,
|
||||
sha: "abc123",
|
||||
},
|
||||
}));
|
||||
vi.mock("@actions/exec", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@actions/exec")>()),
|
||||
getExecOutput: vi.fn(),
|
||||
}));
|
||||
|
||||
const entry: StagedReleaseEntry = {
|
||||
packageName: "pkg-a",
|
||||
version: "1.0.0",
|
||||
tag: "latest",
|
||||
gitTag: "pkg-a@1.0.0",
|
||||
stageId: "1de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f",
|
||||
};
|
||||
|
||||
async function createProject() {
|
||||
return createFixture({
|
||||
"package.json": JSON.stringify({
|
||||
name: "repo",
|
||||
private: true,
|
||||
workspaces: ["packages/*"],
|
||||
}),
|
||||
"package-lock.json": "",
|
||||
"packages/pkg-a/package.json": JSON.stringify({
|
||||
name: "pkg-a",
|
||||
version: "1.0.0",
|
||||
}),
|
||||
"packages/pkg-b/package.json": JSON.stringify({
|
||||
name: "pkg-b",
|
||||
version: "1.0.0",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
describe("staged release handoff", () => {
|
||||
it("prints stage ids in their topological event order", () => {
|
||||
expect(
|
||||
formatStageCommand(
|
||||
[
|
||||
{ type: "npm-stage", ...entry, stageId: "stage-b" },
|
||||
{ type: "npm-stage", ...entry, stageId: "stage-a" },
|
||||
],
|
||||
"approve",
|
||||
),
|
||||
).toBe("changeset stage approve stage-b stage-a");
|
||||
});
|
||||
|
||||
it("prints rejection recovery for a partial publish failure", () => {
|
||||
expect(
|
||||
formatStageCommand(
|
||||
[{ type: "npm-stage", ...entry, stageId: "stage-a" }],
|
||||
"reject",
|
||||
),
|
||||
).toBe("changeset stage reject stage-a");
|
||||
});
|
||||
|
||||
it("validates origin, manifests, tags, and preserves order", async () => {
|
||||
await using fixture = await createProject();
|
||||
const handoff = createStagedReleaseHandoff(
|
||||
[
|
||||
{ type: "npm-stage", ...entry },
|
||||
{
|
||||
type: "npm-stage",
|
||||
...entry,
|
||||
packageName: "pkg-b",
|
||||
gitTag: "pkg-b@1.0.0",
|
||||
stageId: "2de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f",
|
||||
},
|
||||
],
|
||||
{ repository: "changesets/action", runId: 123, sha: "abc123" },
|
||||
);
|
||||
const result = await validateStagedReleaseHandoff(handoff, fixture.path, {
|
||||
repository: "changesets/action",
|
||||
runId: 123,
|
||||
sha: "abc123",
|
||||
});
|
||||
expect(result.handoff.releases.map((release) => release.stageId)).toEqual([
|
||||
"1de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f",
|
||||
"2de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects a handoff from another workflow run", async () => {
|
||||
await using fixture = await createProject();
|
||||
const handoff = createStagedReleaseHandoff(
|
||||
[{ type: "npm-stage", ...entry }],
|
||||
{ repository: "changesets/action", runId: 122, sha: "abc123" },
|
||||
);
|
||||
|
||||
await expect(
|
||||
validateStagedReleaseHandoff(handoff, fixture.path, {
|
||||
repository: "changesets/action",
|
||||
runId: 123,
|
||||
sha: "abc123",
|
||||
}),
|
||||
).rejects.toThrow("do not match this workflow run");
|
||||
});
|
||||
|
||||
it("verifies an exact version through the configured package manager", async () => {
|
||||
await using fixture = await createProject();
|
||||
vi.mocked(getExecOutput).mockResolvedValue({
|
||||
exitCode: 0,
|
||||
stdout: JSON.stringify("1.0.0"),
|
||||
stderr: "",
|
||||
} as never);
|
||||
|
||||
await verifyPublished([entry], fixture.path, { timeoutMs: 0 });
|
||||
|
||||
expect(getExecOutput).toHaveBeenCalledWith(
|
||||
"npm",
|
||||
["view", "pkg-a@1.0.0", "version", "--json"],
|
||||
expect.objectContaining({ cwd: fixture.path }),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts existing correct tags and releases without writes", async () => {
|
||||
const getRef = vi.fn().mockResolvedValue({
|
||||
data: { object: { sha: "abc123" } },
|
||||
});
|
||||
const createRef = vi.fn();
|
||||
const getReleaseByTag = vi.fn().mockResolvedValue({ data: { id: 1 } });
|
||||
const createRelease = vi.fn();
|
||||
const github = {
|
||||
octokit: {
|
||||
rest: {
|
||||
git: { getRef, createRef },
|
||||
repos: { getReleaseByTag, createRelease },
|
||||
},
|
||||
},
|
||||
} as unknown as GitHub;
|
||||
const pkg = {
|
||||
dir: path.join("/repo", "packages/pkg-a"),
|
||||
relativeDir: "packages/pkg-a",
|
||||
packageJson: { name: "pkg-a", version: "1.0.0" },
|
||||
} satisfies Package;
|
||||
|
||||
await finalizeStagedRelease({
|
||||
github,
|
||||
handoff: {
|
||||
version: 1,
|
||||
repository: "changesets/action",
|
||||
runId: 123,
|
||||
sha: "abc123",
|
||||
releases: [entry],
|
||||
},
|
||||
packagesByName: new Map([["pkg-a", pkg]]),
|
||||
cwd: "/repo",
|
||||
verify: false,
|
||||
});
|
||||
|
||||
expect(createRef).not.toHaveBeenCalled();
|
||||
expect(createRelease).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects an existing tag at another commit", async () => {
|
||||
const github = {
|
||||
octokit: {
|
||||
rest: {
|
||||
git: {
|
||||
getRef: vi.fn().mockResolvedValue({
|
||||
data: { object: { sha: "wrong" } },
|
||||
}),
|
||||
createRef: vi.fn(),
|
||||
},
|
||||
repos: {
|
||||
getReleaseByTag: vi.fn(),
|
||||
createRelease: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as GitHub;
|
||||
|
||||
await expect(
|
||||
finalizeStagedRelease({
|
||||
github,
|
||||
handoff: {
|
||||
version: 1,
|
||||
repository: "changesets/action",
|
||||
runId: 123,
|
||||
sha: "abc123",
|
||||
releases: [entry],
|
||||
},
|
||||
packagesByName: new Map([
|
||||
[
|
||||
"pkg-a",
|
||||
{
|
||||
dir: "/repo/pkg-a",
|
||||
relativeDir: "pkg-a",
|
||||
packageJson: { name: "pkg-a", version: "1.0.0" },
|
||||
} satisfies Package,
|
||||
],
|
||||
]),
|
||||
cwd: "/repo",
|
||||
verify: false,
|
||||
}),
|
||||
).rejects.toThrow("expected abc123");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,342 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import artifact from "@actions/artifact";
|
||||
import * as core from "@actions/core";
|
||||
import { getExecOutput } from "@actions/exec";
|
||||
import { context } from "@actions/github";
|
||||
import { getPackages, type Package } from "@manypkg/get-packages";
|
||||
import type { GitHub } from "./github.ts";
|
||||
import { createRelease, type NpmStageEvent } from "./run.ts";
|
||||
import { downloadArtifact } from "./utils.ts";
|
||||
|
||||
export const STAGED_RELEASE_VERSION = 1;
|
||||
export const STAGED_RELEASE_FILENAME = "staged-release.json";
|
||||
const STAGE_ID_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
export type StagedReleaseEntry = Omit<NpmStageEvent, "type">;
|
||||
|
||||
export type StagedReleaseHandoff = {
|
||||
version: typeof STAGED_RELEASE_VERSION;
|
||||
repository: string;
|
||||
runId: number;
|
||||
sha: string;
|
||||
releases: StagedReleaseEntry[];
|
||||
};
|
||||
|
||||
type HandoffIdentity = {
|
||||
repository: string;
|
||||
runId: number;
|
||||
sha: string;
|
||||
};
|
||||
|
||||
function currentIdentity(): HandoffIdentity {
|
||||
return {
|
||||
repository: `${context.repo.owner}/${context.repo.repo}`,
|
||||
runId: context.runId,
|
||||
sha: context.sha,
|
||||
};
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isHttpNotFound(error: unknown) {
|
||||
return (
|
||||
isObject(error) &&
|
||||
(error.status === 404 || error.code === 404 || error.code === "404")
|
||||
);
|
||||
}
|
||||
|
||||
function isStagedReleaseEntry(value: unknown): value is StagedReleaseEntry {
|
||||
return (
|
||||
isObject(value) &&
|
||||
typeof value.packageName === "string" &&
|
||||
value.packageName.length > 0 &&
|
||||
typeof value.version === "string" &&
|
||||
value.version.length > 0 &&
|
||||
typeof value.tag === "string" &&
|
||||
value.tag.length > 0 &&
|
||||
typeof value.gitTag === "string" &&
|
||||
value.gitTag.length > 0 &&
|
||||
typeof value.stageId === "string" &&
|
||||
STAGE_ID_PATTERN.test(value.stageId)
|
||||
);
|
||||
}
|
||||
|
||||
export function createStagedReleaseHandoff(
|
||||
events: readonly NpmStageEvent[],
|
||||
identity: HandoffIdentity = currentIdentity(),
|
||||
): StagedReleaseHandoff {
|
||||
return {
|
||||
version: STAGED_RELEASE_VERSION,
|
||||
...identity,
|
||||
releases: events.map(({ type: _, ...event }) => event),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatStageCommand(
|
||||
events: readonly NpmStageEvent[],
|
||||
operation: "approve" | "reject",
|
||||
) {
|
||||
return `changeset stage ${operation} ${events
|
||||
.map((event) => event.stageId)
|
||||
.join(" ")}`;
|
||||
}
|
||||
|
||||
export async function uploadStagedRelease(
|
||||
events: readonly NpmStageEvent[],
|
||||
operation: "approve" | "reject",
|
||||
): Promise<number> {
|
||||
const verb = operation === "approve" ? "Approve" : "Reject";
|
||||
core.info(`${verb} the staged packages in this order:
|
||||
${formatStageCommand(events, operation)}`);
|
||||
core.info(
|
||||
"This command uses the default registry. For custom or multiple registries, split the IDs and pass --registry to each command.",
|
||||
);
|
||||
|
||||
const tmpDir = process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir()));
|
||||
const outDir = path.join(tmpDir, `changeset-staged-release-${Date.now()}`);
|
||||
const handoffPath = path.join(outDir, STAGED_RELEASE_FILENAME);
|
||||
await fs.mkdir(outDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
handoffPath,
|
||||
`${JSON.stringify(createStagedReleaseHandoff(events), null, 2)}\n`,
|
||||
);
|
||||
|
||||
const result = await artifact.uploadArtifact(
|
||||
`changeset-staged-release-${context.runId}-${Date.now()}`,
|
||||
[handoffPath],
|
||||
outDir,
|
||||
{ skipArchive: true, retentionDays: 30 },
|
||||
);
|
||||
if (result.id === undefined) {
|
||||
throw new Error("Staged release artifact upload did not return an id");
|
||||
}
|
||||
|
||||
return result.id;
|
||||
}
|
||||
|
||||
function expectedGitTag(
|
||||
tool: Awaited<ReturnType<typeof getPackages>>["tool"],
|
||||
entry: StagedReleaseEntry,
|
||||
) {
|
||||
return tool.type === "root"
|
||||
? `v${entry.version}`
|
||||
: `${entry.packageName}@${entry.version}`;
|
||||
}
|
||||
|
||||
export async function validateStagedReleaseHandoff(
|
||||
value: unknown,
|
||||
cwd: string,
|
||||
identity: HandoffIdentity = currentIdentity(),
|
||||
): Promise<{
|
||||
handoff: StagedReleaseHandoff;
|
||||
packagesByName: Map<string, Package>;
|
||||
}> {
|
||||
if (
|
||||
!isObject(value) ||
|
||||
value.version !== STAGED_RELEASE_VERSION ||
|
||||
value.repository !== identity.repository ||
|
||||
value.runId !== identity.runId ||
|
||||
value.sha !== identity.sha ||
|
||||
!Array.isArray(value.releases) ||
|
||||
value.releases.length === 0 ||
|
||||
!value.releases.every(isStagedReleaseEntry)
|
||||
) {
|
||||
throw new Error(
|
||||
"Invalid staged release artifact: version, origin, or release entries do not match this workflow run",
|
||||
);
|
||||
}
|
||||
|
||||
const handoff = value as StagedReleaseHandoff;
|
||||
const uniqueFields: Array<keyof StagedReleaseEntry> = [
|
||||
"packageName",
|
||||
"gitTag",
|
||||
"stageId",
|
||||
];
|
||||
for (const field of uniqueFields) {
|
||||
const values = handoff.releases.map((entry) => entry[field]);
|
||||
if (new Set(values).size !== values.length) {
|
||||
throw new Error(
|
||||
`Invalid staged release artifact: duplicate ${field} value`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { packages, rootPackage, tool } = await getPackages(cwd);
|
||||
const packagesByName = new Map(
|
||||
[...packages, ...(rootPackage ? [rootPackage] : [])].map((pkg) => [
|
||||
pkg.packageJson.name,
|
||||
pkg,
|
||||
]),
|
||||
);
|
||||
for (const entry of handoff.releases) {
|
||||
const pkg = packagesByName.get(entry.packageName);
|
||||
if (!pkg || pkg.packageJson.version !== entry.version) {
|
||||
throw new Error(
|
||||
`Invalid staged release artifact: ${entry.packageName}@${entry.version} does not match the checked-out manifest`,
|
||||
);
|
||||
}
|
||||
if (entry.gitTag !== expectedGitTag(tool, entry)) {
|
||||
throw new Error(
|
||||
`Invalid staged release artifact: unexpected Git tag ${entry.gitTag}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { handoff, packagesByName };
|
||||
}
|
||||
|
||||
export async function downloadAndValidateStagedRelease(
|
||||
artifactId: number,
|
||||
cwd: string,
|
||||
) {
|
||||
const tmpDir = process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir()));
|
||||
const artifactDir = await downloadArtifact(
|
||||
tmpDir,
|
||||
artifactId,
|
||||
"changeset-staged-release",
|
||||
);
|
||||
const handoffPath = path.join(artifactDir, STAGED_RELEASE_FILENAME);
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(await fs.readFile(handoffPath, "utf8"));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read staged release artifact ${artifactId}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
return validateStagedReleaseHandoff(value, cwd);
|
||||
}
|
||||
|
||||
function outputContainsVersion(output: string, version: string) {
|
||||
const values: unknown[] = [];
|
||||
for (const line of output.split("\n")) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
values.push(JSON.parse(line));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const contains = (value: unknown): boolean => {
|
||||
if (value === version) return true;
|
||||
if (Array.isArray(value)) return value.some(contains);
|
||||
if (isObject(value)) return Object.values(value).some(contains);
|
||||
return false;
|
||||
};
|
||||
return values.some(contains);
|
||||
}
|
||||
|
||||
async function isPublished(
|
||||
cwd: string,
|
||||
tool: Awaited<ReturnType<typeof getPackages>>["tool"],
|
||||
entry: StagedReleaseEntry,
|
||||
) {
|
||||
const spec = `${entry.packageName}@${entry.version}`;
|
||||
const command =
|
||||
tool.type === "pnpm" ? "pnpm" : tool.type === "yarn" ? "yarn" : "npm";
|
||||
const args =
|
||||
tool.type === "yarn"
|
||||
? ["npm", "info", spec, "--fields", "version", "--json"]
|
||||
: ["view", spec, "version", "--json"];
|
||||
const result = await getExecOutput(command, args, {
|
||||
cwd,
|
||||
ignoreReturnCode: true,
|
||||
silent: true,
|
||||
env: process.env as Record<string, string>,
|
||||
});
|
||||
return (
|
||||
result.exitCode === 0 && outputContainsVersion(result.stdout, entry.version)
|
||||
);
|
||||
}
|
||||
|
||||
export async function verifyPublished(
|
||||
releases: readonly StagedReleaseEntry[],
|
||||
cwd: string,
|
||||
options: { timeoutMs?: number; intervalMs?: number } = {},
|
||||
) {
|
||||
const timeoutMs = options.timeoutMs ?? 120_000;
|
||||
const intervalMs = options.intervalMs ?? 5_000;
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const { tool } = await getPackages(cwd);
|
||||
|
||||
while (true) {
|
||||
const results = await Promise.all(
|
||||
releases.map((release) => isPublished(cwd, tool, release)),
|
||||
);
|
||||
if (results.every(Boolean)) return;
|
||||
if (Date.now() >= deadline) {
|
||||
const missing = releases
|
||||
.filter((_, index) => !results[index])
|
||||
.map((release) => `${release.packageName}@${release.version}`);
|
||||
throw new Error(
|
||||
`Timed out waiting for approved packages to become visible: ${missing.join(", ")}`,
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, Math.min(intervalMs, deadline - Date.now())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTag(github: GitHub, tag: string, sha: string) {
|
||||
try {
|
||||
const existing = await github.octokit.rest.git.getRef({
|
||||
...context.repo,
|
||||
ref: `tags/${tag}`,
|
||||
});
|
||||
if (existing.data.object.sha !== sha) {
|
||||
throw new Error(
|
||||
`Git tag ${tag} already exists at ${existing.data.object.sha}, expected ${sha}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
if (!isHttpNotFound(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await github.octokit.rest.git.createRef({
|
||||
...context.repo,
|
||||
ref: `refs/tags/${tag}`,
|
||||
sha,
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureRelease(github: GitHub, pkg: Package, tagName: string) {
|
||||
try {
|
||||
await github.octokit.rest.repos.getReleaseByTag({
|
||||
...context.repo,
|
||||
tag: tagName,
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
if (!isHttpNotFound(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await createRelease(github.octokit, { pkg, tagName });
|
||||
}
|
||||
|
||||
export async function finalizeStagedRelease(options: {
|
||||
github: GitHub;
|
||||
handoff: StagedReleaseHandoff;
|
||||
packagesByName: Map<string, Package>;
|
||||
cwd: string;
|
||||
verify: boolean;
|
||||
}) {
|
||||
if (options.verify) {
|
||||
await verifyPublished(options.handoff.releases, options.cwd);
|
||||
}
|
||||
|
||||
for (const entry of options.handoff.releases) {
|
||||
const pkg = options.packagesByName.get(entry.packageName)!;
|
||||
await ensureTag(options.github, entry.gitTag, options.handoff.sha);
|
||||
await ensureRelease(options.github, pkg, entry.gitTag);
|
||||
}
|
||||
}
|
||||
+53
-1
@@ -1,5 +1,11 @@
|
||||
import { createFixture } from "fs-fixture";
|
||||
import { expect, test } from "vitest";
|
||||
import { BumpLevels, getChangelogEntry, sortTheThings } from "./utils.ts";
|
||||
import {
|
||||
BumpLevels,
|
||||
getChangelogEntry,
|
||||
sortTheThings,
|
||||
validateChangesetsCliVersion,
|
||||
} from "./utils.ts";
|
||||
|
||||
let changelog = `# @keystone-alpha/email
|
||||
|
||||
@@ -100,3 +106,49 @@ test("it sorts the things right", () => {
|
||||
];
|
||||
expect(things.sort(sortTheThings)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("throws when the project declares Changesets CLI v2", async () => {
|
||||
await using fixture = await createFixture({
|
||||
"package.json": JSON.stringify({
|
||||
name: "project",
|
||||
devDependencies: { "@changesets/cli": "^2.29.7" },
|
||||
}),
|
||||
"package-lock.json": "",
|
||||
});
|
||||
|
||||
await expect(validateChangesetsCliVersion(fixture.path)).rejects.toThrow(
|
||||
"This version of the Changesets action is designed to work with Changesets CLI v3. Changesets CLI v2 is not supported; use Changesets action v1 instead, which is compatible with CLI v2.",
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts a Changesets CLI v3 contract without an installed CLI", async () => {
|
||||
await using fixture = await createFixture({
|
||||
"package.json": JSON.stringify({
|
||||
name: "project",
|
||||
devDependencies: { "@changesets/cli": "^3.0.0" },
|
||||
}),
|
||||
"package-lock.json": "",
|
||||
});
|
||||
|
||||
await expect(
|
||||
validateChangesetsCliVersion(fixture.path),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("throws when the project has Changesets CLI v2 installed", async () => {
|
||||
await using fixture = await createFixture({
|
||||
"package.json": JSON.stringify({
|
||||
name: "project",
|
||||
devDependencies: { "@changesets/cli": "^3.0.0" },
|
||||
}),
|
||||
"package-lock.json": "",
|
||||
"node_modules/@changesets/cli/package.json": JSON.stringify({
|
||||
name: "@changesets/cli",
|
||||
version: "2.29.7",
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(validateChangesetsCliVersion(fixture.path)).rejects.toThrow(
|
||||
"This version of the Changesets action is designed to work with Changesets CLI v3. Changesets CLI v2 is not supported; use Changesets action v1 instead, which is compatible with CLI v2.",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import artifact from "@actions/artifact";
|
||||
import * as core from "@actions/core";
|
||||
import {
|
||||
exec,
|
||||
getExecOutput,
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
type ExecOutput,
|
||||
} from "@actions/exec";
|
||||
import { getPackages, type Package } from "@manypkg/get-packages";
|
||||
import major from "semver/functions/major.js";
|
||||
import subset from "semver/ranges/subset.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
@@ -123,6 +126,95 @@ export function fileExists(filePath: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getOptionalInput(name: string) {
|
||||
// normalize empty string default return value of `core.getInput` to undefined
|
||||
return core.getInput(name) || undefined;
|
||||
}
|
||||
|
||||
export function getRequiredInput(name: string) {
|
||||
// it's just a small utility wrapper, mainly introduced for usage parity with our custom `getOptionalInput`
|
||||
// note: `core.getBooleanInput` gets used directly as it already normalizes the return value nicely
|
||||
return core.getInput(name, { required: true });
|
||||
}
|
||||
|
||||
export function getOptionalBooleanInput(name: string) {
|
||||
const value = getOptionalInput(name);
|
||||
if (value === undefined) return undefined;
|
||||
if (value.toLowerCase() === "true") return true;
|
||||
if (value.toLowerCase() === "false") return false;
|
||||
throw new Error(
|
||||
`Invalid boolean input ${JSON.stringify(name)}: ${JSON.stringify(value)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function throwOnRenamedInputs(renames: Record<string, string>) {
|
||||
const references: Record<string, string> = {};
|
||||
|
||||
for (const [oldInput, newInput] of Object.entries(renames)) {
|
||||
if (core.getInput(oldInput)) {
|
||||
references[oldInput] = newInput;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(references).length > 0) {
|
||||
const list = Object.entries(references)
|
||||
.map(([oldInput, newInput]) => `- "${oldInput}" -> "${newInput}"`)
|
||||
.join("\n");
|
||||
throw new Error(
|
||||
`The following inputs have been renamed:\n${list}\nPlease update your workflow file.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const changesetsCliCompatibilityError =
|
||||
"This version of the Changesets action is designed to work with Changesets CLI v3. " +
|
||||
"Changesets CLI v2 is not supported; use Changesets action v1 instead, which is compatible with CLI v2.";
|
||||
|
||||
export async function validateChangesetsCliVersion(cwd: string) {
|
||||
const { rootPackage } = await getPackages(cwd);
|
||||
const packageJson = rootPackage?.packageJson;
|
||||
const declaredVersion =
|
||||
packageJson?.devDependencies?.["@changesets/cli"] ??
|
||||
packageJson?.dependencies?.["@changesets/cli"];
|
||||
|
||||
if (typeof declaredVersion === "string") {
|
||||
const range = declaredVersion.startsWith("workspace:")
|
||||
? declaredVersion.slice("workspace:".length)
|
||||
: declaredVersion;
|
||||
|
||||
let isV2 = false;
|
||||
|
||||
try {
|
||||
isV2 = subset(range, ">=2.0.0-0 <3.0.0-0", {
|
||||
includePrerelease: true,
|
||||
});
|
||||
} catch {
|
||||
// it could be a non-semver protocol
|
||||
}
|
||||
|
||||
if (isV2) {
|
||||
throw new Error(changesetsCliCompatibilityError);
|
||||
}
|
||||
}
|
||||
|
||||
let cliPackageJson;
|
||||
|
||||
try {
|
||||
cliPackageJson = require(
|
||||
require.resolve("@changesets/cli/package.json", { paths: [cwd] }),
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof cliPackageJson.version === "string" &&
|
||||
major(cliPackageJson.version) === 2
|
||||
) {
|
||||
throw new Error(changesetsCliCompatibilityError);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveChangesetsCli(cwd: string) {
|
||||
return require.resolve("@changesets/cli/bin.js", {
|
||||
paths: [cwd],
|
||||
|
||||
+25
-19
@@ -1,7 +1,11 @@
|
||||
import * as core from "@actions/core";
|
||||
import { Git } from "../git.ts";
|
||||
import { setupOctokit } from "../octokit.ts";
|
||||
import { GitHub } from "../github.ts";
|
||||
import { runVersion } from "../run.ts";
|
||||
import {
|
||||
getOptionalInput,
|
||||
getRequiredInput,
|
||||
validateChangesetsCliVersion,
|
||||
} from "../utils.ts";
|
||||
|
||||
try {
|
||||
await main();
|
||||
@@ -10,39 +14,41 @@ try {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const githubToken = core.getInput("github-token", { required: true });
|
||||
const script = core.getInput("script");
|
||||
const commitMessage = core.getInput("commit-message", { required: true });
|
||||
const prTitle = core.getInput("pr-title", { required: true });
|
||||
const prDraft = core.getInput("pr-draft") || undefined;
|
||||
const prBaseBranch = core.getInput("pr-base-branch");
|
||||
const commitMode = core.getInput("commit-mode") || "git-cli";
|
||||
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||
const cwd = process.cwd();
|
||||
await validateChangesetsCliVersion(cwd);
|
||||
|
||||
const githubToken = getRequiredInput("github-token");
|
||||
const script = getOptionalInput("script");
|
||||
const commitMessage = getRequiredInput("commit-message");
|
||||
const prTitle = getRequiredInput("pr-title");
|
||||
const prDraft = getOptionalInput("pr-draft");
|
||||
const prBaseBranch = getOptionalInput("pr-base-branch");
|
||||
const commitMode = getOptionalInput("commit-mode") ?? "git-cli";
|
||||
const setupGitUser = core.getBooleanInput("setup-git-user");
|
||||
|
||||
// Validations
|
||||
if (prDraft !== undefined && prDraft !== "always" && prDraft !== "create") {
|
||||
throw new Error(`Invalid pr-draft input: ${prDraft}`);
|
||||
}
|
||||
if (commitMode !== "git-cli" && commitMode !== "github-api") {
|
||||
throw new Error(`Invalid commit-mode input: ${commitMode}`);
|
||||
}
|
||||
|
||||
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||
const cwd = process.cwd();
|
||||
|
||||
const octokit = setupOctokit(githubToken);
|
||||
const git = new Git({
|
||||
octokit: commitMode === "github-api" ? octokit : undefined,
|
||||
const github = new GitHub({
|
||||
cwd,
|
||||
githubToken,
|
||||
commitMode,
|
||||
});
|
||||
|
||||
if (setupGitUser) {
|
||||
core.info("setting git user");
|
||||
await git.setupUser();
|
||||
await github.setupUser();
|
||||
}
|
||||
|
||||
const { pullRequestNumber } = await runVersion({
|
||||
script,
|
||||
githubToken,
|
||||
git,
|
||||
octokit,
|
||||
github,
|
||||
cwd,
|
||||
prTitle,
|
||||
commitMessage,
|
||||
|
||||
+4
-2
@@ -9,7 +9,7 @@ inputs:
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
script:
|
||||
description: "The command to use to publish packages"
|
||||
description: "The command to use to version packages"
|
||||
required: false
|
||||
commit-message:
|
||||
description: "The commit message. Default to `Version Packages`"
|
||||
@@ -37,7 +37,9 @@ inputs:
|
||||
description: Sets up the git user for commits as `"github-actions[bot]"`. Default to `true`
|
||||
required: false
|
||||
default: true
|
||||
outputs: {}
|
||||
outputs:
|
||||
pr-number:
|
||||
description: The pull request number that was created or updated
|
||||
branding:
|
||||
icon: package
|
||||
color: blue
|
||||
|
||||
Reference in New Issue
Block a user