mirror of
https://github.com/changesets/action
synced 2026-10-26 16:39:56 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f7aee55a0 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@changesets/action": patch
|
||||
---
|
||||
|
||||
Validate that projects use Changesets CLI v3 and direct Changesets CLI v2 users to `changesets/action@v1`.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@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.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"@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,14 +1,13 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@4.0.0-next.6/schema.json",
|
||||
"changelog": "@changesets/changelog-github",
|
||||
"$schema": "https://unpkg.com/@changesets/config@1.6.2/schema.json",
|
||||
"changelog": [
|
||||
"@changesets/changelog-github",
|
||||
{ "repo": "changesets/action" }
|
||||
],
|
||||
"commit": false,
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": [],
|
||||
"privatePackages": {
|
||||
"version": true,
|
||||
"tag": true
|
||||
}
|
||||
"ignore": []
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
"@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`
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@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.
|
||||
+4
-7
@@ -1,20 +1,17 @@
|
||||
{
|
||||
"mode": "pre",
|
||||
"tag": "next",
|
||||
"initialVersions": {
|
||||
"@changesets/action": "1.9.0"
|
||||
},
|
||||
"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"
|
||||
"silent-tips-dance"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@changesets/action": major
|
||||
---
|
||||
|
||||
Update to Changesets v3 packages
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@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.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"@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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@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/** @Andarist @bluwy @emmatown
|
||||
/.github/workflows/** @Andarist @bluwy @emmatown
|
||||
/action.yml @Andarist @bluwy @emmatown
|
||||
/scripts/bump.ts @Andarist @bluwy @emmatown
|
||||
/scripts/release.ts @Andarist @bluwy @emmatown
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
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,38 +15,13 @@ concurrency:
|
||||
permissions: {} # each job should define its own permission explicitly
|
||||
|
||||
jobs:
|
||||
select-mode:
|
||||
name: Select mode
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
mode: ${{ steps.select-mode.outputs.mode }}
|
||||
permissions:
|
||||
contents: read # to check out repo (actions/checkout)
|
||||
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
|
||||
|
||||
- name: Select mode
|
||||
id: select-mode
|
||||
uses: ./select-mode
|
||||
|
||||
version:
|
||||
name: Version
|
||||
if: needs.select-mode.outputs.mode == 'version'
|
||||
needs: select-mode
|
||||
runs-on: ubuntu-latest
|
||||
environment: version
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
hasChangesets: ${{ steps.changesets.outputs.hasChangesets }}
|
||||
permissions:
|
||||
contents: read # to check out repo (actions/checkout)
|
||||
steps:
|
||||
@@ -62,8 +37,8 @@ jobs:
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- uses: ./.github/actions/setup-bot-auth
|
||||
id: bot-auth
|
||||
- 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 }}
|
||||
@@ -72,22 +47,28 @@ jobs:
|
||||
|
||||
- name: Create or update release pull request
|
||||
id: changesets
|
||||
uses: ./version
|
||||
uses: ./
|
||||
with:
|
||||
github-token: ${{ steps.bot-auth.outputs.token }}
|
||||
setup-git-user: false
|
||||
script: pnpm bump
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
version: pnpm bump
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
if: needs.select-mode.outputs.mode == 'publish'
|
||||
needs: select-mode
|
||||
if: needs.version.outputs.hasChangesets == 'false'
|
||||
needs: version
|
||||
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:
|
||||
- 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:
|
||||
@@ -100,15 +81,8 @@ jobs:
|
||||
- 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
|
||||
uses: ./
|
||||
with:
|
||||
github-token: ${{ steps.bot-auth.outputs.token }}
|
||||
script: pnpm release
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
publish: pnpm release
|
||||
|
||||
@@ -122,6 +122,13 @@ 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:
|
||||
@@ -166,17 +173,15 @@ jobs:
|
||||
- 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 push the release snapshot branch (release-pr.ts)
|
||||
- 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"
|
||||
|
||||
- name: Release snapshot version
|
||||
run: pnpm release:pr
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.bot-auth.outputs.token }}
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Add success reaction
|
||||
run: gh api "/repos/$GH_REPO/issues/comments/$COMMENT_ID/reactions" -f content='rocket'
|
||||
|
||||
@@ -1,35 +1,5 @@
|
||||
# @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
|
||||
|
||||
@@ -14,23 +14,20 @@ There are also sub-actions hosted in this repository. Check out their respective
|
||||
|
||||
### Inputs
|
||||
|
||||
- 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`
|
||||
- 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`.
|
||||
- cwd - Changes node's `process.cwd()` if the project is not located on the root. Default to `process.cwd()`
|
||||
- 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
|
||||
- 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.
|
||||
|
||||
### Outputs
|
||||
|
||||
- published - A boolean value to indicate whether a publishing has happened or not
|
||||
- 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
|
||||
- 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"}]`
|
||||
|
||||
### Example workflow
|
||||
|
||||
@@ -109,7 +106,7 @@ jobs:
|
||||
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-script: pnpm release
|
||||
publish: pnpm release
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -220,7 +217,7 @@ jobs:
|
||||
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-script: pnpm version
|
||||
version: pnpm version
|
||||
```
|
||||
|
||||
#### With Yarn 2 / Plug'n'Play
|
||||
@@ -230,6 +227,6 @@ If you are using [Yarn Plug'n'Play](https://yarnpkg.com/features/pnp), you shoul
|
||||
```yaml
|
||||
- uses: changesets/action@v2
|
||||
with:
|
||||
version-script: yarn changeset version
|
||||
version: yarn changeset version
|
||||
# ...
|
||||
```
|
||||
|
||||
+20
-29
@@ -8,37 +8,28 @@ inputs:
|
||||
description: "The GitHub token to use for authentication. Defaults to the GitHub-provided token."
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
publish-script:
|
||||
publish:
|
||||
description: "The command to use to build and publish packages"
|
||||
required: false
|
||||
version-script:
|
||||
version:
|
||||
description: "The command to update version, edit CHANGELOG, read and delete changesets. Default to `changeset version` if not provided"
|
||||
required: false
|
||||
commit-message:
|
||||
description: "The commit message. Default to `Version Packages`"
|
||||
commit:
|
||||
description: |
|
||||
The commit message. Default to `Version Packages`
|
||||
required: false
|
||||
default: "Version Packages"
|
||||
pr-title:
|
||||
description: "The pull request title. Default to `Version Packages`"
|
||||
title:
|
||||
description: The pull request title. Default to `Version Packages`
|
||||
required: false
|
||||
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"
|
||||
setupGitUser:
|
||||
description: Sets up the git user for commits as `"github-actions[bot]"`. Default to `true`
|
||||
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`.
|
||||
createGithubReleases:
|
||||
description: "A boolean value to indicate whether to create Github releases after `publish` or not"
|
||||
required: false
|
||||
default: true
|
||||
commit-mode:
|
||||
commitMode:
|
||||
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",
|
||||
@@ -46,21 +37,21 @@ inputs:
|
||||
or app who owns the GITHUB_TOKEN.
|
||||
required: false
|
||||
default: "git-cli"
|
||||
setup-git-user:
|
||||
description: Sets up the git user for commits as `"github-actions[bot]"`. Default to `true`
|
||||
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.'
|
||||
required: false
|
||||
default: true
|
||||
outputs:
|
||||
published:
|
||||
description: A boolean value to indicate whether a publishing is happened or not
|
||||
published-packages:
|
||||
publishedPackages:
|
||||
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 a staged release handoff emitted by the publish script
|
||||
has-changesets:
|
||||
hasChangesets:
|
||||
description: A boolean about whether there were changesets. Useful if you want to create your own publishing functionality.
|
||||
pr-number:
|
||||
pullRequestNumber:
|
||||
description: The pull request number that was created or updated
|
||||
branding:
|
||||
icon: "package"
|
||||
|
||||
Vendored
+49
File diff suppressed because one or more lines are too long
Vendored
+65
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import{_ as e,g as t,h as n,m as r,p as i,v as a}from"./get-packages.cjs-Dvdg-NAr.js";import{n as o,r as s,t as c}from"./run-Cekyj2rz.js";import{r as l,s as u}from"./utils-DQTbg3yF.js";import{t as d}from"./readChangesetState-D8jsqLyj.js";import f from"node:fs/promises";(async()=>{let p=process.env.GITHUB_TOKEN||n(`github-token`);if(!p){e(`Please add the GITHUB_TOKEN to the changesets action`);return}let m=process.cwd(),h=u(`commitMode`)??`git-cli`,g=u(`prDraft`);if(h!==`git-cli`&&h!==`github-api`){e(`Invalid commit mode: ${h}`);return}if(g!==void 0&&g!==`always`&&g!==`create`){e(`Invalid prDraft: ${g}`);return}let _=new s({cwd:m,githubToken:p,commitMode:h});r(`setupGitUser`)&&(t(`setting git user`),await _.setupUser());let{changesets:v}=await d(m),y=n(`publish`),b=v.length!==0,x=v.some(e=>e.releases.length>0),S=!!y;switch(a(`published`,`false`),a(`publishedPackages`,`[]`),a(`hasChangesets`,String(b)),!0){case!b&&!S:t(`No changesets present or were removed by merging release PR. Not publishing because no publish script found.`);return;case!b&&S:{if(t(`No changesets found. Attempting to publish any unpublished packages to npm`),process.env.NPM_TOKEN){let e=`${process.env.HOME}/.npmrc`;await l(e)?(t(`Found existing user .npmrc file`),(await f.readFile(e,`utf8`)).split(`
|
||||
`).find(e=>/^\s*\/\/registry\.npmjs\.org\/:[_-]authToken=/i.test(e))?t(`Found existing auth token for the npm registry in the user .npmrc file`):(t(`Didn't find existing auth token for the npm registry in the user .npmrc file, creating one`),await f.appendFile(e,`\n//registry.npmjs.org/:_authToken=${process.env.NPM_TOKEN}\n`))):(t(`No user .npmrc file found, creating one with NPM_TOKEN used as auth token`),await f.writeFile(e,`//registry.npmjs.org/:_authToken=${process.env.NPM_TOKEN}\n`))}else process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN&&process.env.ACTIONS_ID_TOKEN_REQUEST_URL?t(`No NPM_TOKEN found, but OIDC is available - using npm trusted publishing`):t(`No NPM_TOKEN or OIDC available - assuming npm is already authenticated`);let e=await c({script:y,github:_,createGithubReleases:r(`createGithubReleases`),cwd:m});e.published&&(a(`published`,`true`),a(`publishedPackages`,JSON.stringify(e.publishedPackages))),e.exitCode!==0&&(i(`Publish command exited with code ${e.exitCode}${e.published?`, but some packages were published: ${e.publishedPackages.map(e=>`${e.name}@${e.version}`).join(`, `)}`:``}`),process.exit(e.exitCode));return}case b&&!x:t(`All changesets are empty; not creating PR`);return;case b:{let{pullRequestNumber:e}=await o({script:u(`version`),github:_,cwd:m,prTitle:u(`title`),commitMessage:u(`commit`),hasPublishScript:S,prDraft:g,branch:u(`branch`)});a(`pullRequestNumber`,String(e));return}}})().catch(t=>{i(t),e(t.message)});export{};
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
import{_ as e,v as t}from"./get-packages.cjs-Dvdg-NAr.js";import{f as n,n as r,s as i,t as a}from"./utils-DQTbg3yF.js";import o from"node:fs/promises";import s from"node:path";import c from"node:os";try{await l()}catch(t){e(t.message)}async function l(){let e=i(`publish-plan-artifact-id`),r=process.cwd(),a=process.env.RUNNER_TEMP??await o.realpath(c.tmpdir()),l=s.join(a,`changeset-pack-${Date.now()}`);await u(r,{outDir:l,publishPlanPath:e?await d(a,Number(e)):void 0});let p=await n.uploadArtifact(`changeset-pack-${Date.now()}`,await f(l),l,{retentionDays:30});if(p.id===void 0)throw Error(`Packed artifact upload did not return an artifact id`);t(`pack-dir-artifact-id`,String(p.id))}async function u(e,t){let n=[`pack`,`--out-dir`,t.outDir];t.publishPlanPath&&n.push(`--from-publish-plan`,t.publishPlanPath),await r(n,{cwd:e,env:process.env})}async function d(e,t){let n=await a(e,t,`changeset-publish-plan`);return s.join(n,`publish-plan.json`)}async function f(e){let t=await o.readdir(e,{withFileTypes:!0});return(await Promise.all(t.map(async t=>{let n=s.join(e,t.name);return t.isDirectory()?f(n):[n]}))).flat()}export{};
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
import{_ as e,c as t,g as n,l as r,v as i}from"./get-packages.cjs-Dvdg-NAr.js";import{c as a,s as o}from"./utils-DQTbg3yF.js";try{await s()}catch(t){e(t.message)}async function s(){let e=t.payload.pull_request;if(!e)throw Error("This action should only be run on `pull_request_target` or `pull_request` events");let s=a(`github-token`),l=a(`body`),u=o(`update-id`),d=u?c(u):null,f=d?`${d}\n\n${l}`:l,p={repo:e.base.repo.name,owner:e.base.repo.owner.login,issue_number:e.number,body:f},m=r(s),h;d&&(n(`Checking for existing comment...`),h=await m.rest.issues.listComments({repo:e.base.repo.name,owner:e.base.repo.owner.login,issue_number:e.number}).then(e=>e.data.find(e=>e.body?.includes(d))?.id)),h?(n(`Updating existing comment (id: ${h})...`),await m.rest.issues.updateComment({...p,comment_id:h}),i(`comment-id`,h)):(n(`Creating new comment...`),i(`comment-id`,(await m.rest.issues.createComment(p)).data.id)),n(`Done!`)}function c(e){let t=`changesets-action-pr-comment`;return t===e?`<!-- ${t} -->`:`<!-- ${t}:${e} -->`}export{};
|
||||
Vendored
+39
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
||||
import{_ as e,m as t,v as n}from"./get-packages.cjs-Dvdg-NAr.js";import{r,t as i}from"./run-Cekyj2rz.js";import{c as a,s as o,t as s}from"./utils-DQTbg3yF.js";import c from"node:fs/promises";import l from"node:os";try{await u()}catch(t){e(t.message)}async function u(){let e=a(`github-token`),u=o(`script`),d=o(`pack-dir-artifact-id`),f=t(`create-github-releases`),p=process.cwd(),m=await i({script:u,github:new r({cwd:p,githubToken:e,commitMode:`github-api`}),createGithubReleases:f,cwd:p,fromPackDir:d?await s(process.env.RUNNER_TEMP??await c.realpath(l.tmpdir()),Number(d),`changeset-pack`):void 0});if(m.published?(n(`published`,`true`),n(`published-packages`,JSON.stringify(m.publishedPackages))):n(`published`,`false`),m.exitCode!==0)throw Error(`Publish command exited with code ${m.exitCode}${m.published?`, but some packages were published: ${m.publishedPackages.map(e=>`${e.name}@${e.version}`).join(`, `)}`:``}`)}export{};
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
import{o as e,t}from"./changesets-read.esm-D6fW_L6x.js";async function n(n=process.cwd()){let r=await e(n),i=await t(n);if(r!==void 0&&r.mode===`pre`){let e=new Set(r.changesets);return{preState:r,changesets:i.filter(t=>!e.has(t.id))}}return{preState:void 0,changesets:i}}export{n as t};
|
||||
Vendored
+149
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
||||
import{_ as e,v as t}from"./get-packages.cjs-Dvdg-NAr.js";import{f as n,n as r}from"./utils-DQTbg3yF.js";import{t as i}from"./readChangesetState-D8jsqLyj.js";import a from"node:fs/promises";import o from"node:path";import s from"node:os";try{await c()}catch(t){e(t.message)}async function c(){let e=await l();if(t(`mode`,e.mode),e.mode===`publish`){let r=await n.uploadArtifact(o.basename(e.publishPlanPath,`.json`),[e.publishPlanPath],o.dirname(e.publishPlanPath),{skipArchive:!0,retentionDays:30});if(r.id===void 0)throw Error(`Publish plan artifact upload did not return an artifact id`);t(`publish-plan-artifact-id`,String(r.id))}}async function l(){let{changesets:e}=await i();if(e.length>0)return e.some(e=>e.releases.length>0)?{mode:`version`}:{mode:`none`};let t=process.cwd(),n=o.join(process.env.RUNNER_TEMP??await a.realpath(s.tmpdir()),`changeset-publish-plan-${Date.now()}`,`publish-plan.json`);return await r([`publish-plan`,`--output`,n],{cwd:t,env:process.env}),(await u(n)).length===0?{mode:`none`}:{mode:`publish`,publishPlanPath:n}}async function u(e){let t;try{t=await a.readFile(e,`utf8`)}catch(t){throw Error(`Failed to read publish plan at ${e}`,{cause:t})}let n;try{n=JSON.parse(t)}catch(t){throw Error(`Failed to parse publish plan at ${e}`,{cause:t})}if(typeof n!=`object`||!n||!(`version`in n)||typeof n.version!=`number`||!(`plan`in n)||!Array.isArray(n.plan))throw Error(`Invalid publish plan at ${e}: expected { version: number; plan: unknown[] }`);return n.plan}export{};
|
||||
Vendored
+80
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
||||
import{_ as e,g as t,m as n,v as r}from"./get-packages.cjs-Dvdg-NAr.js";import{n as i,r as a}from"./run-Cekyj2rz.js";import{c as o,s}from"./utils-DQTbg3yF.js";try{await c()}catch(t){e(t.message)}async function c(){let e=o(`github-token`),c=s(`script`),l=o(`commit-message`),u=o(`pr-title`),d=s(`pr-draft`),f=s(`pr-base-branch`),p=s(`commit-mode`)??`git-cli`,m=n(`setup-git-user`);if(d!==void 0&&d!==`always`&&d!==`create`)throw Error(`Invalid pr-draft input: ${d}`);if(p!==`git-cli`&&p!==`github-api`)throw Error(`Invalid commit-mode input: ${p}`);let h=process.cwd(),g=new a({cwd:h,githubToken:e,commitMode:p});m&&(t(`setting git user`),await g.setupUser());let{pullRequestNumber:_}=await i({script:c,github:g,cwd:h,prTitle:u,commitMessage:l,hasPublishScript:!0,prDraft:d,branch:f});r(`pr-number`,String(_))}export{};
|
||||
@@ -1,13 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,23 +0,0 @@
|
||||
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
|
||||
+11
-12
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "@changesets/action",
|
||||
"version": "2.0.0-next.3",
|
||||
"private": true,
|
||||
"version": "2.0.0-next.2",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
@@ -22,12 +21,12 @@
|
||||
"@actions/core": "^3.0.1",
|
||||
"@actions/exec": "^3.0.0",
|
||||
"@actions/github": "^9.1.1",
|
||||
"@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",
|
||||
"@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",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-throttling": "^11.0.3",
|
||||
"human-id": "^4.1.3",
|
||||
@@ -36,10 +35,10 @@
|
||||
"tinyexec": "^1.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@changesets/changelog-github": "^0.7.0",
|
||||
"@changesets/cli": "^2.31.0",
|
||||
"@changesets/types": "^6.1.0",
|
||||
"@changesets/write": "^0.4.0",
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@types/node": "^24.12.4",
|
||||
"@types/semver": "^7.7.1",
|
||||
|
||||
Generated
+1044
-273
File diff suppressed because it is too large
Load Diff
+1
-17
@@ -3,23 +3,7 @@ dedupePeers: true
|
||||
dedupePeerDependents: true
|
||||
minimumReleaseAge: 10080
|
||||
minimumReleaseAgeExclude:
|
||||
- "@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"
|
||||
- "@changesets/ghcommit@2.1.1"
|
||||
|
||||
overrides:
|
||||
lightningcss: "-" # we do not bundle any css
|
||||
|
||||
+1
-22
@@ -1,24 +1,3 @@
|
||||
# changesets/action/publish
|
||||
|
||||
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.
|
||||
TODO
|
||||
|
||||
@@ -11,12 +11,6 @@ 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
|
||||
@@ -24,20 +18,12 @@ 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,7 +9,6 @@ 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",
|
||||
|
||||
+1
-3
@@ -1,14 +1,12 @@
|
||||
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");
|
||||
|
||||
+31
-11
@@ -1,6 +1,6 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import path from "node:path";
|
||||
import { exec } from "@actions/exec";
|
||||
import { exec, getExecOutput } from "@actions/exec";
|
||||
import pkgJson from "../package.json" with { type: "json" };
|
||||
|
||||
const tag = `v${pkgJson.version}`;
|
||||
@@ -20,17 +20,37 @@ const gitEnv = {
|
||||
|
||||
process.chdir(path.join(import.meta.dirname, ".."));
|
||||
|
||||
await exec("git", ["checkout", "--detach"]);
|
||||
await exec("git", ["add", "--force", "dist"]);
|
||||
await exec("git", ["commit", "-m", tag]);
|
||||
(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("changeset", ["git-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;
|
||||
}
|
||||
|
||||
if (isPrerelease) {
|
||||
await exec("git", ["push", "origin", `refs/tags/${tag}`], {
|
||||
env: gitEnv,
|
||||
});
|
||||
} else {
|
||||
await exec(
|
||||
"git",
|
||||
[
|
||||
@@ -44,4 +64,4 @@ if (isPrerelease) {
|
||||
env: gitEnv,
|
||||
},
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
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,
|
||||
})),
|
||||
),
|
||||
);
|
||||
}
|
||||
+10
-3
@@ -2,7 +2,7 @@ import { Buffer } from "node:buffer";
|
||||
import * as core from "@actions/core";
|
||||
import { exec, getExecOutput } from "@actions/exec";
|
||||
import { context } from "@actions/github";
|
||||
import { commitChangesSinceBase } from "@changesets/ghcommit";
|
||||
import { commitChangesFromRepo } from "@changesets/ghcommit/git";
|
||||
import { setupOctokit, type Octokit } from "./octokit.ts";
|
||||
|
||||
export type CommitMode = "git-cli" | "github-api";
|
||||
@@ -146,7 +146,14 @@ export class GitHub {
|
||||
|
||||
async pushChanges({ branch, message }: { branch: string; message: string }) {
|
||||
if (this.commitMode === "github-api") {
|
||||
await commitChangesSinceBase({
|
||||
/**
|
||||
* 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({
|
||||
octokit: this.octokit,
|
||||
...context.repo,
|
||||
branch,
|
||||
@@ -155,8 +162,8 @@ export class GitHub {
|
||||
commit: context.sha,
|
||||
},
|
||||
cwd: this.cwd,
|
||||
force: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!(await checkIfClean({ cwd: this.cwd }))) {
|
||||
await commitAll(message, { cwd: this.cwd });
|
||||
|
||||
+24
-64
@@ -3,49 +3,29 @@ import * as core from "@actions/core";
|
||||
import { GitHub } from "./github.ts";
|
||||
import readChangesetState from "./readChangesetState.ts";
|
||||
import { runPublish, runVersion } from "./run.ts";
|
||||
import { uploadStagedRelease } from "./staged-release.ts";
|
||||
import {
|
||||
fileExists,
|
||||
getOptionalInput,
|
||||
getRequiredInput,
|
||||
throwOnRenamedInputs,
|
||||
validateChangesetsCliVersion,
|
||||
} from "./utils.ts";
|
||||
import { fileExists, getOptionalInput } from "./utils.ts";
|
||||
|
||||
(async () => {
|
||||
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||
const cwd = process.cwd();
|
||||
await validateChangesetsCliVersion(cwd);
|
||||
// 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");
|
||||
|
||||
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.",
|
||||
);
|
||||
if (!githubToken) {
|
||||
core.setFailed("Please add the GITHUB_TOKEN to the changesets action");
|
||||
return;
|
||||
}
|
||||
|
||||
const commitMode = getOptionalInput("commit-mode") ?? "git-cli";
|
||||
const prDraft = getOptionalInput("pr-draft");
|
||||
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||
const cwd = process.cwd();
|
||||
|
||||
const commitMode = getOptionalInput("commitMode") ?? "git-cli";
|
||||
const prDraft = getOptionalInput("prDraft");
|
||||
if (commitMode !== "git-cli" && commitMode !== "github-api") {
|
||||
core.setFailed(`Invalid commit mode: ${commitMode}`);
|
||||
return;
|
||||
}
|
||||
if (prDraft !== undefined && prDraft !== "always" && prDraft !== "create") {
|
||||
core.setFailed(`Invalid pr-draft: ${prDraft}`);
|
||||
core.setFailed(`Invalid prDraft: ${prDraft}`);
|
||||
return;
|
||||
}
|
||||
const github = new GitHub({
|
||||
@@ -54,7 +34,7 @@ import {
|
||||
commitMode,
|
||||
});
|
||||
|
||||
let setupGitUser = core.getBooleanInput("setup-git-user");
|
||||
let setupGitUser = core.getBooleanInput("setupGitUser");
|
||||
|
||||
if (setupGitUser) {
|
||||
core.info("setting git user");
|
||||
@@ -63,7 +43,7 @@ import {
|
||||
|
||||
let { changesets } = await readChangesetState(cwd);
|
||||
|
||||
let publishScript = core.getInput("publish-script");
|
||||
let publishScript = core.getInput("publish");
|
||||
let hasChangesets = changesets.length !== 0;
|
||||
const hasNonEmptyChangesets = changesets.some(
|
||||
(changeset) => changeset.releases.length > 0,
|
||||
@@ -71,8 +51,8 @@ import {
|
||||
let hasPublishScript = !!publishScript;
|
||||
|
||||
core.setOutput("published", "false");
|
||||
core.setOutput("published-packages", "[]");
|
||||
core.setOutput("has-changesets", String(hasChangesets));
|
||||
core.setOutput("publishedPackages", "[]");
|
||||
core.setOutput("hasChangesets", String(hasChangesets));
|
||||
|
||||
switch (true) {
|
||||
case !hasChangesets && !hasPublishScript:
|
||||
@@ -130,37 +110,17 @@ import {
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
github,
|
||||
createGithubReleases,
|
||||
pushGitTags,
|
||||
createGithubReleases: core.getBooleanInput("createGithubReleases"),
|
||||
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(
|
||||
"published-packages",
|
||||
"publishedPackages",
|
||||
JSON.stringify(result.publishedPackages),
|
||||
);
|
||||
}
|
||||
@@ -184,17 +144,17 @@ import {
|
||||
return;
|
||||
case hasChangesets: {
|
||||
const { pullRequestNumber } = await runVersion({
|
||||
script: getOptionalInput("version-script"),
|
||||
script: getOptionalInput("version"),
|
||||
github,
|
||||
cwd,
|
||||
prTitle: getOptionalInput("pr-title"),
|
||||
commitMessage: getOptionalInput("commit-message"),
|
||||
prTitle: getOptionalInput("title"),
|
||||
commitMessage: getOptionalInput("commit"),
|
||||
hasPublishScript,
|
||||
prDraft,
|
||||
branch: getOptionalInput("pr-base-branch"),
|
||||
branch: getOptionalInput("branch"),
|
||||
});
|
||||
|
||||
core.setOutput("pr-number", String(pullRequestNumber));
|
||||
core.setOutput("pullRequestNumber", String(pullRequestNumber));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
+2
-5
@@ -7,7 +7,6 @@ import {
|
||||
downloadArtifact,
|
||||
execChangesetsCli,
|
||||
getOptionalInput,
|
||||
validateChangesetsCliVersion,
|
||||
} from "../utils.ts";
|
||||
|
||||
try {
|
||||
@@ -17,12 +16,10 @@ try {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 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");
|
||||
|
||||
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||
const cwd = process.cwd();
|
||||
const tmpDir = process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir()));
|
||||
const outDir = path.join(tmpDir, `changeset-pack-${Date.now()}`);
|
||||
|
||||
|
||||
@@ -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,7 +28,6 @@ describe("getPullRequestWorktree", () => {
|
||||
private: true,
|
||||
workspaces: ["packages/*"],
|
||||
}),
|
||||
"package-lock.json": "",
|
||||
"packages/pkg-a/package.json": JSON.stringify({
|
||||
name: "pkg-a",
|
||||
version: "1.0.0",
|
||||
|
||||
+2
-29
@@ -3,15 +3,11 @@ import os from "node:os";
|
||||
import * as core from "@actions/core";
|
||||
import { GitHub } from "../github.ts";
|
||||
import { runPublish } from "../run.ts";
|
||||
import { uploadStagedRelease } from "../staged-release.ts";
|
||||
import {
|
||||
downloadArtifact,
|
||||
getOptionalBooleanInput,
|
||||
getOptionalInput,
|
||||
getRequiredInput,
|
||||
validateChangesetsCliVersion,
|
||||
} from "../utils.ts";
|
||||
import { assertValidStageInput } from "./options.ts";
|
||||
|
||||
try {
|
||||
await main();
|
||||
@@ -20,26 +16,13 @@ try {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 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 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.",
|
||||
);
|
||||
}
|
||||
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||
const cwd = process.cwd();
|
||||
|
||||
// NOTE: Always use API mode here as publish does not need a commit-mode.
|
||||
const github = new GitHub({ cwd, githubToken, commitMode: "github-api" });
|
||||
@@ -54,22 +37,12 @@ async function main() {
|
||||
|
||||
const result = await runPublish({
|
||||
script,
|
||||
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(
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
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 = {
|
||||
|
||||
+4
-158
@@ -1,11 +1,10 @@
|
||||
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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { GitHub } from "./github.ts";
|
||||
import { readChangesetsOutput, runPublish, runVersion } from "./run.ts";
|
||||
import { runVersion } from "./run.ts";
|
||||
|
||||
vi.mock("@actions/github", () => ({
|
||||
context: {
|
||||
@@ -21,9 +20,7 @@ vi.mock("@actions/github", () => ({
|
||||
graphql: mockedGraphql,
|
||||
}),
|
||||
}));
|
||||
vi.mock("@actions/core", { spy: true });
|
||||
const mockedWarning = vi.mocked(core.warning);
|
||||
vi.mock("@changesets/ghcommit");
|
||||
vi.mock("@changesets/ghcommit/git");
|
||||
|
||||
let mockedGithubMethods = {
|
||||
pulls: {
|
||||
@@ -59,7 +56,6 @@ function createSimpleProjectFixture() {
|
||||
private: true,
|
||||
workspaces: ["packages/*"],
|
||||
}),
|
||||
"package-lock.json": "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,7 +82,6 @@ function createIgnoredPackageFixture() {
|
||||
private: true,
|
||||
workspaces: ["packages/*"],
|
||||
}),
|
||||
"package-lock.json": "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,155 +100,6 @@ 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();
|
||||
|
||||
+58
-174
@@ -1,6 +1,4 @@
|
||||
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 {
|
||||
@@ -31,7 +29,7 @@ import {
|
||||
// To avoid that, we ensure to cap the message to 60k chars.
|
||||
const MAX_CHARACTERS_PER_MESSAGE = 60000;
|
||||
|
||||
export const createRelease = async (
|
||||
const createRelease = async (
|
||||
octokit: Octokit,
|
||||
{ pkg, tagName }: { pkg: Package; tagName: string },
|
||||
) => {
|
||||
@@ -66,149 +64,37 @@ export const createRelease = async (
|
||||
type PublishOptions = {
|
||||
script?: string;
|
||||
fromPackDir?: string;
|
||||
stage?: boolean;
|
||||
createGithubReleases: boolean;
|
||||
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,
|
||||
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: github.getToken(),
|
||||
CHANGESETS_OUTPUT: outputFile,
|
||||
},
|
||||
env: { ...process.env, GITHUB_TOKEN: github.getToken() },
|
||||
};
|
||||
|
||||
if (script) {
|
||||
@@ -219,9 +105,6 @@ 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);
|
||||
}
|
||||
@@ -231,81 +114,75 @@ export async function runPublish({
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
if (pkg === undefined) {
|
||||
throw new Error(
|
||||
`Package "${event.packageName}" not found.` +
|
||||
`Package "${pkgName}" not found.` +
|
||||
"This is probably a bug in the action, please open an issue",
|
||||
);
|
||||
}
|
||||
return { pkg, tag: event.tag };
|
||||
});
|
||||
releasedPackages.push(pkg);
|
||||
}
|
||||
|
||||
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 (createGithubReleases) {
|
||||
await Promise.all(
|
||||
releasedPackages.map(async (pkg) => {
|
||||
const tagName = `${pkg.packageJson.name}@${pkg.packageJson.version}`;
|
||||
await github.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:/;
|
||||
|
||||
if (createGithubReleases || pushGitTags) {
|
||||
await Promise.all(
|
||||
releases.map(async ({ pkg, tag }) => {
|
||||
if (pushGitTags) {
|
||||
await github.pushTag(tag);
|
||||
}
|
||||
for (let line of changesetPublishOutput.stdout.split("\n")) {
|
||||
let match = line.match(newTagRegex);
|
||||
|
||||
if (match) {
|
||||
releasedPackages.push(pkg);
|
||||
if (createGithubReleases) {
|
||||
await createRelease(octokit, { pkg, tagName: tag });
|
||||
const tagName = `v${pkg.packageJson.version}`;
|
||||
await github.pushTag(tagName);
|
||||
await createRelease(octokit, { pkg, tagName });
|
||||
}
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (releases.length) {
|
||||
if (releasedPackages.length) {
|
||||
return {
|
||||
published: true,
|
||||
publishedPackages: releases.map(({ pkg }) => ({
|
||||
publishedPackages: releasedPackages.map((pkg) => ({
|
||||
name: pkg.packageJson.name,
|
||||
version: pkg.packageJson.version,
|
||||
})),
|
||||
...(stagedPackages.length > 0 ? { stagedPackages } : {}),
|
||||
exitCode: changesetPublishOutput.exitCode,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
published: false,
|
||||
...(stagedPackages.length > 0 ? { stagedPackages } : {}),
|
||||
exitCode: changesetPublishOutput.exitCode,
|
||||
};
|
||||
return { published: false, exitCode: changesetPublishOutput.exitCode };
|
||||
}
|
||||
|
||||
type GetMessageOptions = {
|
||||
@@ -444,6 +321,13 @@ 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({
|
||||
...context.repo,
|
||||
state: "open",
|
||||
|
||||
@@ -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, validateChangesetsCliVersion } from "../utils.ts";
|
||||
import { execChangesetsCli } from "../utils.ts";
|
||||
|
||||
type ModeResult =
|
||||
| {
|
||||
@@ -27,10 +27,7 @@ try {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const cwd = process.cwd();
|
||||
await validateChangesetsCliVersion(cwd);
|
||||
|
||||
const result = await getMode(cwd);
|
||||
const result = await getMode();
|
||||
core.setOutput("mode", result.mode);
|
||||
if (result.mode === "publish") {
|
||||
const publishPlanArtifact = await artifact.uploadArtifact(
|
||||
@@ -51,8 +48,8 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
async function getMode(cwd: string): Promise<ModeResult> {
|
||||
const { changesets } = await readChangesetState(cwd);
|
||||
async function getMode(): Promise<ModeResult> {
|
||||
const { changesets } = await readChangesetState();
|
||||
|
||||
if (changesets.length > 0) {
|
||||
const hasNonEmptyChangesets = changesets.some(
|
||||
@@ -64,6 +61,7 @@ async function getMode(cwd: string): 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()}`,
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -1,342 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
+1
-53
@@ -1,11 +1,5 @@
|
||||
import { createFixture } from "fs-fixture";
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
BumpLevels,
|
||||
getChangelogEntry,
|
||||
sortTheThings,
|
||||
validateChangesetsCliVersion,
|
||||
} from "./utils.ts";
|
||||
import { BumpLevels, getChangelogEntry, sortTheThings } from "./utils.ts";
|
||||
|
||||
let changelog = `# @keystone-alpha/email
|
||||
|
||||
@@ -106,49 +100,3 @@ 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.",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -10,8 +10,6 @@ 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);
|
||||
|
||||
@@ -137,84 +135,6 @@ export function getRequiredInput(name: string) {
|
||||
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],
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import * as core from "@actions/core";
|
||||
import { GitHub } from "../github.ts";
|
||||
import { runVersion } from "../run.ts";
|
||||
import {
|
||||
getOptionalInput,
|
||||
getRequiredInput,
|
||||
validateChangesetsCliVersion,
|
||||
} from "../utils.ts";
|
||||
import { getOptionalInput, getRequiredInput } from "../utils.ts";
|
||||
|
||||
try {
|
||||
await main();
|
||||
@@ -14,10 +10,6 @@ try {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 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");
|
||||
@@ -35,6 +27,9 @@ async function main() {
|
||||
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 github = new GitHub({
|
||||
cwd,
|
||||
githubToken,
|
||||
|
||||
+1
-3
@@ -37,9 +37,7 @@ inputs:
|
||||
description: Sets up the git user for commits as `"github-actions[bot]"`. Default to `true`
|
||||
required: false
|
||||
default: true
|
||||
outputs:
|
||||
pr-number:
|
||||
description: The pull request number that was created or updated
|
||||
outputs: {}
|
||||
branding:
|
||||
icon: package
|
||||
color: blue
|
||||
|
||||
Reference in New Issue
Block a user