diff --git a/.agents/skills/canary/SKILL.md b/.agents/skills/canary/SKILL.md index df58c93c3ff6..8ecfbe5d8dbb 100644 --- a/.agents/skills/canary/SKILL.md +++ b/.agents/skills/canary/SKILL.md @@ -1,68 +1,158 @@ --- name: canary -description: Triggers a canary release for a Storybook PR. Use when the user wants to publish a canary version, create a pre-release, or test a PR via npm. +description: Finds or publishes a pkg.pr.new canary release for a Storybook branch. Use when the user wants the canary package specifier for a branch or needs to trigger the canary workflow manually. allowed-tools: Bash --- -# Publish Canary Release +# Find Or Publish Canary Release -Publishes a canary version of Storybook from a PR to npm. +Use this skill to get a branch-specific canary build from `pkg.pr.new`. -## Usage +Canary publishes are driven by the `publish-canary.yml` workflow. -To trigger a canary release, run: +The labels that trigger automatic canary publishes on PRs are: + +- `ci:normal` +- `ci:merged` +- `ci:daily` + +## Version string + +The canary version string is constructed like this: + +```text +storybook@https://pkg.pr.new/storybookjs/storybook/storybook@ +``` + +Replace `` with the full commit SHA. + +## Check whether a canary already exists ```bash -gh workflow run --repo storybookjs/storybook publish.yml --field pr= +SHA=$(git rev-parse HEAD) +curl -I "https://pkg.pr.new/storybookjs/storybook/storybook@$SHA" ``` -## What happens +An HTTP `200` status code means the canary already exists for that commit. + +## Decision flow + +Use this skill with the following if-then behavior. -1. GitHub Actions builds and publishes the PR as `0.0.0-pr--sha-` -2. The version is published to npm with the `canary` tag -3. The PR body is updated with the exact version and install instructions +### A. If the branch already has a PR with one of the CI labels -## Version format +If the branch already has an associated PR labeled `ci:normal`, `ci:merged`, or `ci:daily`, do not trigger anything manually first. Reuse the workflow run that should already exist. -The canary version follows a **predictable structure**: +Find the labeled PR for the current branch: +```bash +BRANCH=$(git branch --show-current) + +gh pr list \ + --repo storybookjs/storybook \ + --head "$BRANCH" \ + --state open \ + --json number,title,labels,url \ + --jq '.[] | select(any(.labels[]?; .name == "ci:normal" or .name == "ci:merged" or .name == "ci:daily"))' ``` -0.0.0-pr--sha- + +Find the latest successful canary workflow run for that branch: + +```bash +BRANCH=$(git branch --show-current) + +RUN_ID=$(gh run list \ + --repo storybookjs/storybook \ + --workflow publish-canary.yml \ + --branch "$BRANCH" \ + --event pull_request \ + --json databaseId,conclusion \ + --jq '.[] | select(.conclusion == "success") | .databaseId' \ + | head -n 1) + +gh run view "$RUN_ID" --repo storybookjs/storybook +``` + +Pull the SHA from that run and construct the version string from it: + +```bash +RUN_SHA=$(gh run view "$RUN_ID" --repo storybookjs/storybook --json headSha --jq '.headSha') +echo "storybook@https://pkg.pr.new/storybookjs/storybook/storybook@$RUN_SHA" ``` -- ``: The PR number (e.g., `33526`) -- ``: First 8 characters of the commit SHA (e.g., `a2e09fa2`) +Optionally confirm the package is live: -**Example:** For PR #33526 with commit `a2e09fa284a...`, the canary version is: -`0.0.0-pr-33526-sha-a2e09fa2` +```bash +curl -I "https://pkg.pr.new/storybookjs/storybook/storybook@$RUN_SHA" +``` -You can construct the version yourself if you know the PR number and the latest commit SHA on that PR. +### B. If the branch does not have a PR with one of the CI labels -## After publishing +Trigger the canary workflow manually on the branch and watch it finish. It usually takes about 10 minutes. -Check the PR body for the published version. It will show something like: +```bash +BRANCH=$(git branch --show-current) -> This pull request has been released as version `0.0.0-pr-33365-sha-b6656566` +gh workflow run --repo storybookjs/storybook publish-canary.yml --ref "$BRANCH" +``` -Then test with: +Find the new workflow run and watch it: ```bash -npx storybook@ sandbox +BRANCH=$(git branch --show-current) + +RUN_ID=$(gh run list \ + --repo storybookjs/storybook \ + --workflow publish-canary.yml \ + --branch "$BRANCH" \ + --event workflow_dispatch \ + --json databaseId \ + --jq '.[0].databaseId') + +gh run watch "$RUN_ID" --repo storybookjs/storybook +``` + +When it finishes successfully, pull the SHA from the run and construct the version string: + +```bash +RUN_SHA=$(gh run view "$RUN_ID" --repo storybookjs/storybook --json headSha --jq '.headSha') +echo "storybook@https://pkg.pr.new/storybookjs/storybook/storybook@$RUN_SHA" +``` + +Optionally confirm the package is live: + +```bash +curl -I "https://pkg.pr.new/storybookjs/storybook/storybook@$RUN_SHA" ``` -Or upgrade an existing project: +## Use the canary + +For a new project: + +```bash +npm create storybook@https://pkg.pr.new/storybookjs/storybook/storybook@ +``` + +For an existing project: ```bash -npx storybook@ upgrade +npx storybook@https://pkg.pr.new/storybookjs/storybook/storybook@ upgrade ``` ## Requirements -- You must have admin permissions on the storybookjs/storybook repo -- The PR must exist and be open -- You need `gh` CLI authenticated +- You need `gh` CLI authenticated for `storybookjs/storybook` +- You need permission to run workflows in the repository for manual dispatch +- The canary workflow is `publish-canary.yml` ## Monitor progress -Watch the workflow run at: -https://github.com/storybookjs/storybook/actions/workflows/publish.yml +Workflow page: + +- https://github.com/storybookjs/storybook/actions/workflows/publish-canary.yml + +CLI: + +```bash +gh run list --repo storybookjs/storybook --workflow publish-canary.yml +``` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a57d93ef584e..0c6fb1253e6d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -65,13 +65,13 @@ Thank you for contributing to Storybook! Please submit all PRs to the `next` bra -### 🦋 Canary release + +## 🦋 Canary Release - 🚫 Not run + -This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the `@storybookjs/core` team here. - -_core team members can create a canary release [here](https://github.com/storybookjs/storybook/actions/workflows/publish.yml) or locally with `gh workflow run --repo storybookjs/storybook publish.yml --field pr=`_ +This PR does not have a canary release associated. Canary releases are automatically created when one of `ci:normal`, `ci:merged`, or `ci:daily` labels are added to the PR. diff --git a/.github/workflows/publish-canary.yml b/.github/workflows/publish-canary.yml new file mode 100644 index 000000000000..44fb5c805225 --- /dev/null +++ b/.github/workflows/publish-canary.yml @@ -0,0 +1,158 @@ +name: Publish Canary Packages + +on: + workflow_dispatch: + pull_request: + types: [opened, synchronize, reopened, labeled] + +env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 + +permissions: + contents: read + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + publish-canary: + name: Publish Canary Packages + if: | + github.event_name == 'workflow_dispatch' || + ( + (github.event.action == 'labeled' && contains(fromJSON('["ci:normal","ci:merged","ci:daily"]'), github.event.label.name)) || + (github.event.action != 'labeled' && ( + contains(github.event.pull_request.labels.*.name, 'ci:normal') || + contains(github.event.pull_request.labels.*.name, 'ci:merged') || + contains(github.event.pull_request.labels.*.name, 'ci:daily') + )) + ) + runs-on: ubuntu-latest + steps: + - name: Build Canary Release Status + id: canary-status + if: ${{ github.event_name == 'pull_request' }} + env: + REPOSITORY: ${{ github.repository }} + RUN_ID: ${{ github.run_id }} + run: | + echo "run_url=https://github.com/$REPOSITORY/actions/runs/$RUN_ID" >> "$GITHUB_OUTPUT" + + - name: "Update Canary Release Heading: Pending" + id: replace-canary-heading-pending + if: ${{ github.event_name == 'pull_request' }} + continue-on-error: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + uses: ivangabriele/find-and-replace-pull-request-body@9d26f37218b2649daea37dbc04bfd092b9ebeeca # v1.1.5 + with: + githubToken: ${{ github.token }} + find: "CANARY_RELEASE_HEADING" + isHtmlCommentTag: true + replace: "## 🦋 Canary Release - ⏳ [Pending](${{ steps.canary-status.outputs.run_url }})" + + - name: Checkout Pull Request Head + uses: actions/checkout@v4 + with: + repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Setup Node.js and Install Dependencies + uses: ./.github/actions/setup-node-and-install + with: + install-code-deps: true + + - name: Build + working-directory: code + run: yarn task --task compile --start-from=auto --no-link + + - name: Publish + id: publish + working-directory: scripts + run: | + yarn workspaces list --json --no-private \ + | jq -j '"../" + .location + "\u0000"' \ + | xargs -0 -r yarn pkg-pr-new publish --no-template --comment=off --json=./pkg-pr-new-output.json + + test -s ./pkg-pr-new-output.json + + - name: Build Canary Release Content + id: canary-content + if: ${{ github.event_name == 'pull_request' }} + working-directory: scripts + env: + COMMIT_SHA: ${{ github.event.pull_request.head.sha }} + run: | + storybook_package_url=$(jq -r '.packages[] | select(.name == "storybook") | .url' ./pkg-pr-new-output.json) + + if [[ -z "$storybook_package_url" ]]; then + storybook_package_url="https://pkg.pr.new/storybookjs/storybook/storybook@$COMMIT_SHA" + fi + + echo 'heading<> "$GITHUB_OUTPUT" + printf '## 🦋 Canary Release - 🚢 Released `%s`\n' "$storybook_package_url" >> "$GITHUB_OUTPUT" + echo 'EOF' >> "$GITHUB_OUTPUT" + + echo 'body<> "$GITHUB_OUTPUT" + cat <> "$GITHUB_OUTPUT" + This pull request has been released as canary packages. Try it out in a new project or update an existing project with the commands below. + + ```sh + # For a new project + npm create storybook@$storybook_package_url + + # or for an existing project + npx storybook@$storybook_package_url upgrade + ``` + + EOF + echo 'EOF' >> "$GITHUB_OUTPUT" + + - name: "Update Canary Release Heading: Released" + id: replace-canary-heading-released + if: ${{ github.event_name == 'pull_request' && steps.canary-content.outcome == 'success' }} + continue-on-error: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + uses: ivangabriele/find-and-replace-pull-request-body@9d26f37218b2649daea37dbc04bfd092b9ebeeca # v1.1.5 + with: + githubToken: ${{ github.token }} + find: "CANARY_RELEASE_HEADING" + isHtmlCommentTag: true + replace: ${{ steps.canary-content.outputs.heading }} + + - name: Update Canary Release Body + id: replace-pr-body + if: ${{ github.event_name == 'pull_request' }} + continue-on-error: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + uses: ivangabriele/find-and-replace-pull-request-body@9d26f37218b2649daea37dbc04bfd092b9ebeeca # v1.1.5 + with: + githubToken: ${{ github.token }} + find: "CANARY_RELEASE_SECTION" + isHtmlCommentTag: true + replace: ${{ steps.canary-content.outputs.body }} + + - name: "Update Canary Release Heading: Failed" + id: replace-canary-heading-failed + if: ${{ github.event_name == 'pull_request' && failure() && steps.publish.outcome != 'success' }} + continue-on-error: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + uses: ivangabriele/find-and-replace-pull-request-body@9d26f37218b2649daea37dbc04bfd092b9ebeeca # v1.1.5 + with: + githubToken: ${{ github.token }} + find: "CANARY_RELEASE_HEADING" + isHtmlCommentTag: true + replace: "## 🦋 Canary Release - 💥 [Failed](${{ steps.canary-status.outputs.run_url }})" + + - name: Create Failure Comment on PR + if: ${{ github.event_name == 'pull_request' && failure() && steps.publish.outcome != 'success' && steps.replace-pr-body.outcome != 'failure' }} + continue-on-error: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPOSITORY: ${{ github.repository }} + TRIGGERING_ACTOR: ${{ github.triggering_actor }} + RUN_ID: ${{ github.run_id }} + run: | + gh pr comment "$PR_NUMBER" \ + --repo "$REPOSITORY" \ + --body "Failed to publish canary packages for this pull request, triggered by @$TRIGGERING_ACTOR. See the failed workflow run at: https://github.com/$REPOSITORY/actions/runs/$RUN_ID" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ca9f12f069c9..21b00245e6ee 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,22 +1,11 @@ name: Publish -run-name: "${{ github.event_name == 'workflow_dispatch' && format('Publish Canary on PR #{0}, triggered by {1}', inputs.pr, github.triggering_actor) || format('Publish new version on {0}, triggered by {1}', github.ref_name, github.triggering_actor) }}" +run-name: "${{ format('Publish new version on {0}, triggered by {1}', github.ref_name, github.triggering_actor) }}" on: push: - # Normal releases, major/minor/patch/prerelease branches: - latest-release - next-release - workflow_dispatch: - # Manual canary releases on PRs - inputs: - pr: - description: "⚠️ CANARY RELEASES ONLY - Enter the pull request number to create a canary release for" - required: true - type: number - pull_request: - # Automated canary releases on PRs with the "with-canary-release"-suffix in the branch name - types: [opened, synchronize, reopened] env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 @@ -25,17 +14,9 @@ env: permissions: id-token: write contents: write - pull-requests: write - -concurrency: - # Group concurrent runs based on the event type: - # - For workflow_dispatch and pull_request: group by PR number to allow only one canary release per PR - # - For push events: group by branch name to prevent multiple releases on the same branch - group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-{1}', github.workflow, inputs.pr) || github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || format('{0}-{1}', github.workflow, github.ref_name) }} - cancel-in-progress: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' }} jobs: - publish-normal: + publish: name: Publish normal version runs-on: ubuntu-latest if: | @@ -202,106 +183,3 @@ jobs: uses: Ilshidur/action-discord@d2594079a10f1d6739ee50a2471f0ca57418b554 # v0.4.0 with: args: "The GitHub Action for publishing version ${{ steps.version.outputs.current-version }} (triggered by ${{ github.triggering_actor }}) failed! See run at: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - - publish-canary: - name: Publish canary version - runs-on: ubuntu-latest - if: | - github.repository_owner == 'storybookjs' && - ( - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && endsWith(github.head_ref, 'with-canary-release')) - ) && - contains(github.event.head_commit.message, '[skip ci]') != true - environment: Release - steps: - - name: Fail if triggering actor is not administrator - uses: prince-chrismc/check-actor-permissions-action@87c6d9b36c730377858fd9719fbbac1b58fa678d # no version attached, ahead of last release - with: - permission: admin - - - name: Get pull request information - id: info - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr || github.event.pull_request.number }} - REPOSITORY: ${{ github.repository }} - run: | - PR_INFO=$(gh pr view "$PR_NUMBER" --repo "$REPOSITORY" --json isCrossRepository,headRefOid,headRefName,headRepository,headRepositoryOwner --jq '{isFork: .isCrossRepository, owner: .headRepositoryOwner.login, repoName: .headRepository.name, branch: .headRefName, sha: .headRefOid}') - echo $PR_INFO - # Loop through each key-value pair in PR_INFO and set as step output - for key in $(echo "$PR_INFO" | jq -r 'keys[]'); do - value=$(echo "$PR_INFO" | jq -r ".$key") - echo "$key=$value" >> "$GITHUB_OUTPUT" - done - echo "repository=$(echo "$PR_INFO" | jq -r ".owner")/$(echo "$PR_INFO" | jq -r ".repoName")" >> $GITHUB_OUTPUT - echo "shortSha=$(echo "$PR_INFO" | jq -r ".sha" | cut -c 1-8)" >> $GITHUB_OUTPUT - echo "date=$(date)" >> $GITHUB_OUTPUT - echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT - - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: ${{ steps.info.outputs.isFork == 'true' && steps.info.outputs.repository || null }} - ref: ${{ steps.info.outputs.sha }} - token: ${{ secrets.GH_TOKEN }} - - - name: Setup Node.js and Install Dependencies - uses: ./.github/actions/setup-node-and-install - with: - install-code-deps: true - - - name: Set version - id: version - working-directory: scripts - env: - PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr || github.event.pull_request.number }} - SHORT_SHA: ${{ steps.info.outputs.shortSha }} - run: | - yarn release:version --exact "0.0.0-pr-$PR_NUMBER-sha-$SHORT_SHA" --verbose - - - name: Publish v${{ steps.version.outputs.next-version }} - env: - NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - working-directory: scripts - run: yarn release:publish --tag canary --verbose - - - name: Replace Pull Request Body - uses: ivangabriele/find-and-replace-pull-request-body@042438c6cbfbacf6a4701d6042f59b1f73db2fd8 # no version attached, ahead of last release - with: - githubToken: ${{ secrets.GH_TOKEN }} - prNumber: ${{ github.event_name == 'workflow_dispatch' && inputs.pr || '' }} - find: "CANARY_RELEASE_SECTION" - isHtmlCommentTag: true - replace: | - This pull request has been released as version `${{ steps.version.outputs.next-version }}`. Try it out in a new sandbox by running `npx storybook@${{ steps.version.outputs.next-version }} sandbox` or in an existing project with `npx storybook@${{ steps.version.outputs.next-version }} upgrade`. -
- More information - - | | | - | --- | --- | - | **Published version** | [`${{ steps.version.outputs.next-version }}`](https://npmjs.com/package/storybook/v/${{ steps.version.outputs.next-version }}) | - | **Triggered by** | @${{ github.triggering_actor }} | - | **Repository** | [${{ steps.info.outputs.repository }}](https://github.com/${{ steps.info.outputs.repository }}) | - | **Branch** | [`${{ steps.info.outputs.branch }}`](https://github.com/${{ steps.info.outputs.repository }}/tree/${{ steps.info.outputs.branch }}) | - | **Commit** | [`${{ steps.info.outputs.shortSha }}`](https://github.com/${{ steps.info.outputs.repository }}/commit/${{ steps.info.outputs.sha }}) | - | **Datetime** | ${{ steps.info.outputs.date }} (`${{ steps.info.outputs.timestamp }}`) | - | **Workflow run** | [${{ github.run_id }}](https://github.com/storybookjs/storybook/actions/runs/${{ github.run_id }}) | - - To request a new release of this pull request, mention the `@storybookjs/core` team. - - _core team members can create a new canary release [here](https://github.com/storybookjs/storybook/actions/workflows/publish.yml) or locally with `gh workflow run --repo storybookjs/storybook publish.yml --field pr=${{ github.event_name == 'workflow_dispatch' && inputs.pr || github.event.pull_request.number }}`_ -
- - - name: Create failing comment on PR - if: failure() - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr || github.event.pull_request.number }} - REPOSITORY: ${{ github.repository }} - TRIGGERING_ACTOR: ${{ github.triggering_actor }} - RUN_ID: ${{ github.run_id }} - run: | - gh pr comment "$PR_NUMBER"\ - --repo "$REPOSITORY"\ - --body "Failed to publish canary version of this pull request, triggered by @$TRIGGERING_ACTOR. See the failed workflow run at: https://github.com/$REPOSITORY/actions/runs/$RUN_ID" diff --git a/CONTRIBUTING/RELEASING.md b/CONTRIBUTING/RELEASING.md index 8c517c161ecb..239e09a4199d 100644 --- a/CONTRIBUTING/RELEASING.md +++ b/CONTRIBUTING/RELEASING.md @@ -22,8 +22,9 @@ - [Releasing changes to older minor versions](#releasing-changes-to-older-minor-versions) - [Releasing Locally in an Emergency 🚨](#releasing-locally-in-an-emergency-) - [Canary Releases](#canary-releases) - - [With GitHub UI](#with-github-ui) - - [With the CLI](#with-the-cli) + - [Manual Canary Release](#manual-canary-release) + - [With GitHub UI](#with-github-ui) + - [With the CLI](#with-the-cli) - [Versioning Scenarios](#versioning-scenarios) - [Prereleases - `7.1.0-alpha.12` -\> `7.1.0-alpha.13`](#prereleases---710-alpha12---710-alpha13) - [Prerelease promotions - `7.1.0-alpha.13` -\> `7.1.0-beta.0`](#prerelease-promotions---710-alpha13---710-beta0) @@ -429,49 +430,24 @@ Before you start you should make sure that your working tree is clean and the re ## Canary Releases -It's possible to release any pull request as a canary release multiple times during development. This is an effective way to try out changes in standalone projects without linking projects together via package managers. +For most pull requests, no manual action is needed to get canary packages. PRs labeled `ci:normal`, `ci:merged`, or `ci:daily` automatically trigger the [canary publish workflow](../.github/workflows/publish-canary.yml), which publishes packages to `pkg.pr.new` and updates the PR body with commands for creating a new project or upgrading an existing one. -To create a canary release, a core team member (or anyone else with administrator privileges) must manually trigger the publish workflow with the pull request number. +### Manual Canary Release -**Before creating a canary release from contributors, the core team member must ensure that the code being released is not malicious.** +If you want to publish a canary without triggering the whole CI or on a branch that doesn't have a PR yet, you can trigger the canary publish workflow yourself on the branch you want to publish. Manual canary publishes only publish the packages; they do not edit any PR body or post comments, so you have to check the workflow to see the published version number. -Creating a canary release can either be done via GitHub's UI or the [CLI](https://cli.github.com/): +#### With GitHub UI -### With GitHub UI - -1. Open the workflow UI at https://github.com/storybookjs/storybook/actions/workflows/publish.yml +1. Open the workflow UI at https://github.com/storybookjs/storybook/actions/workflows/publish-canary.yml 2. On the top right corner, click "Run workflow" -3. For "branch", **always select `next`**, regardless of which branch your pull request is on -4. For the pull request number, input the number for the pull request **without a leading #** - -### With the CLI +3. For "branch", select the branch you want to publish canary packages from -The following command will trigger a workflow run - replace `` with the actual pull request number: +#### With the CLI ```bash -gh workflow run --repo storybookjs/storybook publish.yml --field pr= +gh workflow run --repo storybookjs/storybook publish-canary.yml --ref ``` -When the release succeeds, it will update the "Canary release" section of the pull request with information about the release and how to use it (see example [here](https://github.com/storybookjs/storybook/pull/23508)). If it fails, it will create a comment on the pull request, tagging the triggering actor to let them know that it failed (see example [here](https://github.com/storybookjs/storybook/pull/23508#issuecomment-1642850467)). - -The canary release will have the following version format: `0.0.0-pr--sha-`, e.g., `0.0.0-pr-23508-5ec8c1c3`. Using v0.0.0 ensures that no user will accidentally get the canary release when using a canary with prereleases, eg. `^7.2.0-alpha.0` - -> ** Note ** -> All canary releases are released under the same "canary" dist tag. This means you'll technically be able to install it with `npm install @storybook/cli@canary`. However, this doesn't make sense, as releases from subsequent pull requests will overwrite that tag quickly. Therefore you should always install the specific version string, e.g., `npm install @storybook/cli@0.0.0-pr-23508-sha-5ec8c1c3`. - -
- Isn't there a simpler/smarter way to do this? - -The simple approach would be to release canaries for all pull requests automatically; however, this would be insecure as any contributor with Write privileges to the repository (200+ users) could create a malicious pull request that alters the release script to release a malicious release (e.g., release a patch version that adds a crypto miner). - -To alleviate this, we only allow the "Release" GitHub environment that contains the npm token to be accessible from workflows running on the protected branches (`next`, `main`, etc.). - -You could also be tempted to require approval from admins before running the workflows. However, this would spam the core team with GitHub notifications for workflow runs seeking approval - even when a core team member triggered the workflow. Therefore we are doing it the other way around, requiring contributors and maintainers to ask for a canary release to be created explicitly. - -Instead of triggering the workflow manually, you could also do something smart, like trigger it when there's a specific label on the pull request or when someone writes a specific comment on the pull request. However, this would create a lot of unnecessary workflow runs because there isn't a way to filter workflow runs based on labels or comment content. The only way to achieve this would be to trigger the workflow on every comment/labeling, then cancel it if it didn't contain the expected content, which is inefficient. - -
- ## Versioning Scenarios There are multiple types of releases that use the same principles, but are done somewhat differently. diff --git a/code/addons/a11y/package.json b/code/addons/a11y/package.json index dbb19f47ec2e..0e8bc01204a1 100644 --- a/code/addons/a11y/package.json +++ b/code/addons/a11y/package.json @@ -25,7 +25,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/addons/a11y" }, "funding": { diff --git a/code/addons/docs/package.json b/code/addons/docs/package.json index 15d42885084e..7defa3272dc7 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -28,7 +28,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/addons/docs" }, "funding": { diff --git a/code/addons/links/package.json b/code/addons/links/package.json index 8bab9cb627f8..5eaa029b8acd 100644 --- a/code/addons/links/package.json +++ b/code/addons/links/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/addons/links" }, "funding": { diff --git a/code/addons/onboarding/package.json b/code/addons/onboarding/package.json index c06e86f798ca..d60c06b3ef05 100644 --- a/code/addons/onboarding/package.json +++ b/code/addons/onboarding/package.json @@ -21,7 +21,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/addons/onboarding" }, "funding": { diff --git a/code/addons/pseudo-states/package.json b/code/addons/pseudo-states/package.json index 7f525d4b93f8..c19ef5f25178 100644 --- a/code/addons/pseudo-states/package.json +++ b/code/addons/pseudo-states/package.json @@ -23,7 +23,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/addons/pseudo-states" }, "funding": { diff --git a/code/addons/themes/package.json b/code/addons/themes/package.json index e5b741b225bb..fcccbd666519 100644 --- a/code/addons/themes/package.json +++ b/code/addons/themes/package.json @@ -24,7 +24,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/addons/themes" }, "funding": { diff --git a/code/addons/vitest/package.json b/code/addons/vitest/package.json index 6ebd4c572753..660e0bdf7912 100644 --- a/code/addons/vitest/package.json +++ b/code/addons/vitest/package.json @@ -22,7 +22,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/addons/vitest" }, "funding": { diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json index c98f79c6dcfb..014889948470 100644 --- a/code/builders/builder-vite/package.json +++ b/code/builders/builder-vite/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/builders/builder-vite" }, "funding": { diff --git a/code/builders/builder-webpack5/package.json b/code/builders/builder-webpack5/package.json index 8d0a9697da52..bd63626eaa4f 100644 --- a/code/builders/builder-webpack5/package.json +++ b/code/builders/builder-webpack5/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/builders/builder-webpack5" }, "funding": { diff --git a/code/core/package.json b/code/core/package.json index 7d00d9dc88e5..b8b4f462e1c3 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -25,7 +25,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/core" }, "funding": { diff --git a/code/core/src/common/index.ts b/code/core/src/common/index.ts index 45fed303b598..ec4c0109734f 100644 --- a/code/core/src/common/index.ts +++ b/code/core/src/common/index.ts @@ -10,6 +10,7 @@ export * from './utils/envs.ts'; export * from './utils/common-glob-options.ts'; export * from './utils/framework.ts'; export * from './utils/get-builder-options.ts'; +export * from './utils/get-pkg-pr-new-package-specifier.ts'; export * from './utils/get-framework-name.ts'; export * from './utils/get-renderer-name.ts'; export * from './utils/get-storybook-configuration.ts'; diff --git a/code/core/src/common/js-package-manager/JsPackageManager.test.ts b/code/core/src/common/js-package-manager/JsPackageManager.test.ts index 63353cc26828..cbdfccb33689 100644 --- a/code/core/src/common/js-package-manager/JsPackageManager.test.ts +++ b/code/core/src/common/js-package-manager/JsPackageManager.test.ts @@ -46,5 +46,14 @@ describe('JsPackageManager', () => { expect(result).toEqual(['@storybook/new-addon@^next']); }); + + it('should map pkg.pr.new create-storybook specifiers to Storybook packages', async () => { + const result = await jsPackageManager.getVersionedPackages(['@storybook/react'], { + storybookVersionSpecifier: 'https://pkg.pr.new/create-storybook@abc123', + }); + + expect(result).toEqual(['@storybook/react@https://pkg.pr.new/@storybook/react@abc123']); + expect(mockLatestVersion).not.toHaveBeenCalled(); + }); }); }); diff --git a/code/core/src/common/js-package-manager/JsPackageManager.ts b/code/core/src/common/js-package-manager/JsPackageManager.ts index 0c2b70f72d6b..27fd45dc055e 100644 --- a/code/core/src/common/js-package-manager/JsPackageManager.ts +++ b/code/core/src/common/js-package-manager/JsPackageManager.ts @@ -15,6 +15,7 @@ import invariant from 'tiny-invariant'; import { HandledError } from '../utils/HandledError.ts'; import type { ExecuteCommandOptions } from '../utils/command.ts'; +import { getPkgPrNewPackageSpecifier } from '../utils/get-pkg-pr-new-package-specifier.ts'; import { findFilesUp, getProjectRoot } from '../utils/paths.ts'; import storybookPackagesVersions from '../versions.ts'; import type { PackageJson, PackageJsonWithDepsAndDevDeps } from './PackageJson.ts'; @@ -71,14 +72,20 @@ export function getPrettyPackageManagerName(packageManager: string | undefined): * @returns A tuple of 2 elements: [packageName, packageVersion] */ export function getPackageDetails(pkg: string): [string, string?] { - const idx = pkg.lastIndexOf('@'); - // If the only `@` is the first character, it is a scoped package - // If it isn't in the string, it will be -1 - if (idx <= 0) { + const isScopedPackage = pkg.startsWith('@'); + const scopeSeparatorIndex = isScopedPackage ? pkg.indexOf('/') : -1; + const versionSeparatorIndex = isScopedPackage + ? scopeSeparatorIndex === -1 + ? -1 + : pkg.indexOf('@', scopeSeparatorIndex + 1) + : pkg.indexOf('@'); + + if (versionSeparatorIndex <= 0) { return [pkg, undefined]; } - const packageName = pkg.slice(0, idx); - const packageVersion = pkg.slice(idx + 1); + + const packageName = pkg.slice(0, versionSeparatorIndex); + const packageVersion = pkg.slice(versionSeparatorIndex + 1); return [packageName, packageVersion]; } @@ -420,7 +427,10 @@ export abstract class JsPackageManager { * * @param packages */ - public getVersionedPackages(packages: string[]): Promise { + public getVersionedPackages( + packages: string[], + options?: { storybookVersionSpecifier?: string } + ): Promise { return Promise.all( packages.map(async (pkg) => { const [packageName, packageVersion] = getPackageDetails(pkg); @@ -431,6 +441,17 @@ export abstract class JsPackageManager { return pkg; } + if (packageName in storybookPackagesVersions) { + const pkgPrNewSpecifier = getPkgPrNewPackageSpecifier( + packageName, + options?.storybookVersionSpecifier + ); + + if (pkgPrNewSpecifier) { + return `${packageName}@${pkgPrNewSpecifier}`; + } + } + const latestInRange = await this.latestVersion(packageName, packageVersion); const k = packageName as keyof typeof storybookPackagesVersions; diff --git a/code/core/src/common/utils/get-pkg-pr-new-package-specifier.ts b/code/core/src/common/utils/get-pkg-pr-new-package-specifier.ts new file mode 100644 index 000000000000..9fa32f2c3df8 --- /dev/null +++ b/code/core/src/common/utils/get-pkg-pr-new-package-specifier.ts @@ -0,0 +1,23 @@ +const PKG_PR_NEW_STORYBOOK_SPECIFIER_RE = + /^(https?:\/\/[^/\s]*pkg\.pr\.new\/)(?:create-storybook|storybook|@storybook\/[^@\s]+)(@[^\s]+)$/; + +export const isPkgPrNewVersionSpecifier = (storybookVersionSpecifier?: string) => + !!storybookVersionSpecifier?.match(PKG_PR_NEW_STORYBOOK_SPECIFIER_RE); + +export const getPkgPrNewPackageSpecifier = ( + packageName: string, + storybookVersionSpecifier?: string +) => { + if (!storybookVersionSpecifier) { + return undefined; + } + + const match = storybookVersionSpecifier.match(PKG_PR_NEW_STORYBOOK_SPECIFIER_RE); + + if (!match) { + return undefined; + } + + const [, prefix, suffix] = match; + return `${prefix}${packageName}${suffix}`; +}; diff --git a/code/frameworks/angular/package.json b/code/frameworks/angular/package.json index 9b8f71b290a6..beb22b063fba 100644 --- a/code/frameworks/angular/package.json +++ b/code/frameworks/angular/package.json @@ -15,7 +15,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/angular" }, "funding": { diff --git a/code/frameworks/ember/package.json b/code/frameworks/ember/package.json index 1919c22e7dee..a66685a21853 100644 --- a/code/frameworks/ember/package.json +++ b/code/frameworks/ember/package.json @@ -15,7 +15,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/ember" }, "funding": { diff --git a/code/frameworks/html-vite/package.json b/code/frameworks/html-vite/package.json index c2bac355d224..a436bcc367b0 100644 --- a/code/frameworks/html-vite/package.json +++ b/code/frameworks/html-vite/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/html-vite" }, "funding": { diff --git a/code/frameworks/nextjs-vite/package.json b/code/frameworks/nextjs-vite/package.json index 5fb3de4ca54d..66a398acaecf 100644 --- a/code/frameworks/nextjs-vite/package.json +++ b/code/frameworks/nextjs-vite/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/nextjs" }, "funding": { diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index d21e84328319..9d8b146a3de2 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/nextjs" }, "funding": { diff --git a/code/frameworks/preact-vite/package.json b/code/frameworks/preact-vite/package.json index 60c3b76df2c9..90f81aace324 100644 --- a/code/frameworks/preact-vite/package.json +++ b/code/frameworks/preact-vite/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/preact-vite" }, "funding": { diff --git a/code/frameworks/react-native-web-vite/package.json b/code/frameworks/react-native-web-vite/package.json index 6b8893bfa9db..d7d03201f2a1 100644 --- a/code/frameworks/react-native-web-vite/package.json +++ b/code/frameworks/react-native-web-vite/package.json @@ -18,7 +18,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/react-native-web-vite" }, "funding": { diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json index 27f2070b71e2..793bdb51b2c8 100644 --- a/code/frameworks/react-vite/package.json +++ b/code/frameworks/react-vite/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/react-vite" }, "funding": { diff --git a/code/frameworks/react-webpack5/package.json b/code/frameworks/react-webpack5/package.json index 524652bea225..e222c963e885 100644 --- a/code/frameworks/react-webpack5/package.json +++ b/code/frameworks/react-webpack5/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/react-webpack5" }, "funding": { diff --git a/code/frameworks/server-webpack5/package.json b/code/frameworks/server-webpack5/package.json index 32675ba48254..1cea6085fe1a 100644 --- a/code/frameworks/server-webpack5/package.json +++ b/code/frameworks/server-webpack5/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/server-webpack5" }, "funding": { diff --git a/code/frameworks/svelte-vite/package.json b/code/frameworks/svelte-vite/package.json index ff19c0fe11ec..3d172b2a571c 100644 --- a/code/frameworks/svelte-vite/package.json +++ b/code/frameworks/svelte-vite/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/svelte-vite" }, "funding": { diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index 2469f0eeca34..df72ef47e0c5 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -18,7 +18,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/sveltekit" }, "funding": { diff --git a/code/frameworks/tanstack-react/package.json b/code/frameworks/tanstack-react/package.json index fa21f3b80a4e..f106181b36b9 100644 --- a/code/frameworks/tanstack-react/package.json +++ b/code/frameworks/tanstack-react/package.json @@ -19,7 +19,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/tanstack-react" }, "funding": { diff --git a/code/frameworks/vue3-vite/package.json b/code/frameworks/vue3-vite/package.json index e201a001373d..b3485f7d868b 100644 --- a/code/frameworks/vue3-vite/package.json +++ b/code/frameworks/vue3-vite/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/vue3-vite" }, "funding": { diff --git a/code/frameworks/web-components-vite/package.json b/code/frameworks/web-components-vite/package.json index 411a803d7e7a..a4dbf4c887ab 100644 --- a/code/frameworks/web-components-vite/package.json +++ b/code/frameworks/web-components-vite/package.json @@ -18,7 +18,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/frameworks/web-components-vite" }, "funding": { diff --git a/code/lib/cli-sb/package.json b/code/lib/cli-sb/package.json index e306cdbe4673..d766e7a4c687 100644 --- a/code/lib/cli-sb/package.json +++ b/code/lib/cli-sb/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/lib/cli-sb" }, "funding": { diff --git a/code/lib/cli-storybook/package.json b/code/lib/cli-storybook/package.json index 5d85e36d3f3a..7cc4c3a41512 100644 --- a/code/lib/cli-storybook/package.json +++ b/code/lib/cli-storybook/package.json @@ -16,7 +16,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/lib/cli-storybook" }, "funding": { @@ -56,6 +56,7 @@ "leven": "^4.0.0", "p-limit": "^7.2.0", "picocolors": "^1.1.0", + "process-ancestry": "^0.0.2", "semver": "^7.7.3", "slash": "^5.0.0", "tiny-invariant": "^1.3.3", diff --git a/code/lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages.ts b/code/lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages.ts index 1741555024f5..e73a2d885852 100644 --- a/code/lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages.ts +++ b/code/lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages.ts @@ -103,11 +103,6 @@ export const checkPackageCompatibility = async ( export const getIncompatibleStorybookPackages = async ( context: Context ): Promise => { - if (context.currentStorybookVersion.includes('0.0.0')) { - // We can't know if a Storybook canary version is compatible with other packages, so we skip it - return []; - } - const allDeps = context.packageManager.getAllDependencies(); const storybookLikeDeps = Object.keys(allDeps).filter((dep) => dep.includes('storybook')); if (storybookLikeDeps.length === 0 && !context.skipErrors) { diff --git a/code/lib/cli-storybook/src/upgrade.test.ts b/code/lib/cli-storybook/src/upgrade.test.ts index 08bf9adced34..db724a7f0a73 100644 --- a/code/lib/cli-storybook/src/upgrade.test.ts +++ b/code/lib/cli-storybook/src/upgrade.test.ts @@ -4,13 +4,22 @@ import type * as sbcc from 'storybook/internal/common'; import type { JsPackageManager } from 'storybook/internal/common'; import { getStorybookVersion } from './upgrade.ts'; -import { generateUpgradeSpecs } from './util.ts'; +import { collectProjects, generateUpgradeSpecs } from './util.ts'; const findInstallationsMock = vi.fn<(arg: string[]) => Promise>(); const getInstalledVersionMock = vi.fn<(arg: string) => Promise>(); +const { getStorybookDataMock } = vi.hoisted(() => ({ + getStorybookDataMock: vi.fn(), +})); vi.mock('storybook/internal/telemetry'); +vi.mock('./autoblock/index.ts', () => ({ + autoblock: vi.fn(async () => null), +})); +vi.mock('./automigrate/helpers/mainConfigFile.ts', () => ({ + getStorybookData: getStorybookDataMock, +})); vi.mock('storybook/internal/common', async (importOriginal) => { const originalModule = (await importOriginal()) as typeof sbcc; return { @@ -149,6 +158,58 @@ describe('toUpgradedDependencies', () => { expect(result).toEqual(['@storybook/react@9.0.0']); }); + + it('should use pkg.pr.new specs for monorepo packages when invoked from a preview URL', async () => { + const deps = { + '@storybook/react': '^8.0.0', + '@storybook/vue3': '~8.0.0', + }; + + const result = await generateUpgradeSpecs(deps, { + packageManager: mockPackageManager, + isCanary: false, + isCLIOutdated: false, + isCLIPrerelease: false, + isCLIExactPrerelease: false, + isCLIExactLatest: false, + storybookVersionSpecifier: 'https://pkg.pr.new/storybook@abc123', + }); + + expect(result).toEqual([ + '@storybook/react@https://pkg.pr.new/@storybook/react@abc123', + '@storybook/vue3@https://pkg.pr.new/@storybook/vue3@abc123', + ]); + }); + + it('should treat pkg.pr.new Storybook specifiers as canaries during project collection', async () => { + const mockPackageManager = { + latestVersion: vi.fn(async (packageName: string) => + packageName === 'storybook@next' ? '9.1.0-beta.1' : '9.0.0' + ), + } as unknown as JsPackageManager; + + getStorybookDataMock.mockResolvedValueOnce({ + configDir: '.storybook', + mainConfig: false, + mainConfigPath: undefined, + packageManager: mockPackageManager, + previewConfigPath: undefined, + storiesPaths: [], + versionSpecifier: 'https://pkg.pr.new/storybookjs/storybook/storybook@abc123', + versionInstalled: '10.0.0', + hasCsfFactoryPreview: false, + }); + + const results = await collectProjects({ force: true } as any, ['.storybook'], () => {}); + + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + isCanary: true, + beforeVersion: '10.0.0', + currentCLIVersion: '9.0.0', + storybookVersionSpecifier: 'https://pkg.pr.new/storybookjs/storybook/storybook@abc123', + }); + }); }); describe('satellite packages', () => { diff --git a/code/lib/cli-storybook/src/upgrade.ts b/code/lib/cli-storybook/src/upgrade.ts index f1590daa6047..1199e4fabf09 100644 --- a/code/lib/cli-storybook/src/upgrade.ts +++ b/code/lib/cli-storybook/src/upgrade.ts @@ -22,6 +22,7 @@ import { telemetry } from 'storybook/internal/telemetry'; import { sync as spawnSync } from 'cross-spawn'; import picocolors from 'picocolors'; +import { getProcessAncestry } from 'process-ancestry'; import semver, { clean, lt } from 'semver'; import { dedent } from 'ts-dedent'; @@ -78,6 +79,21 @@ const deprecatedPackages = [ const formatPackage = (pkg: Package) => `${pkg.package}@${pkg.version}`; +const getStorybookVersionSpecifierFromAncestry = (): string | undefined => { + try { + for (const ancestor of getProcessAncestry().toReversed()) { + const match = ancestor.command?.match(/\s(?:create-storybook|storybook)@([^\s]+)/); + if (match) { + return match[1]; + } + } + } catch { + // Ignore ancestry lookup failures and fall back to embedded version behavior. + } + + return undefined; +}; + const warnPackages = (pkgs: Package[]) => pkgs.map((pkg) => `- ${formatPackage(pkg)}`).join('\n'); export const checkVersionConsistency = () => { @@ -326,6 +342,7 @@ async function sendMultiUpgradeTelemetry(options: MultiUpgradeTelemetryOptions) } export async function upgrade(options: UpgradeOptions): Promise { + const storybookVersionSpecifier = getStorybookVersionSpecifierFromAncestry(); const projectsResult = await getProjects(options); if (projectsResult === undefined || projectsResult.selectedProjects.length === 0) { @@ -430,6 +447,8 @@ export async function upgrade(options: UpgradeOptions): Promise { isCLIPrerelease: project.isCLIPrerelease, isCLIExactLatest: project.isCLIExactLatest, isCLIExactPrerelease: project.isCLIExactPrerelease, + storybookVersionSpecifier: + storybookVersionSpecifier ?? project.storybookVersionSpecifier, }); } task.success(`Updated package versions in package.json files`); diff --git a/code/lib/cli-storybook/src/util.ts b/code/lib/cli-storybook/src/util.ts index 6a74118de8a1..06938ac4f2ab 100644 --- a/code/lib/cli-storybook/src/util.ts +++ b/code/lib/cli-storybook/src/util.ts @@ -1,6 +1,14 @@ import type { PackageJsonWithDepsAndDevDeps } from 'storybook/internal/common'; -import { HandledError, JsPackageManager, normalizeStories } from 'storybook/internal/common'; -import { getProjectRoot, isSatelliteAddon, versions } from 'storybook/internal/common'; +import { + HandledError, + JsPackageManager, + getPkgPrNewPackageSpecifier, + getProjectRoot, + isPkgPrNewVersionSpecifier, + isSatelliteAddon, + normalizeStories, + versions, +} from 'storybook/internal/common'; import { StoryIndexGenerator, experimental_loadStorybook } from 'storybook/internal/core-server'; import { logTracker, logger, prompt } from 'storybook/internal/node-logger'; import { @@ -32,6 +40,7 @@ interface UpgradeConfig { readonly isCLIPrerelease: boolean; readonly isCLIExactPrerelease: boolean; readonly isCLIExactLatest: boolean; + readonly storybookVersionSpecifier?: string; } /** Result of successfully collecting project data */ @@ -140,7 +149,10 @@ const getVersionModifier = (versionSpecifier: string): VersionModifier => { * @returns True if the version is a canary release */ const isCanaryVersion = (version: string): boolean => - version.startsWith('0.0.0') || version.startsWith('portal:') || version.startsWith('workspace:'); + version.startsWith('0.0.0') || + version.startsWith('portal:') || + version.startsWith('workspace:') || + isPkgPrNewVersionSpecifier(version); /** * Validates that a version string is not empty or undefined @@ -291,6 +303,7 @@ const processProject = async ({ packageManager, previewConfigPath, storiesPaths, + versionSpecifier, versionInstalled, hasCsfFactoryPreview, } = await getStorybookData({ configDir }); @@ -298,7 +311,10 @@ const processProject = async ({ // Validate version and upgrade compatibility logger.debug(`${name} - Validating before version... ${versionInstalled}`); validateVersion(versionInstalled); - const isCanary = isCanaryVersion(currentCLIVersion) || isCanaryVersion(versionInstalled); + const isCanary = + isCanaryVersion(currentCLIVersion) || + isCanaryVersion(versionInstalled) || + isPkgPrNewVersionSpecifier(versionSpecifier); logger.debug(`${name} - Validating upgrade compatibility...`); validateUpgradeCompatibility(currentCLIVersion, versionInstalled, isCanary); @@ -347,6 +363,7 @@ const processProject = async ({ currentCLIVersion, latestCLIVersionOnNPM: latestCLIVersionOnNPM!, isCLIExactPrerelease, + storybookVersionSpecifier: versionSpecifier, autoblockerCheckResults, previewConfigPath, storiesPaths, @@ -430,6 +447,15 @@ export const generateUpgradeSpecs = async ( const storybookCoreUpgrades = monorepoDependencies.map((dependency) => { const versionSpec = dependencies[dependency]; + const pkgPrNewSpecifier = getPkgPrNewPackageSpecifier( + dependency, + config.storybookVersionSpecifier + ); + + if (pkgPrNewSpecifier) { + return `${dependency}@${pkgPrNewSpecifier}`; + } + if (!versionSpec) { return `${dependency}@${versions[dependency]}`; } diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index a8c2acde89cc..5012d35398ec 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/lib/codemod" }, "funding": { diff --git a/code/lib/core-webpack/package.json b/code/lib/core-webpack/package.json index 73ce7654429a..f132371e30e8 100644 --- a/code/lib/core-webpack/package.json +++ b/code/lib/core-webpack/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/lib/core-webpack" }, "funding": { diff --git a/code/lib/create-storybook/package.json b/code/lib/create-storybook/package.json index 9433fab45f61..4df48654a272 100644 --- a/code/lib/create-storybook/package.json +++ b/code/lib/create-storybook/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/lib/cli" }, "funding": { diff --git a/code/lib/create-storybook/src/commands/GeneratorExecutionCommand.ts b/code/lib/create-storybook/src/commands/GeneratorExecutionCommand.ts index 051563029fd1..4d98d3e1eca4 100644 --- a/code/lib/create-storybook/src/commands/GeneratorExecutionCommand.ts +++ b/code/lib/create-storybook/src/commands/GeneratorExecutionCommand.ts @@ -98,6 +98,7 @@ export class GeneratorExecutionCommand { renderer: frameworkInfo.renderer, builder: frameworkInfo.builder, language, + storybookVersionSpecifier: options.storybookVersionSpecifier, telemetryService: this.telemetryService, linkable: !!options.linkable, features: selectedFeatures, @@ -110,6 +111,7 @@ export class GeneratorExecutionCommand { builder: frameworkInfo.builder, framework: frameworkInfo.framework, renderer: frameworkInfo.renderer, + storybookVersionSpecifier: options.storybookVersionSpecifier, linkable: !!options.linkable, pnp: !!options.usePnp, yes: !!options.yes, diff --git a/code/lib/create-storybook/src/commands/PreflightCheckCommand.ts b/code/lib/create-storybook/src/commands/PreflightCheckCommand.ts index 32c22fe6fb5d..ae184c6a7275 100644 --- a/code/lib/create-storybook/src/commands/PreflightCheckCommand.ts +++ b/code/lib/create-storybook/src/commands/PreflightCheckCommand.ts @@ -11,6 +11,7 @@ import { CLI_COLORS, deprecate, logger } from 'storybook/internal/node-logger'; import { MinimumReleaseAgeHandledError } from 'storybook/internal/server-errors'; import { dedent } from 'ts-dedent'; +import { getProcessAncestry } from 'process-ancestry'; import type { CommandOptions } from '../generators/types.ts'; import { currentDirectoryIsEmpty, scaffoldNewProject } from '../scaffold-new-project.ts'; @@ -37,6 +38,13 @@ export class PreflightCheckCommand { private readonly telemetryService = new TelemetryService() ) {} async execute(options: CommandOptions): Promise { + try { + options.storybookVersionSpecifier = + this.versionService.getStorybookVersionFromAncestry(getProcessAncestry()); + } catch { + // Ignore ancestry lookup failures and fall back to the embedded release versions. + } + const isEmptyDirProject = options.force !== true && currentDirectoryIsEmpty(); let packageManagerType = JsPackageManagerFactory.getPackageManagerType(); diff --git a/code/lib/create-storybook/src/generators/REACT_NATIVE/index.ts b/code/lib/create-storybook/src/generators/REACT_NATIVE/index.ts index 03f0a2672e8e..81395a1b85fe 100644 --- a/code/lib/create-storybook/src/generators/REACT_NATIVE/index.ts +++ b/code/lib/create-storybook/src/generators/REACT_NATIVE/index.ts @@ -130,7 +130,9 @@ export default defineGeneratorModule({ 'storybook', ]; - const versionedPackages = await packageManager.getVersionedPackages(packagesToResolve); + const versionedPackages = await packageManager.getVersionedPackages(packagesToResolve, { + storybookVersionSpecifier: context.storybookVersionSpecifier, + }); const babelDependencies = await getBabelDependencies(packageManager as any); const packages: string[] = [ diff --git a/code/lib/create-storybook/src/generators/baseGenerator.ts b/code/lib/create-storybook/src/generators/baseGenerator.ts index 9255d7dc302b..da9de7c3780a 100644 --- a/code/lib/create-storybook/src/generators/baseGenerator.ts +++ b/code/lib/create-storybook/src/generators/baseGenerator.ts @@ -130,7 +130,16 @@ const hasFrameworkTemplates = (framework?: string) => { export async function baseGenerator( packageManager: JsPackageManager, npmOptions: NpmOptions, - { language, builder, framework, renderer, pnp, features, dependencyCollector }: GeneratorOptions, + { + language, + builder, + framework, + renderer, + pnp, + features, + dependencyCollector, + storybookVersionSpecifier, + }: GeneratorOptions, _options: FrameworkOptions ) { const options = { ...defaultOptions, ..._options }; @@ -222,7 +231,8 @@ export async function baseGenerator( } const versionedPackages = await packageManager.getVersionedPackages( - packagesToInstall as string[] + packagesToInstall as string[], + { storybookVersionSpecifier } ); if (versionedPackages.length > 0) { diff --git a/code/lib/create-storybook/src/generators/types.ts b/code/lib/create-storybook/src/generators/types.ts index 241c8d793dfb..c052e986b214 100644 --- a/code/lib/create-storybook/src/generators/types.ts +++ b/code/lib/create-storybook/src/generators/types.ts @@ -19,6 +19,7 @@ export type GeneratorOptions = { builder: SupportedBuilder; framework: SupportedFramework; renderer: SupportedRenderer; + storybookVersionSpecifier?: string; linkable: boolean; // TODO: Remove in SB11 pnp: boolean; @@ -92,6 +93,7 @@ export interface GeneratorContext { renderer: SupportedRenderer; builder: SupportedBuilder; language: SupportedLanguage; + storybookVersionSpecifier?: string; telemetryService: TelemetryService; features: Set; dependencyCollector: DependencyCollector; @@ -130,6 +132,7 @@ export interface GeneratorModule { export type CommandOptions = { packageManager: PackageManagerName; + storybookVersionSpecifier?: string; usePnp?: boolean; features?: Array; type?: ProjectType; diff --git a/code/lib/create-storybook/src/services/VersionService.test.ts b/code/lib/create-storybook/src/services/VersionService.test.ts index 0b2d0f839d42..96b12f73c0c0 100644 --- a/code/lib/create-storybook/src/services/VersionService.test.ts +++ b/code/lib/create-storybook/src/services/VersionService.test.ts @@ -90,6 +90,17 @@ describe('VersionService', () => { expect(version).toBe('latest'); }); + it('should extract pkg.pr.new specifier from create-storybook command', () => { + const ancestry = [ + { command: 'npx create-storybook@https://pkg.pr.new/create-storybook@abc123' }, + { command: 'node /usr/local/bin/npm' }, + ]; + + const version = versionService.getStorybookVersionFromAncestry(ancestry as any); + + expect(version).toBe('https://pkg.pr.new/create-storybook@abc123'); + }); + it('should return undefined if no version found', () => { const ancestry = [{ command: 'npm install' }, { command: 'node /usr/local/bin/npm' }]; diff --git a/code/lib/csf-plugin/package.json b/code/lib/csf-plugin/package.json index b2f0450c0501..5fc1c6ef9b61 100644 --- a/code/lib/csf-plugin/package.json +++ b/code/lib/csf-plugin/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/lib/csf-plugin" }, "funding": { diff --git a/code/lib/eslint-plugin/package.json b/code/lib/eslint-plugin/package.json index 4b5c0a088de7..63c3b76e802a 100644 --- a/code/lib/eslint-plugin/package.json +++ b/code/lib/eslint-plugin/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/lib/eslint-plugin" }, "license": "MIT", diff --git a/code/lib/react-dom-shim/package.json b/code/lib/react-dom-shim/package.json index 760b3680bc07..c55941814d8e 100644 --- a/code/lib/react-dom-shim/package.json +++ b/code/lib/react-dom-shim/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/lib/react-dom-shim" }, "funding": { diff --git a/code/package.json b/code/package.json index 639b9ec36f48..c1aa6cf4a7c0 100644 --- a/code/package.json +++ b/code/package.json @@ -6,7 +6,7 @@ "homepage": "https://storybook.js.org/", "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git" + "url": "git+https://github.com/storybookjs/storybook.git" }, "funding": { "type": "opencollective", diff --git a/code/presets/create-react-app/package.json b/code/presets/create-react-app/package.json index 91b25ac23fc0..33117496ff86 100644 --- a/code/presets/create-react-app/package.json +++ b/code/presets/create-react-app/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/presets/create-react-app" }, "funding": { diff --git a/code/presets/react-webpack/package.json b/code/presets/react-webpack/package.json index b7105987ede9..064f20f6030b 100644 --- a/code/presets/react-webpack/package.json +++ b/code/presets/react-webpack/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/presets/react-webpack" }, "funding": { diff --git a/code/presets/server-webpack/package.json b/code/presets/server-webpack/package.json index 8fec72350385..69085f904e53 100644 --- a/code/presets/server-webpack/package.json +++ b/code/presets/server-webpack/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/presets/server-webpack" }, "funding": { diff --git a/code/renderers/html/package.json b/code/renderers/html/package.json index b1eb569f76f7..d65ce3bc0905 100644 --- a/code/renderers/html/package.json +++ b/code/renderers/html/package.json @@ -15,7 +15,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/renderers/html" }, "funding": { diff --git a/code/renderers/preact/package.json b/code/renderers/preact/package.json index 3b26426e7fce..e676baae427a 100644 --- a/code/renderers/preact/package.json +++ b/code/renderers/preact/package.json @@ -15,7 +15,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/renderers/preact" }, "funding": { diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json index 741f83c6d7e7..566cc90851b4 100644 --- a/code/renderers/react/package.json +++ b/code/renderers/react/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/renderers/react" }, "funding": { diff --git a/code/renderers/server/package.json b/code/renderers/server/package.json index 7b91e7db52cc..92fbb90c7755 100644 --- a/code/renderers/server/package.json +++ b/code/renderers/server/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/renderers/server" }, "funding": { diff --git a/code/renderers/svelte/package.json b/code/renderers/svelte/package.json index 2430112b5528..236f965aacf9 100644 --- a/code/renderers/svelte/package.json +++ b/code/renderers/svelte/package.json @@ -14,7 +14,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/renderers/svelte" }, "funding": { diff --git a/code/renderers/vue3/package.json b/code/renderers/vue3/package.json index 5ec58a100854..cb8f5937477b 100644 --- a/code/renderers/vue3/package.json +++ b/code/renderers/vue3/package.json @@ -15,7 +15,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/renderers/vue3" }, "funding": { diff --git a/code/renderers/web-components/package.json b/code/renderers/web-components/package.json index 5d1b8da3274d..0752b26aad3a 100644 --- a/code/renderers/web-components/package.json +++ b/code/renderers/web-components/package.json @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/storybookjs/storybook.git", + "url": "git+https://github.com/storybookjs/storybook.git", "directory": "code/renderers/web-components" }, "funding": { diff --git a/scripts/eval/README.md b/scripts/eval/README.md index 0666c00ce196..690e8197d825 100644 --- a/scripts/eval/README.md +++ b/scripts/eval/README.md @@ -141,7 +141,7 @@ The script ensures each repo is on its default branch with no local changes, fet node scripts/eval/sync-storybook-version.ts --version 9.1.0 # Upgrade to a canary published from a Storybook PR -node scripts/eval/sync-storybook-version.ts --version 0.0.0-pr-34297-sha-abcdef12 +node scripts/eval/sync-storybook-version.ts --version https://pkg.pr.new/storybookjs/storybook/storybook@abcdef1234567890 # Upgrade a subset of projects node scripts/eval/sync-storybook-version.ts --version latest --project mealdrop --project edgy diff --git a/scripts/eval/sync-storybook-version.ts b/scripts/eval/sync-storybook-version.ts index c4317fb6ccfe..bbde9c429a6c 100644 --- a/scripts/eval/sync-storybook-version.ts +++ b/scripts/eval/sync-storybook-version.ts @@ -28,7 +28,7 @@ type RunUpgrade = (args: HookArgs & { version: string }) => Promise; type RunInstall = (args: HookArgs) => Promise; export interface SyncStorybookVersionOptions { - /** Storybook version to upgrade to (e.g. `latest`, `9.1.0`, `0.0.0-pr-1-sha-abc`). */ + /** Storybook version to upgrade to (e.g. `latest`, `9.1.0`, `https://pkg.pr.new/storybookjs/storybook/storybook@`). */ version: string; /** Per-project clones live under `reposRoot/`. Defaults to `REPOS_DIR`. */ reposRoot?: string; @@ -53,7 +53,8 @@ const cliOptions = { version: { type: 'string' as const, short: 'V', - description: 'Storybook version to upgrade to (e.g. latest, 9.1.0, 0.0.0-pr-1-sha-abc)', + description: + 'Storybook version to upgrade to (e.g. latest, 9.1.0, https://pkg.pr.new/storybookjs/storybook/storybook@)', }, project: { type: 'string' as const, diff --git a/scripts/package.json b/scripts/package.json index ef90cf9c1a72..50d76221b42c 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -133,6 +133,7 @@ "p-retry": "^7.1.0", "pathe": "^1.1.2", "picocolors": "^1.1.0", + "pkg-pr-new": "^0.0.72", "playwright": "1.58.2", "playwright-core": "1.58.2", "polka": "^1.0.0-next.28", diff --git a/yarn.lock b/yarn.lock index 65b2fed75c9b..2131b84a3515 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8501,6 +8501,7 @@ __metadata: leven: "npm:^4.0.0" p-limit: "npm:^7.2.0" picocolors: "npm:^1.1.0" + process-ancestry: "npm:^0.0.2" semver: "npm:^7.7.3" slash: "npm:^5.0.0" storybook: "workspace:*" @@ -9201,6 +9202,7 @@ __metadata: p-retry: "npm:^7.1.0" pathe: "npm:^1.1.2" picocolors: "npm:^1.1.0" + pkg-pr-new: "npm:^0.0.72" playwright: "npm:1.58.2" playwright-core: "npm:1.58.2" polka: "npm:^1.0.0-next.28" @@ -25899,6 +25901,15 @@ __metadata: languageName: node linkType: hard +"pkg-pr-new@npm:^0.0.72": + version: 0.0.72 + resolution: "pkg-pr-new@npm:0.0.72" + bin: + pkg-pr-new: bin/cli.js + checksum: 10c0/020a2dea1f4d23aeb1cb6d405762c6ae00d796edec233780986c5d39e7de4a1975da37efc74aeb3aa1fb9c5876bddc303bd08ca7e3c7992fdcc2778276b1f574 + languageName: node + linkType: hard + "pkg-up@npm:^2.0.0": version: 2.0.0 resolution: "pkg-up@npm:2.0.0"