diff --git a/.github/actions/setup-node-and-install/action.yml b/.github/actions/setup-node-and-install/action.yml new file mode 100644 index 000000000000..c70c0c16e2dc --- /dev/null +++ b/.github/actions/setup-node-and-install/action.yml @@ -0,0 +1,42 @@ +name: 'Setup Node.js and Install Dependencies' +description: 'Sets up Node.js, caches dependencies, and installs packages for Storybook' + +inputs: + install-code-deps: + description: 'Whether to install code dependencies in addition to script dependencies' + required: false + default: 'false' + +runs: + using: 'composite' + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - name: Update npm to latest + shell: bash + run: npm install -g npm@latest + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: | + ~/.yarn/berry/cache + key: yarn-v1-${{ hashFiles('scripts/yarn.lock') }}-${{ hashFiles('code/yarn.lock') }} + restore-keys: | + yarn-v1-${{ hashFiles('scripts/yarn.lock') }}-${{ hashFiles('code/yarn.lock') }} + yarn-v1-${{ hashFiles('scripts/yarn.lock') }} + yarn-v1 + + - name: Install script dependencies + shell: bash + working-directory: scripts + run: yarn install + + - name: Install code dependencies + if: inputs.install-code-deps == 'true' + shell: bash + working-directory: code + run: yarn install diff --git a/.github/workflows/canary-release-pr.yml b/.github/workflows/canary-release-pr.yml deleted file mode 100644 index 3e93ef40df5b..000000000000 --- a/.github/workflows/canary-release-pr.yml +++ /dev/null @@ -1,130 +0,0 @@ -name: Publish canary release of PR -run-name: "Canary release: PR #${{ github.event_name == 'workflow_dispatch' && inputs.pr || github.event.pull_request.number }}, triggered by ${{ github.triggering_actor }}" - -on: - workflow_dispatch: - inputs: - pr: - description: 'Pull request number to create a canary release for' - required: true - type: number - pull_request: - types: [opened, synchronize, reopened] - -env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 - -concurrency: - group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && inputs.pr || github.event.pull_request.number }} - cancel-in-progress: true - -permissions: - pull-requests: write - -jobs: - release-canary: - name: Release canary version - runs-on: ubuntu-latest - environment: release - if: github.event_name == 'workflow_dispatch' || endsWith(github.head_ref, 'with-canary-release') - steps: - - name: Fail if triggering actor is not administrator - uses: prince-chrismc/check-actor-permissions-action@v2.0.4 - with: - permission: admin - - - name: Get pull request information - id: info - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - run: | - PR_NUMBER=${{ github.event_name == 'workflow_dispatch' && inputs.pr || github.event.pull_request.number }} - PR_INFO=$(gh pr view $PR_NUMBER --repo ${{ github.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@v4 - 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 - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: | - ~/.yarn/berry/cache - key: yarn-v1-${{ hashFiles('scripts/yarn.lock') }}-${{ hashFiles('code/yarn.lock') }} - restore-keys: | - yarn-v1-${{ hashFiles('scripts/yarn.lock') }}-${{ hashFiles('code/yarn.lock') }} - yarn-v1-${{ hashFiles('scripts/yarn.lock') }} - yarn-v1 - - - name: Install dependencies - run: yarn task --task=install --start-from=install - - - name: Set version - id: version - working-directory: scripts - run: | - PR_NUMBER=${{ github.event_name == 'workflow_dispatch' && inputs.pr || github.event.pull_request.number }} - yarn release:version --exact 0.0.0-pr-$PR_NUMBER-sha-${{ steps.info.outputs.shortSha }} --verbose - - - name: Publish v${{ steps.version.outputs.next-version }} - env: - YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - working-directory: scripts - run: yarn release:publish --tag canary --verbose - - - name: Replace Pull Request Body - # TODO: replace with ivangabriele/find-and-replace-pull-request-body@vX when https://github.com/ivangabriele/find-and-replace-pull-request-body/pull/11 has been released - uses: mcky/find-and-replace-pull-request-body@v1.1.6-mcky - 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/canary-release-pr.yml) or locally with `gh workflow run --repo storybookjs/storybook canary-release-pr.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 }} - run: | - PR_NUMBER=${{ github.event_name == 'workflow_dispatch' && inputs.pr || github.event.pull_request.number }} - gh pr comment $PR_NUMBER\ - --repo "${{github.repository }}"\ - --body "Failed to publish canary version of this pull request, triggered by @${{ github.triggering_actor }}. See the failed workflow run at: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/.github/workflows/generate-sandboxes.yml b/.github/workflows/generate-sandboxes.yml index 425ae0f1fb44..b30b4a7b7f2f 100644 --- a/.github/workflows/generate-sandboxes.yml +++ b/.github/workflows/generate-sandboxes.yml @@ -2,7 +2,7 @@ name: Generate and publish sandboxes on: schedule: - - cron: '2 2 */1 * *' + - cron: "2 2 */1 * *" workflow_dispatch: # To test fixes on push rather than wait for the scheduling, do the following: # 1. Uncomment the lines below and add your branch. @@ -14,8 +14,8 @@ on: # 4. 👉 DON'T FORGET TO UNDO THE STEPS BEFORE YOU MERGE YOUR CHANGES! env: - YARN_ENABLE_IMMUTABLE_INSTALLS: 'false' - CLEANUP_SANDBOX_NODE_MODULES: 'true' + YARN_ENABLE_IMMUTABLE_INSTALLS: "false" + CLEANUP_SANDBOX_NODE_MODULES: "true" defaults: run: @@ -32,7 +32,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" - name: Setup git user run: | @@ -68,7 +68,7 @@ jobs: if: failure() env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_MONITORING_URL }} - uses: Ilshidur/action-discord@master + uses: Ilshidur/action-discord@d2594079a10f1d6739ee50a2471f0ca57418b554 with: args: | The generation of some or all sandboxes on the **next** branch has failed. @@ -84,7 +84,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" - name: Setup git user run: | @@ -120,7 +120,7 @@ jobs: if: failure() env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_MONITORING_URL }} - uses: Ilshidur/action-discord@master + uses: Ilshidur/action-discord@d2594079a10f1d6739ee50a2471f0ca57418b554 with: args: | The generation of some or all sandboxes on the **main** branch has failed. diff --git a/.github/workflows/prepare-non-patch-release.yml b/.github/workflows/prepare-non-patch-release.yml index c495114d461e..7443d0d4df42 100644 --- a/.github/workflows/prepare-non-patch-release.yml +++ b/.github/workflows/prepare-non-patch-release.yml @@ -37,7 +37,7 @@ jobs: prepare-non-patch-pull-request: name: Prepare non-patch pull request runs-on: ubuntu-latest - environment: release + environment: Release defaults: run: working-directory: scripts @@ -51,26 +51,8 @@ jobs: fetch-depth: 10000 token: ${{ secrets.GH_TOKEN }} - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: ".nvmrc" - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: | - ~/.yarn/berry/cache - key: yarn-v1-${{ hashFiles('scripts/yarn.lock') }}-${{ hashFiles('code/yarn.lock') }} - restore-keys: | - yarn-v1-${{ hashFiles('scripts/yarn.lock') }}-${{ hashFiles('code/yarn.lock') }} - yarn-v1-${{ hashFiles('scripts/yarn.lock') }} - yarn-v1 - - - name: Install Dependencies - working-directory: . - run: | - yarn task --task=install + - name: Setup Node.js and Install Dependencies + uses: ./.github/actions/setup-node-and-install - name: Check if pull request is frozen if: github.event_name != 'workflow_dispatch' @@ -160,6 +142,6 @@ jobs: if: failure() env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_MONITORING_URL }} - uses: Ilshidur/action-discord@master + uses: Ilshidur/action-discord@d2594079a10f1d6739ee50a2471f0ca57418b554 with: args: "The GitHub Action for preparing the release pull request bumping from v${{ steps.bump-version.outputs.current-version }} to v${{ steps.bump-version.outputs.next-version }} (triggered by ${{ github.triggering_actor }}) failed! See run at: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/.github/workflows/prepare-patch-release.yml b/.github/workflows/prepare-patch-release.yml index f66258c0d836..f8012dcb7e69 100644 --- a/.github/workflows/prepare-patch-release.yml +++ b/.github/workflows/prepare-patch-release.yml @@ -19,7 +19,7 @@ jobs: prepare-patch-pull-request: name: Prepare patch pull request runs-on: ubuntu-latest - environment: release + environment: Release defaults: run: working-directory: scripts @@ -30,26 +30,8 @@ jobs: ref: main token: ${{ secrets.GH_TOKEN }} - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: ".nvmrc" - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: | - ~/.yarn/berry/cache - key: yarn-v1-${{ hashFiles('scripts/yarn.lock') }}-${{ hashFiles('code/yarn.lock') }} - restore-keys: | - yarn-v1-${{ hashFiles('scripts/yarn.lock') }}-${{ hashFiles('code/yarn.lock') }} - yarn-v1-${{ hashFiles('scripts/yarn.lock') }} - yarn-v1 - - - name: Install Dependencies - working-directory: . - run: | - yarn task --task=install + - name: Setup Node.js and Install Dependencies + uses: ./.github/actions/setup-node-and-install - name: Check if pull request is frozen if: github.event_name != 'workflow_dispatch' @@ -183,6 +165,6 @@ jobs: if: failure() env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_MONITORING_URL }} - uses: Ilshidur/action-discord@master + uses: Ilshidur/action-discord@d2594079a10f1d6739ee50a2471f0ca57418b554 with: args: "The GitHub Action for preparing the release pull request bumping from v${{ steps.versions.outputs.current }} to v${{ steps.versions.outputs.next }} (triggered by ${{ github.triggering_actor }}) failed! See run at: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fda55114e1d7..691a9e9c31fe 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,11 +1,22 @@ name: Publish -run-name: Publish new version on ${{ github.ref_name }}, triggered by ${{ github.triggering_actor }} +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) }}" 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 @@ -17,54 +28,35 @@ permissions: pull-requests: write concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }} + # 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: - name: Publish new version + publish-normal: + name: Publish normal version runs-on: ubuntu-latest + if: | + github.event_name == 'push' && + (github.ref_name == 'latest-release' || github.ref_name == 'next-release') && + contains(github.event.head_commit.message, '[skip ci]') != true environment: Release defaults: run: working-directory: scripts steps: - - name: Cancel if [skip ci] - if: contains(github.event.head_commit.message, '[skip ci]') - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # From https://stackoverflow.com/a/75809743 - run: | - gh run cancel ${{ github.run_id }} - gh run watch ${{ github.run_id }} - - name: Checkout ${{ github.ref_name }} uses: actions/checkout@v4 with: fetch-depth: 100 token: ${{ secrets.GH_TOKEN }} - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: ".nvmrc" - - - name: Update npm to latest - run: npm install -g npm@latest - - - name: Cache dependencies - uses: actions/cache@v4 + - name: Setup Node.js and Install Dependencies + uses: ./.github/actions/setup-node-and-install with: - path: | - ~/.yarn/berry/cache - key: yarn-v1-${{ hashFiles('scripts/yarn.lock') }}-${{ hashFiles('code/yarn.lock') }} - restore-keys: | - yarn-v1-${{ hashFiles('scripts/yarn.lock') }}-${{ hashFiles('code/yarn.lock') }} - yarn-v1-${{ hashFiles('scripts/yarn.lock') }} - yarn-v1 - - - name: Install script dependencies - run: | - yarn install + install-code-deps: true - name: Cancel all release preparation runs env: @@ -73,6 +65,8 @@ jobs: - name: Apply deferred version bump and commit working-directory: . + env: + REF_NAME: ${{ github.ref_name }} run: | CURRENT_VERSION=$(cat ./code/package.json | jq '.version') DEFERRED_NEXT_VERSION=$(cat ./code/package.json | jq '.deferredNextVersion') @@ -89,7 +83,7 @@ jobs: git config --global user.email "32066757+storybook-bot@users.noreply.github.com" git add . git commit -m "Bump version from $CURRENT_VERSION to $DEFERRED_NEXT_VERSION [skip ci]" || true - git push origin ${{ github.ref_name }} + git push origin "$REF_NAME" - name: Get current version id: version @@ -97,17 +91,16 @@ jobs: - name: Check if publish is needed id: publish-needed - run: yarn release:is-version-published ${{ steps.version.outputs.current-version }} + env: + CURRENT_VERSION: ${{ steps.version.outputs.current-version }} + run: yarn release:is-version-published "$CURRENT_VERSION" - name: Check release vs prerelease if: steps.publish-needed.outputs.published == 'false' id: is-prerelease - run: yarn release:is-prerelease ${{ steps.version.outputs.current-version }} --verbose - - - name: Install code dependencies - if: steps.publish-needed.outputs.published == 'false' - working-directory: . - run: yarn task --task=install --start-from=install + env: + CURRENT_VERSION: ${{ steps.version.outputs.current-version }} + run: yarn release:is-prerelease "$CURRENT_VERSION" --verbose - name: Publish if: steps.publish-needed.outputs.published == 'false' @@ -120,7 +113,9 @@ jobs: - name: Get changelog for ${{ steps.version.outputs.current-version }} if: steps.publish-needed.outputs.published == 'false' id: changelog - run: yarn release:get-changelog-from-file ${{ steps.version.outputs.current-version }} + env: + CURRENT_VERSION: ${{ steps.version.outputs.current-version }} + run: yarn release:get-changelog-from-file "$CURRENT_VERSION" # tags are needed to get list of patches to label as picked - name: Fetch git tags @@ -138,23 +133,31 @@ jobs: if: steps.publish-needed.outputs.published == 'false' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CURRENT_VERSION: ${{ steps.version.outputs.current-version }} + REPOSITORY: ${{ github.repository }} + REF_NAME: ${{ github.ref_name }} + CHANGELOG: ${{ steps.changelog.outputs.changelog }} + IS_PRERELEASE: ${{ steps.is-prerelease.outputs.prerelease == 'true' && '--prerelease' || '' }} run: | gh release create \ - v${{ steps.version.outputs.current-version }} \ - --repo "${{ github.repository }}" \ - --target ${{ github.ref_name }} \ - --title "v${{ steps.version.outputs.current-version }}" \ - --notes "${{ steps.changelog.outputs.changelog }}" \ - ${{ steps.is-prerelease.outputs.prerelease == 'true' && '--prerelease' || '' }} + "v$CURRENT_VERSION" \ + --repo "$REPOSITORY" \ + --target "$REF_NAME" \ + --title "v$CURRENT_VERSION" \ + --notes "$CHANGELOG" \ + $IS_PRERELEASE - name: Merge ${{ github.ref_name }} into ${{ steps.target.outputs.target }} + env: + REF_NAME: ${{ github.ref_name }} + TARGET_BRANCH: ${{ steps.target.outputs.target }} run: | git config --global user.name "storybook-bot" git config --global user.email "32066757+storybook-bot@users.noreply.github.com" - git fetch origin ${{ steps.target.outputs.target }} - git checkout ${{ steps.target.outputs.target }} - git merge ${{ github.ref_name }} - git push origin ${{ steps.target.outputs.target }} + git fetch origin "$TARGET_BRANCH" + git checkout "$TARGET_BRANCH" + git merge "$REF_NAME" + git push origin "$TARGET_BRANCH" - name: Force push from 'next' to 'latest-release' and 'main' on minor/major releases if: github.ref_name == 'next-release' && steps.is-prerelease.outputs.prerelease == 'false' @@ -167,13 +170,15 @@ jobs: - name: Sync CHANGELOG.md from `main` to `next` if: steps.target.outputs.target == 'main' working-directory: . + env: + CURRENT_VERSION: ${{ steps.version.outputs.current-version }} run: | git fetch origin next git checkout next git pull git checkout origin/main ./CHANGELOG.md git add ./CHANGELOG.md - git commit -m "Update CHANGELOG.md for v${{ steps.version.outputs.current-version }} [skip ci]" || true + git commit -m "Update CHANGELOG.md for v$CURRENT_VERSION [skip ci]" || true git push origin next # Sync the next.json version file to the main branch so it gets deployed to the docs site @@ -181,6 +186,8 @@ jobs: - name: Sync version JSONs from `next-release` to `main` if: github.ref_name == 'next-release' && steps.is-prerelease.outputs.prerelease == 'true' working-directory: . + env: + CURRENT_VERSION: ${{ steps.version.outputs.current-version }} run: | VERSION_FILE="./docs/versions/next.json" git fetch origin main @@ -188,7 +195,7 @@ jobs: git pull git checkout origin/next-release $VERSION_FILE git add $VERSION_FILE - git commit -m "Update $VERSION_FILE for v${{ steps.version.outputs.current-version }}" + git commit -m "Update $VERSION_FILE for v$CURRENT_VERSION" git push origin main - name: Create Sentry release @@ -206,6 +213,106 @@ jobs: if: failure() env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_MONITORING_URL }} - uses: Ilshidur/action-discord@master + uses: Ilshidur/action-discord@d2594079a10f1d6739ee50a2471f0ca57418b554 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.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 + 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@v4 + 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 }} + working-directory: scripts + run: yarn release:publish --tag canary --verbose + + - name: Replace Pull Request Body + uses: ivangabriele/find-and-replace-pull-request-body@042438c6cbfbacf6a4701d6042f59b1f73db2fd8 + 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/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index 0c6a9f61b333..d3b64ec3b796 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -18,22 +18,11 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 2 - - name: Set node version - uses: actions/setup-node@v4 - with: - node-version-file: ".nvmrc" - cache: 'yarn' - cache-dependency-path: | - code/yarn.lock - scripts/yarn.lock - - - name: install scripts - run: | - cd scripts && yarn install - - name: install code - run: | - cd code && yarn install + - name: Setup Node.js and Install Dependencies + uses: ./.github/actions/setup-node-and-install + with: + install-code-deps: true - name: compile run: yarn task --task compile --start-from=compile diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 2a610c976c93..e604d48fb17b 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,11 @@ +## 10.0.0-beta.10 + +- Core: Make `subtype` an optional property on an index input - [#32602](https://github.com/storybookjs/storybook/pull/32602), thanks @JReinhold! +- Core: Various QA fixes - [#32629](https://github.com/storybookjs/storybook/pull/32629), thanks @ghengeveld! +- Fix: Incorrect URLS for the upgrade command - [#32624](https://github.com/storybookjs/storybook/pull/32624), thanks @jonniebigodes! +- Onboarding: Prevent confetti overlay from intercepting pointer events - [#32660](https://github.com/storybookjs/storybook/pull/32660), thanks @ghengeveld! +- Telemetry: Add metadata for react routers - [#32615](https://github.com/storybookjs/storybook/pull/32615), thanks @shilman! + ## 10.0.0-beta.9 - Automigrations: Add automigration for viewport and backgrounds - [#31614](https://github.com/storybookjs/storybook/pull/31614), thanks @valentinpalkovic! diff --git a/CONTRIBUTING/RELEASING.md b/CONTRIBUTING/RELEASING.md index 33a74bfdc224..e99dd2a0b47f 100644 --- a/CONTRIBUTING/RELEASING.md +++ b/CONTRIBUTING/RELEASING.md @@ -442,7 +442,7 @@ Before you start you should make sure that your working tree is clean and the re 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. -To create a canary release, a core team member (or anyone else with administrator privileges) must manually trigger the canary release workflow. +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. **Before creating a canary release from contributors, the core team member must ensure that the code being released is not malicious.** @@ -450,7 +450,7 @@ Creating a canary release can either be done via GitHub's UI or the [CLI](https: ### With GitHub UI -1. Open the workflow UI at https://github.com/storybookjs/storybook/actions/workflows/canary-release-pr.yml +1. Open the workflow UI at https://github.com/storybookjs/storybook/actions/workflows/publish.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 #** @@ -460,7 +460,7 @@ Creating a canary release can either be done via GitHub's UI or the [CLI](https: The following command will trigger a workflow run - replace `` with the actual pull request number: ```bash -gh workflow run --repo storybookjs/storybook canary-release-pr.yml --field pr= +gh workflow run --repo storybookjs/storybook publish.yml --field pr= ``` 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)). diff --git a/code/addons/onboarding/src/Onboarding.tsx b/code/addons/onboarding/src/Onboarding.tsx index ade6e5d18e87..9d208321b76c 100644 --- a/code/addons/onboarding/src/Onboarding.tsx +++ b/code/addons/onboarding/src/Onboarding.tsx @@ -178,6 +178,7 @@ export default function Onboarding({ api }: { api: API }) { setShowConfetti(true); setStep('5:StoryCreated'); setTimeout(() => api.clearNotification('save-story-success')); + setTimeout(() => setShowConfetti(false), 10000); }); }, [api]); diff --git a/code/addons/onboarding/src/components/Confetti/Confetti.tsx b/code/addons/onboarding/src/components/Confetti/Confetti.tsx index 026e792f3407..36972edc9022 100644 --- a/code/addons/onboarding/src/components/Confetti/Confetti.tsx +++ b/code/addons/onboarding/src/components/Confetti/Confetti.tsx @@ -10,6 +10,7 @@ const Wrapper = styled.div({ left: '50%', width: '50%', height: '100%', + pointerEvents: 'none', }); export const Confetti = React.memo(function Confetti({ diff --git a/code/core/src/component-testing/components/Panel.tsx b/code/core/src/component-testing/components/Panel.tsx index 977c37ea3c63..3ac770251dee 100644 --- a/code/core/src/component-testing/components/Panel.tsx +++ b/code/core/src/component-testing/components/Panel.tsx @@ -68,6 +68,8 @@ const playStatusMap: Record< aborted: 'aborted', }; +const terminalStatuses: PlayStatus[] = ['completed', 'errored', 'aborted']; + const storyStatusMap: Record = { [CallStates.DONE]: 'status-value:success', [CallStates.ERROR]: 'status-value:error', @@ -293,7 +295,7 @@ export const Panel = memo<{ refId?: string; storyId: string; storyUrl: string }> } else { set((state) => { const status = - event.newPhase in playStatusMap + event.newPhase in playStatusMap && !terminalStatuses.includes(state.status) ? playStatusMap[event.newPhase as keyof typeof playStatusMap] : state.status; return getPanelState( diff --git a/code/core/src/component-testing/components/StatusBadge.tsx b/code/core/src/component-testing/components/StatusBadge.tsx index 8d94c1eabece..23a3b26d13a1 100644 --- a/code/core/src/component-testing/components/StatusBadge.tsx +++ b/code/core/src/component-testing/components/StatusBadge.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { type Color, styled, typography } from 'storybook/theming'; +import { TooltipNote, WithTooltip } from '../../components'; + export type PlayStatus = 'rendering' | 'playing' | 'completed' | 'errored' | 'aborted'; export interface StatusBadgeProps { @@ -24,6 +26,14 @@ const StatusTextMapping: Record = { aborted: 'Bail', } as const; +const StatusNoteMapping: Record = { + rendering: 'Story is rendering', + playing: 'Interactions are running', + completed: 'Story ran successfully', + errored: 'Story failed to complete', + aborted: 'Interactions aborted due to file changes', +} as const; + const StyledBadge = styled.div(({ theme, status }) => { const backgroundColor = theme.color[StatusColorMapping[status]]; return { @@ -44,9 +54,17 @@ const StyledBadge = styled.div(({ theme, status }) => { export const StatusBadge: React.FC = ({ status }) => { const badgeText = StatusTextMapping[status]; + const badgeNote = StatusNoteMapping[status]; return ( - - {badgeText} - + } + > + + {badgeText} + + ); }; diff --git a/code/core/src/component-testing/components/Subnav.tsx b/code/core/src/component-testing/components/Subnav.tsx index 875d2a4d6eba..51729516fc2d 100644 --- a/code/core/src/component-testing/components/Subnav.tsx +++ b/code/core/src/component-testing/components/Subnav.tsx @@ -35,7 +35,7 @@ const SubnavWrapper = styled.div(({ theme }) => ({ })); const StyledSubnav = styled.nav({ - height: 40, + height: 39, display: 'flex', alignItems: 'center', justifyContent: 'space-between', @@ -102,7 +102,6 @@ const RewindButton = styled(StyledIconButton)({ const JumpToEndButton = styled(StyledButton)({ marginLeft: 9, marginRight: 9, - marginBottom: 1, lineHeight: '12px', }); diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.ts b/code/core/src/core-server/utils/StoryIndexGenerator.ts index 68367af6a3f8..97849f187cc9 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.ts @@ -17,6 +17,7 @@ import type { Path, StoryIndex, StoryIndexEntry, + StoryIndexInput, StorybookConfigRaw, Tag, } from 'storybook/internal/types'; @@ -414,7 +415,9 @@ export class StoryIndexGenerator { invariant(indexer, `No matching indexer found for ${absolutePath}`); - const indexInputs = await indexer.createIndex(absolutePath, { makeTitle: defaultMakeTitle }); + const indexInputs = (await indexer.createIndex(absolutePath, { + makeTitle: defaultMakeTitle, + })) as StoryIndexInput[]; // we don't actually support DocsIndexInputs at runtime, although types say we do const tsconfigPath = find.up('tsconfig.json', { cwd: this.options.workingDir, last: getProjectRoot(), @@ -439,10 +442,11 @@ export class StoryIndexGenerator { const id = input.__id ?? toId(input.metaId ?? title, storyNameFromExport(input.exportName)); const tags = combineTags(...projectTags, ...(input.tags ?? [])); + const subtype = input.subtype ?? 'story'; - return { + const entry: StoryIndexEntryWithExtra & { tags: Tag[] } = { type: 'story', - subtype: input.type === 'story' ? input.subtype : 'story', + subtype, id, extra: { metaId: input.metaId, @@ -453,11 +457,17 @@ export class StoryIndexGenerator { importPath, componentPath, tags, - ...(input.type === 'story' && input.subtype === 'test' - ? { parent: input.parent, parentName: input.parentName } - : {}), - ...(input.exportName ? { exportName: input.exportName } : {}), }; + + if (subtype === 'test') { + entry.parent = input.parent; + entry.parentName = input.parentName; + } + if (input.exportName) { + entry.exportName = input.exportName; + } + + return entry; } ); diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx index 16694d57f876..cfd094f6a704 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx @@ -27,6 +27,20 @@ const BUILT_IN_TAGS = new Set([ 'test-fn', ]); +// Immutable set operations +const add = (set: Set, id: string) => { + const copy = new Set(set); + copy.add(id); + return copy; +}; +const remove = (set: Set, id: string) => { + const copy = new Set(set); + copy.delete(id); + return copy; +}; +const equal = (left: Set, right: Set) => + left.size === right.size && new Set([...left, ...right]).size === left.size; + const Wrapper = styled.div({ position: 'relative', }); @@ -58,17 +72,20 @@ export interface TagsFilterProps { export const TagsFilter = ({ api, indexJson, isDevelopment, tagPresets }: TagsFilterProps) => { const filtersById = useMemo<{ [id: string]: Filter }>(() => { - const userTagsCounts = Object.values(indexJson.entries).reduce((acc, entry) => { - entry.tags?.forEach((tag: Tag) => { - if (!BUILT_IN_TAGS.has(tag)) { - acc.set(tag, (acc.get(tag) || 0) + 1); - } - }); - return acc; - }, new Map()); + const userTagsCounts = Object.values(indexJson.entries).reduce<{ [key: Tag]: number }>( + (acc, entry) => { + entry.tags?.forEach((tag: Tag) => { + if (!BUILT_IN_TAGS.has(tag)) { + acc[tag] = (acc[tag] || 0) + 1; + } + }); + return acc; + }, + {} + ); const userFilters = Object.fromEntries( - userTagsCounts.entries().map(([tag, count]) => { + Object.entries(userTagsCounts).map(([tag, count]) => { const filterFn = (entry: API_PreparedIndexEntry, excluded?: boolean) => excluded ? !entry.tags?.includes(tag) : !!entry.tags?.includes(tag); return [tag, { id: tag, type: 'tag', title: tag, count, filterFn }]; @@ -163,19 +180,18 @@ export const TagsFilter = ({ api, indexJson, isDevelopment, tagPresets }: TagsFi const toggleFilter = useCallback( (id: string, selected: boolean, excluded?: boolean) => { - const set = new Set([id]); if (excluded === true) { - setExcludedFilters(excludedFilters.union(set)); - setIncludedFilters(includedFilters.difference(set)); + setExcludedFilters(add(excludedFilters, id)); + setIncludedFilters(remove(includedFilters, id)); } else if (excluded === false) { - setIncludedFilters(includedFilters.union(set)); - setExcludedFilters(excludedFilters.difference(set)); + setIncludedFilters(add(includedFilters, id)); + setExcludedFilters(remove(excludedFilters, id)); } else if (selected) { - setIncludedFilters(includedFilters.union(set)); - setExcludedFilters(excludedFilters.difference(set)); + setIncludedFilters(add(includedFilters, id)); + setExcludedFilters(remove(excludedFilters, id)); } else { - setIncludedFilters(includedFilters.difference(set)); - setExcludedFilters(excludedFilters.difference(set)); + setIncludedFilters(remove(includedFilters, id)); + setExcludedFilters(remove(excludedFilters, id)); } }, [includedFilters, excludedFilters] @@ -224,8 +240,7 @@ export const TagsFilter = ({ api, indexJson, isDevelopment, tagPresets }: TagsFi resetFilters={resetFilters} isDevelopment={isDevelopment} isDefaultSelection={ - includedFilters.symmetricDifference(defaultIncluded).size === 0 && - excludedFilters.symmetricDifference(defaultExcluded).size === 0 + equal(includedFilters, defaultIncluded) && equal(excludedFilters, defaultExcluded) } hasDefaultSelection={defaultIncluded.size > 0 || defaultExcluded.size > 0} /> diff --git a/code/core/src/manager/components/sidebar/useExpanded.ts b/code/core/src/manager/components/sidebar/useExpanded.ts index 878cc5f5f741..9ed62872df0b 100644 --- a/code/core/src/manager/components/sidebar/useExpanded.ts +++ b/code/core/src/manager/components/sidebar/useExpanded.ts @@ -41,18 +41,24 @@ const initializeExpanded = ({ initialExpanded, highlightedRef, rootIds, + selectedStoryId, }: { refId: string; data: StoriesHash; initialExpanded?: ExpandedState; highlightedRef: MutableRefObject; rootIds: string[]; + selectedStoryId: string | null; }) => { - const highlightedAncestors = - highlightedRef.current?.refId === refId - ? getAncestorIds(data, highlightedRef.current?.itemId) - : []; - return [...rootIds, ...highlightedAncestors].reduce( + const selectedStory = selectedStoryId && data[selectedStoryId]; + const candidates = [...rootIds]; + if (highlightedRef.current?.refId === refId) { + candidates.push(...getAncestorIds(data, highlightedRef.current?.itemId)); + } + if (selectedStory && 'children' in selectedStory && selectedStory.children?.length) { + candidates.push(selectedStoryId); + } + return candidates.reduce( // @ts-expect-error (non strict) (acc, id) => Object.assign(acc, { [id]: id in initialExpanded ? initialExpanded[id] : true }), {} @@ -90,7 +96,7 @@ export const useExpanded = ({ (state, { ids, value }) => ids.reduce((acc, id) => Object.assign(acc, { [id]: value }), { ...state }), // @ts-expect-error (non strict) - { refId, data, highlightedRef, rootIds, initialExpanded }, + { refId, data, highlightedRef, rootIds, initialExpanded, selectedStoryId }, initializeExpanded ); diff --git a/code/core/src/telemetry/storybook-metadata.ts b/code/core/src/telemetry/storybook-metadata.ts index 2aafcbe5bac7..5c301d80fef2 100644 --- a/code/core/src/telemetry/storybook-metadata.ts +++ b/code/core/src/telemetry/storybook-metadata.ts @@ -34,6 +34,9 @@ export const metaFrameworks = { '@nrwl/storybook': 'nx', '@vue/cli-service': 'vue-cli', '@sveltejs/kit': 'sveltekit', + '@tanstack/react-router': 'tanstack-react', + '@react-router/dev': 'react-router', + '@remix-run/dev': 'remix', } as Record; export const sanitizeAddonName = (name: string) => { diff --git a/code/core/src/types/modules/indexer.ts b/code/core/src/types/modules/indexer.ts index ff43c215d34e..428ee186cd1b 100644 --- a/code/core/src/types/modules/indexer.ts +++ b/code/core/src/types/modules/indexer.ts @@ -74,6 +74,8 @@ export interface BaseIndexEntry { export type StoryIndexEntry = BaseIndexEntry & { type: 'story'; subtype: 'story' | 'test'; + componentPath?: string; + exportName?: string; parent?: StoryId; // exists only on tests parentName?: StoryName; // exists only on tests }; @@ -134,7 +136,7 @@ export type BaseIndexInput = { /** The input for indexing a story entry. */ export type StoryIndexInput = BaseIndexInput & { type: 'story'; - subtype: 'story' | 'test'; + subtype?: 'story' | 'test'; parent?: StoryId; // exists only on tests parentName?: StoryName; // exists only on tests }; diff --git a/code/e2e-tests/component-tests.spec.ts b/code/e2e-tests/component-tests.spec.ts index 3059bda9d5a7..0af1a3f08b09 100644 --- a/code/e2e-tests/component-tests.spec.ts +++ b/code/e2e-tests/component-tests.spec.ts @@ -69,7 +69,7 @@ test.describe('interactions', () => { await expect(interactionsTab).toBeVisible(); const panel = sbPage.panelContent(); - const runStatusBadge = panel.locator('[aria-label="Status of the test run"]'); + const runStatusBadge = panel.locator('[aria-label="Story status"]'); await expect(runStatusBadge).toContainText(/Pass/); await expect(panel).toContainText(/"initial value"/); await expect(panel).toContainText(/clear/); @@ -139,7 +139,7 @@ test.describe('interactions', () => { await expect(button).toContainText('Button', { timeout: 50000 }); const panel = sbPage.panelContent(); - await expect(panel).toContainText(/Pass/); + await expect(panel).toContainText(/Fail/); await expect(panel).toContainText(/Found 1 unhandled error/); await expect(panel).toBeVisible(); }); diff --git a/code/e2e-tests/preview-api.spec.ts b/code/e2e-tests/preview-api.spec.ts index 997f8fc6eff2..3e3b9947e1b5 100644 --- a/code/e2e-tests/preview-api.spec.ts +++ b/code/e2e-tests/preview-api.spec.ts @@ -30,7 +30,7 @@ test.describe('preview-api', () => { const interactionsTab = page.locator('#tabbutton-storybook-interactions-panel'); await expect(interactionsTab).toBeVisible(); const panel = sbPage.panelContent(); - const runStatusBadge = panel.locator('[aria-label="Status of the test run"]'); + const runStatusBadge = panel.locator('[aria-label="Story status"]'); await expect(runStatusBadge).toContainText(/Pass/); // click outside, to remove focus from the input of the story, then press S to toggle sidebar diff --git a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts index ddca7ea5f343..1c4f22ff5bc7 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts @@ -111,7 +111,7 @@ describe('logMigrationSummary', () => { The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. - Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/migration-guide?ref=upgrade?ref=upgrade + Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/releases/migration-guide?ref=upgrade And reach out on Discord if you need help: https://discord.gg/storybook" `); }); @@ -132,7 +132,7 @@ describe('logMigrationSummary', () => { The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. - Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/migration-guide?ref=upgrade?ref=upgrade + Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/releases/migration-guide?ref=upgrade And reach out on Discord if you need help: https://discord.gg/storybook" `); }); @@ -153,7 +153,7 @@ describe('logMigrationSummary', () => { The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. - Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/migration-guide?ref=upgrade?ref=upgrade + Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/releases/migration-guide?ref=upgrade And reach out on Discord if you need help: https://discord.gg/storybook" `); }); diff --git a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts index 4e16e97797a1..527b15db41ad 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts @@ -63,7 +63,7 @@ export function logMigrationSummary({ The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. Please check the changelog and migration guide for manual migrations and more information: ${picocolors.yellow( - 'https://storybook.js.org/docs/migration-guide?ref=upgrade?ref=upgrade' + 'https://storybook.js.org/docs/releases/migration-guide?ref=upgrade' )} And reach out on Discord if you need help: ${picocolors.yellow('https://discord.gg/storybook')} `); diff --git a/code/lib/cli-storybook/src/upgrade.ts b/code/lib/cli-storybook/src/upgrade.ts index 2fc52b9caba5..0209de238178 100644 --- a/code/lib/cli-storybook/src/upgrade.ts +++ b/code/lib/cli-storybook/src/upgrade.ts @@ -263,7 +263,7 @@ function logUpgradeResults( } logger.log( - `For a full list of changes, please check our migration guide: ${CLI_COLORS.cta('https://storybook.js.org/docs/migration-guide?ref=upgrade?ref=upgrade')}` + `For a full list of changes, please check our migration guide: ${CLI_COLORS.cta('https://storybook.js.org/docs/releases/migration-guide?ref=upgrade')}` ); } diff --git a/code/package.json b/code/package.json index 28813603a5f4..69413829c281 100644 --- a/code/package.json +++ b/code/package.json @@ -283,5 +283,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "10.0.0-beta.10" } diff --git a/code/renderers/server/src/preset.ts b/code/renderers/server/src/preset.ts index 36e820d8c987..40173907e335 100644 --- a/code/renderers/server/src/preset.ts +++ b/code/renderers/server/src/preset.ts @@ -31,7 +31,6 @@ export const experimental_indexers: PresetProperty<'experimental_indexers'> = ( title: content.title, tags, type: 'story', - subtype: 'story', }; }); }, diff --git a/docs/_assets/writing-stories/custom-tag-filter.png b/docs/_assets/writing-stories/custom-tag-filter.png deleted file mode 100644 index 3e8705e37c53..000000000000 Binary files a/docs/_assets/writing-stories/custom-tag-filter.png and /dev/null differ diff --git a/docs/_assets/writing-stories/tag-filter.png b/docs/_assets/writing-stories/tag-filter.png new file mode 100644 index 000000000000..f23f0857cf2a Binary files /dev/null and b/docs/_assets/writing-stories/tag-filter.png differ diff --git a/docs/_snippets/main-config-tags-test-fn-exclude.md b/docs/_snippets/main-config-tags-test-fn-exclude.md new file mode 100644 index 000000000000..ca43bf468932 --- /dev/null +++ b/docs/_snippets/main-config-tags-test-fn-exclude.md @@ -0,0 +1,31 @@ +```js filename=".storybook/main.js" renderer="common" language="js" +export default { + // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. + framework: '@storybook/your-framework', + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + tags: { + // 👇 Adjust the default configuration of this tag + 'test-fn': { + defaultFilterSelection: 'exclude', + }, + }, +}; +``` + +```ts filename=".storybook/main.ts" renderer="common" language="ts" +// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. +import type { StorybookConfig } from '@storybook/your-framework'; + +const config: StorybookConfig = { + framework: '@storybook/your-framework', + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + tags: { + // 👇 Adjust the default configuration of this tag + 'test-fn': { + defaultFilterSelection: 'exclude', + }, + }, +}; + +export default config; +``` diff --git a/docs/_snippets/main-config-tags.md b/docs/_snippets/main-config-tags.md new file mode 100644 index 000000000000..40c4ff8c73ba --- /dev/null +++ b/docs/_snippets/main-config-tags.md @@ -0,0 +1,31 @@ +```js filename=".storybook/main.js" renderer="common" language="js" +export default { + // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. + framework: '@storybook/your-framework', + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + tags: { + // 👇 Define a custom tag named "experimental" + experimental: { + defaultFilterSelection: 'exclude', // Or 'include' + }, + }, +}; +``` + +```ts filename=".storybook/main.ts" renderer="common" language="ts" +// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. +import type { StorybookConfig } from '@storybook/your-framework'; + +const config: StorybookConfig = { + framework: '@storybook/your-framework', + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + tags: { + // 👇 Define a custom tag named "experimental" + experimental: { + defaultFilterSelection: 'exclude', // Or 'include' + }, + }, +}; + +export default config; +``` diff --git a/docs/api/main-config/main-config-indexers.mdx b/docs/api/main-config/main-config-indexers.mdx index 06b5fdef8de5..a74399529364 100644 --- a/docs/api/main-config/main-config-indexers.mdx +++ b/docs/api/main-config/main-config-indexers.mdx @@ -93,6 +93,7 @@ Type: exportName: string; importPath: string; type: 'story'; + subtype?: 'story' | 'test'; rawComponentPath?: string; metaId?: string; name?: string; @@ -130,6 +131,16 @@ Type: `'story'` The type of entry. +##### `subtype` + +(⚠️ **Experimental**) + +Type: `'story' | 'test'` + +Default: `'story'` + +The subtype of the story entry when [`type`](#type) is `'story'`. Use this to mark an entry as a [_test_ (experimental)](https://github.com/storybookjs/storybook/discussions/30119). If not specified, defaults to `'story'`. + ##### `rawComponentPath` Type: `string` @@ -487,4 +498,4 @@ Some example usages of custom indexers include: This example's code and live demo are available on [StackBlitz](https://stackblitz.com/~/github.com/Sidnioulz/storybook-sidebar-urls). - \ No newline at end of file + diff --git a/docs/api/main-config/main-config-tags.mdx b/docs/api/main-config/main-config-tags.mdx new file mode 100644 index 000000000000..d0cc7613bb4a --- /dev/null +++ b/docs/api/main-config/main-config-tags.mdx @@ -0,0 +1,28 @@ +--- +title: 'tags' +sidebar: + order: 21 + title: tags +--- + +Parent: [main.js|ts configuration](./main-config.mdx) + +Type: `{ [tagName: string]: { defaultFilterSelection?: 'include' | 'exclude' } }` + +Define custom [tags](../../writing-stories/tags.mdx) for your stories, or alter the default configuration of built-in tags. + + + +## `[tagName]` + +Type: `string` + +The name of the tag. This can be any static (i.e. not created dynamically) string, either a built-in tag or a custom tag of your own design. + +### `[tagName].defaultFilterSelection` + +Type: `'include' | 'exclude'` + +Set the default filter selection state for a tag in the Storybook sidebar. If set to `include`, stories with this tag are selected as included. If set to `exclude`, stories with this tag are selected as excluded, and must be explicitly included by selecting the tag in the sidebar filter menu. If not set, the tag has no default selection. + +![Filtering by tags](../../_assets/writing-stories/tag-filter.png) diff --git a/docs/api/main-config/main-config-typescript.mdx b/docs/api/main-config/main-config-typescript.mdx index e3d1c66afc3f..c23f8e693a77 100644 --- a/docs/api/main-config/main-config-typescript.mdx +++ b/docs/api/main-config/main-config-typescript.mdx @@ -1,7 +1,7 @@ --- title: 'typescript' sidebar: - order: 21 + order: 22 title: typescript --- diff --git a/docs/api/main-config/main-config-vite-final.mdx b/docs/api/main-config/main-config-vite-final.mdx index 4a51a9171c84..c0761375db83 100644 --- a/docs/api/main-config/main-config-vite-final.mdx +++ b/docs/api/main-config/main-config-vite-final.mdx @@ -1,7 +1,7 @@ --- title: 'viteFinal' sidebar: - order: 22 + order: 23 title: viteFinal --- diff --git a/docs/api/main-config/main-config-webpack-final.mdx b/docs/api/main-config/main-config-webpack-final.mdx index 1ae0dfa19eaa..07139fbd8dc9 100644 --- a/docs/api/main-config/main-config-webpack-final.mdx +++ b/docs/api/main-config/main-config-webpack-final.mdx @@ -1,7 +1,7 @@ --- title: 'webpackFinal' sidebar: - order: 23 + order: 24 title: webpackFinal --- diff --git a/docs/api/main-config/main-config.mdx b/docs/api/main-config/main-config.mdx index 5a577c21c111..bdcd243df931 100644 --- a/docs/api/main-config/main-config.mdx +++ b/docs/api/main-config/main-config.mdx @@ -49,6 +49,7 @@ An object to configure Storybook containing the following properties: * [`refs`](./main-config-refs.mdx) * [`staticDirs`](./main-config-static-dirs.mdx) * [`swc`](./main-config-swc.mdx) +* [`tags`](./main-config-tags.mdx) * [`typescript`](./main-config-typescript.mdx) * [`viteFinal`](./main-config-vite-final.mdx) * [`webpackFinal`](./main-config-webpack-final.mdx) diff --git a/docs/versions/next.json b/docs/versions/next.json index e16cebf3fb22..2f8646b7a0f9 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"10.0.0-beta.9","info":{"plain":"- Automigrations: Add automigration for viewport and backgrounds - [#31614](https://github.com/storybookjs/storybook/pull/31614), thanks @valentinpalkovic!\n- Svelte: Simplify public types - use modern `Component` - [#31394](https://github.com/storybookjs/storybook/pull/31394), thanks @xeho91!\n- Telemetry: Log userAgent in onboarding - [#32566](https://github.com/storybookjs/storybook/pull/32566), thanks @shilman!"}} \ No newline at end of file +{"version":"10.0.0-beta.10","info":{"plain":"- Core: Make `subtype` an optional property on an index input - [#32602](https://github.com/storybookjs/storybook/pull/32602), thanks @JReinhold!\n- Core: Various QA fixes - [#32629](https://github.com/storybookjs/storybook/pull/32629), thanks @ghengeveld!\n- Fix: Incorrect URLS for the upgrade command - [#32624](https://github.com/storybookjs/storybook/pull/32624), thanks @jonniebigodes!\n- Onboarding: Prevent confetti overlay from intercepting pointer events - [#32660](https://github.com/storybookjs/storybook/pull/32660), thanks @ghengeveld!\n- Telemetry: Add metadata for react routers - [#32615](https://github.com/storybookjs/storybook/pull/32615), thanks @shilman!"}} \ No newline at end of file diff --git a/docs/writing-stories/tags.mdx b/docs/writing-stories/tags.mdx index 763dcabd1741..b30146f086ac 100644 --- a/docs/writing-stories/tags.mdx +++ b/docs/writing-stories/tags.mdx @@ -11,17 +11,40 @@ Tags allow you to control which stories are included in your Storybook, enabling The following tags are available in every Storybook project: -| Tag | Applied by default? | Description | -| ---------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `autodocs` | No | Stories tagged with `autodocs` are included in the [docs page](../writing-docs/autodocs.mdx). If a CSF file does not contain at least one story tagged with `autodocs`, that component will not generate a docs page. | -| `dev` | Yes | Stories tagged with `dev` are rendered in Storybook's sidebar. | +| Tag | Applied by default? | Description | +| ---------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `dev` | Yes | Stories tagged with `dev` are rendered in Storybook's sidebar. | | `test` | Yes | Stories tagged with `test` are included in [test runner](../writing-tests/integrations/test-runner.mdx#run-tests-for-a-subset-of-stories) or [Vitest addon](../writing-tests/integrations/vitest-addon.mdx#including-excluding-or-skipping-tests) runs. | +| `autodocs` | No | Stories tagged with `autodocs` are included in the [docs page](../writing-docs/autodocs.mdx). If a CSF file does not contain at least one story tagged with `autodocs`, that component will not generate a docs page. | +| `play-fn` | No | Applied automatically to stories with a [play function](./play-function.mdx) defined. | +| `test-fn` | No | Applied automatically to tests defined using the [experimental `.test` method on CSF Factories](https://github.com/storybookjs/storybook/discussions/30119). | The `dev` and `test` tags are automatically, implicitly applied to every story in your Storybook project. +## Custom tags + +You're not limited to the built-in tags. Custom tags enable a flexible layer of categorization on top of Storybook's sidebar hierarchy. Sample uses might include: + +- Status, such as `experimental`, `new`, `stable`, or `deprecated` +- User persona, such as `admin`, `user`, or `developer` +- Component/code ownership + +There are two ways to create a custom tag: + +1. Apply it to a story, component (meta), or project (preview.js|ts) as described below. +2. Define it in your Storybook configuration file (`.storybook/main.js|ts`) to provide more configuration options, like default [filter selection](#filtering-the-sidebar-by-tags). + +For example, to define an "experimental" tag that is excluded by default in the sidebar, you can add this to your Storybook config: + + + +If `defaultFilterSelection` is set to `include`, stories with this tag are selected as included in the filter menu. If set to `exclude`, stories with this tag are selected as excluded, and must be explicitly included by selecting the tag in the sidebar filter menu. If not set, the tag has no default selection. + +You can also use the [`tags` configuration](../api/main-config/main-config-tags.mdx) to alter the configuration of built-in tags. + ## Applying tags -A tag can be any static (i.e. not created dynamically) string, either the [built-in tags](#built-in-tags) or custom tags of your own design. To apply tags to a story, assign an array of strings to the `tags` property. Tags may be applied at the project, component (meta), or story levels. +A tag can be any static (i.e. not created dynamically) string, either the [built-in tags](#built-in-tags) or [custom tags](#custom-tags) of your own design. To apply tags to a story, assign an array of strings to the `tags` property. Tags may be applied at the project, component (meta), or story levels. For example, to apply the `autodocs` tag to all stories in your project, you can use `.storybook/preview.js|ts`: @@ -51,21 +74,19 @@ To remove a tag from a story, prefix it with `!`. For example: Tags can be removed for all stories in your project (in `.storybook/preview.js|ts`), all stories for a component (in the CSF file meta), or a single story (as above). -## Filtering by custom tags +## Filtering the sidebar by tags -Custom tags enable a flexible layer of categorization on top of Storybook's sidebar hierarchy. In the example above, we created an `experimental` tag to indicate that a story is not yet stable. +Both built-in and custom tags are available as filters in Storybook's sidebar. Selecting a tag in the filter causes the sidebar to only show stories with that tag. Selecting multiple tags shows stories that contain any of those tags. -You can create custom tags for any purpose. Sample uses might include: +Pressing the Exclude button for a tag in the filter menu excludes stories with that tag from the sidebar. You can exclude multiple tags, and stories with any of those tags will be excluded. You can also mix inclusion and exclusion. -- Status, such as `experimental`, `new`, `stable`, or `deprecated` -- User persona, such as `admin`, `user`, or `developer` -- Component/code ownership +When no tags are selected, all stories are shown. -Custom tags are useful because they show up as filters in Storybook's sidebar. Selecting a tag in the filter causes the sidebar to only show stories with that tag. Selecting multiple tags shows stories that contain any of those tags. +In this example, the `experimental` tag has been excluded and the Documentation tag (`autodocs`) has been included, so only stories tagged with `autodocs` but not `experimental` are shown. -![Filtering by custom tag](../_assets/writing-stories/custom-tag-filter.png) +![Filtering by tags](../_assets/writing-stories/tag-filter.png) -Filtering by tags is a powerful way to focus on a subset of stories, especially in large Storybook projects. You can also narrow your stories by tag and then search within that subset. +Filtering by tags is a powerful way to focus on a subset of stories, especially in large Storybook projects. When searching, the filter is applied first, so search results are limited to the currently filtered tags. ## Recipes @@ -88,3 +109,11 @@ For a component with many variants, like a Button, a grid of those variants all {/* prettier-ignore-end */} + +### Test cases that don't clutter the sidebar + +(⚠️ **Experimental**: While this API is available for all tags, the built-in `test-fn` tag is experimental) + +If you're using the [experimental `.test` method on CSF Factories](https://github.com/storybookjs/storybook/discussions/30119), you can alter the default behavior of the `test-fn` tag to exclude tests from the sidebar by default. This reduces clutter in the sidebar while still allowing you to run tests for all stories, or adjust the filter to show tests when needed. + +