diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000000..59dd245f7676 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,54 @@ +version: 2.1 + +# https://circleci.com/docs/guides/orchestrate/dynamic-config/ +setup: true + +orbs: + git-shallow-clone: guitarrapc/git-shallow-clone@2.8.0 + continuation: circleci/continuation@2.0.1 + node: circleci/node@7.2.1 + +parameters: + ghBaseBranch: + default: next + description: The name of the base branch (the target of the PR) + type: string + ghPrNumber: + default: '' + description: The PR number + type: string + workflow: + default: skipped + description: Which workflow to run + enum: + - normal + - merged + - daily + - skipped + - docs + type: enum + +jobs: + generate-and-run-config: + executor: + name: node/default + resource_class: small + steps: + - node/install: + install-yarn: true + - git-shallow-clone/checkout_advanced: + clone_options: '--depth 1' + - run: + name: Install dependencies + command: yarn workspaces focus @storybook/scripts + - run: + name: Generate config + command: | + yarn dlx jiti ./scripts/ci/main.ts --workflow=<< pipeline.parameters.workflow >> + - continuation/continue: + configuration_path: .circleci/config.generated.yml +workflows: + setup: + jobs: + - generate-and-run-config + when: pipeline.parameters.workflow != "skipped" diff --git a/.github/workflows/nx.yml b/.github/workflows/nx.yml index 5c4962860b1c..0a8246efae8e 100644 --- a/.github/workflows/nx.yml +++ b/.github/workflows/nx.yml @@ -36,7 +36,50 @@ jobs: runs-on: ubuntu-latest env: - ALL_TASKS: compile,check,knip,test,lint,fmt,sandbox,build,e2e-tests,e2e-tests-dev,test-runner,vitest-integration,check-sandbox,e2e-ui,jest,vitest,playwright-ct + NX_CI_EXECUTION_ENV: 'linux' + ALL_TASKS: compile,check,knip,test,lint,fmt,sandbox,build,e2e-tests,e2e-tests-dev,vitest-integration,e2e-ui,jest,vitest,playwright-ct,chromatic,storybook-chromatic,bench-packages,init-empty,init-features,daily-test-runner + # Benchmark packages upload to BigQuery + GCP_CREDENTIALS: ${{ secrets.GCP_CREDENTIALS }} + GH_BASE_BRANCH: ${{ github.event.pull_request.base.ref || 'next' }} + GH_PR_NUMBER: ${{ github.event.pull_request.number || '0' }} + # Per-sandbox Chromatic tokens (must exist as GitHub repo secrets; + # mirror of the CircleCI CHROMATIC_TOKEN_* project env vars) + CHROMATIC_TOKEN_ANGULAR_CLI_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_ANGULAR_CLI_DEFAULT_TS }} + CHROMATIC_TOKEN_ANGULAR_CLI_PRERELEASE: ${{ secrets.CHROMATIC_TOKEN_ANGULAR_CLI_PRERELEASE }} + CHROMATIC_TOKEN_BENCH_REACT_VITE_DEFAULT_TS_TEST_BUILD: ${{ secrets.CHROMATIC_TOKEN_BENCH_REACT_VITE_DEFAULT_TS_TEST_BUILD }} + CHROMATIC_TOKEN_BENCH_REACT_WEBPACK_18_TS_TEST_BUILD: ${{ secrets.CHROMATIC_TOKEN_BENCH_REACT_WEBPACK_18_TS_TEST_BUILD }} + CHROMATIC_TOKEN_HTML_RSBUILD_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_HTML_RSBUILD_DEFAULT_TS }} + CHROMATIC_TOKEN_HTML_VITE_DEFAULT_JS: ${{ secrets.CHROMATIC_TOKEN_HTML_VITE_DEFAULT_JS }} + CHROMATIC_TOKEN_HTML_VITE_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_HTML_VITE_DEFAULT_TS }} + CHROMATIC_TOKEN_INTERNAL_REACT16_WEBPACK: ${{ secrets.CHROMATIC_TOKEN_INTERNAL_REACT16_WEBPACK }} + CHROMATIC_TOKEN_INTERNAL_REACT18_WEBPACK_BABEL: ${{ secrets.CHROMATIC_TOKEN_INTERNAL_REACT18_WEBPACK_BABEL }} + CHROMATIC_TOKEN_LIT_RSBUILD_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_LIT_RSBUILD_DEFAULT_TS }} + CHROMATIC_TOKEN_LIT_VITE_DEFAULT_JS: ${{ secrets.CHROMATIC_TOKEN_LIT_VITE_DEFAULT_JS }} + CHROMATIC_TOKEN_LIT_VITE_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_LIT_VITE_DEFAULT_TS }} + CHROMATIC_TOKEN_NEXTJS_14_TS: ${{ secrets.CHROMATIC_TOKEN_NEXTJS_14_TS }} + CHROMATIC_TOKEN_NEXTJS_15_TS: ${{ secrets.CHROMATIC_TOKEN_NEXTJS_15_TS }} + CHROMATIC_TOKEN_NEXTJS_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_NEXTJS_DEFAULT_TS }} + CHROMATIC_TOKEN_NEXTJS_PRERELEASE: ${{ secrets.CHROMATIC_TOKEN_NEXTJS_PRERELEASE }} + CHROMATIC_TOKEN_NEXTJS_VITE_14_TS: ${{ secrets.CHROMATIC_TOKEN_NEXTJS_VITE_14_TS }} + CHROMATIC_TOKEN_NEXTJS_VITE_15_TS: ${{ secrets.CHROMATIC_TOKEN_NEXTJS_VITE_15_TS }} + CHROMATIC_TOKEN_NEXTJS_VITE_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_NEXTJS_VITE_DEFAULT_TS }} + CHROMATIC_TOKEN_PREACT_VITE_DEFAULT_JS: ${{ secrets.CHROMATIC_TOKEN_PREACT_VITE_DEFAULT_JS }} + CHROMATIC_TOKEN_PREACT_VITE_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_PREACT_VITE_DEFAULT_TS }} + CHROMATIC_TOKEN_REACT_NATIVE_WEB_VITE_EXPO_TS: ${{ secrets.CHROMATIC_TOKEN_REACT_NATIVE_WEB_VITE_EXPO_TS }} + CHROMATIC_TOKEN_REACT_RSBUILD_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_REACT_RSBUILD_DEFAULT_TS }} + CHROMATIC_TOKEN_REACT_VITE_DEFAULT_JS: ${{ secrets.CHROMATIC_TOKEN_REACT_VITE_DEFAULT_JS }} + CHROMATIC_TOKEN_REACT_VITE_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_REACT_VITE_DEFAULT_TS }} + CHROMATIC_TOKEN_REACT_VITE_PRERELEASE_TS: ${{ secrets.CHROMATIC_TOKEN_REACT_VITE_PRERELEASE_TS }} + CHROMATIC_TOKEN_REACT_WEBPACK_17_TS: ${{ secrets.CHROMATIC_TOKEN_REACT_WEBPACK_17_TS }} + CHROMATIC_TOKEN_REACT_WEBPACK_18_TS: ${{ secrets.CHROMATIC_TOKEN_REACT_WEBPACK_18_TS }} + CHROMATIC_TOKEN_REACT_WEBPACK_PRERELEASE_TS: ${{ secrets.CHROMATIC_TOKEN_REACT_WEBPACK_PRERELEASE_TS }} + CHROMATIC_TOKEN_SOLID_VITE_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_SOLID_VITE_DEFAULT_TS }} + CHROMATIC_TOKEN_SVELTE_KIT_SKELETON_TS: ${{ secrets.CHROMATIC_TOKEN_SVELTE_KIT_SKELETON_TS }} + CHROMATIC_TOKEN_SVELTE_VITE_DEFAULT_JS: ${{ secrets.CHROMATIC_TOKEN_SVELTE_VITE_DEFAULT_JS }} + CHROMATIC_TOKEN_SVELTE_VITE_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_SVELTE_VITE_DEFAULT_TS }} + CHROMATIC_TOKEN_VUE3_RSBUILD_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_VUE3_RSBUILD_DEFAULT_TS }} + CHROMATIC_TOKEN_VUE3_VITE_DEFAULT_JS: ${{ secrets.CHROMATIC_TOKEN_VUE3_VITE_DEFAULT_JS }} + CHROMATIC_TOKEN_VUE3_VITE_DEFAULT_TS: ${{ secrets.CHROMATIC_TOKEN_VUE3_VITE_DEFAULT_TS }} steps: - uses: actions/checkout@v4 with: @@ -72,7 +115,17 @@ jobs: else echo "config=./.nx/workflows/distribution-config.yaml" >> "$GITHUB_OUTPUT" fi - - run: npx nx-cloud@latest start-ci-run --distribute-on="${{ steps.dist.outputs.config }}" --stop-agents-after="$ALL_TASKS" + - name: Build --with-env-vars list + id: envvars + run: | + # Forward Chromatic + benchmark secrets from primary to NX Cloud agents. + # List stays in sync with the env: block above. + vars="GCP_CREDENTIALS,GH_BASE_BRANCH,GH_PR_NUMBER" + for key in $(env | grep -oE '^CHROMATIC_TOKEN_[A-Z0-9_]+'); do + vars="${vars},${key}" + done + echo "vars=$vars" >> "$GITHUB_OUTPUT" + - run: npx nx-cloud@latest start-ci-run --distribute-on="${{ steps.dist.outputs.config }}" --stop-agents-after="$ALL_TASKS" --with-env-vars="${{ steps.envvars.outputs.vars }}" - name: Create Nx Cloud Status (pending) uses: actions/github-script@v7 with: @@ -99,10 +152,20 @@ jobs: - id: nx name: 'Run nx' run: | - echo 'nx_output<> "$GITHUB_OUTPUT" - yarn nx run-many -t $ALL_TASKS -c production -p="tag:library,tag:ci:${{ steps.tag.outputs.tag }}" | tee -a "$GITHUB_OUTPUT" + set -o pipefail + output_file=$(mktemp) + + yarn nx run-many -t $ALL_TASKS -c production -p="tag:library,tag:ci:${{ steps.tag.outputs.tag }}" | tee "$output_file" status=${PIPESTATUS[0]} - echo 'EOF' >> "$GITHUB_OUTPUT" + + # Use a random delimiter so any "EOF" lines inside nx output + # cannot terminate the GITHUB_OUTPUT heredoc prematurely. + delim="NXOUTPUT_$(openssl rand -hex 8)" + { + echo "nx_output<<${delim}" + cat "$output_file" + echo "${delim}" + } >> "$GITHUB_OUTPUT" exit $status - name: Create per-task Nx statuses @@ -161,4 +224,107 @@ jobs: ? `Nx Cloud run failed (${failedCount} tasks failed)` : 'Nx Cloud run finished successfully', context: `nx: ${tag}`, - }); \ No newline at end of file + }); + + nx-windows: + if: > + github.repository == 'storybookjs/storybook' && + ((github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository && + contains(github.event.pull_request.labels.*.name, 'ci:daily') + ) || (github.event_name == 'schedule')) + runs-on: windows-latest + # GH will auto-cancel the job when the timeout expires. Needed because + # NX Cloud manual agents on Windows occasionally fail to stop after the + # primary finishes, which would otherwise leave the job running for + # hours (confirmed w/ James Henry on NX side). + timeout-minutes: 60 + defaults: + run: + shell: bash + env: + NX_CI_EXECUTION_ENV: 'windows' + # Matches CircleCI daily Windows jobs: common build/test + init-empty + per-first-template sandbox build/e2e. + # e2e-tests-dev is intentionally excluded: it depends on the continuous `dev` task which + # crashes with exit code 1 when NX Cloud tries to stop it on Windows agents (known NX + # team-side bug, being tracked). Without it the Windows pipeline reliably terminates; + # with it the primary waits forever for a misbehaving agent. Keep `e2e-tests` (which + # uses `serve` — same continuous pattern but stops cleanly). + # TODO: re-add e2e-tests-dev once NX fixes continuous-task cleanup on Windows. + WINDOWS_TASKS: compile,test,init-empty,sandbox,build,e2e-tests + # NX_BRANCH links the primary and the agents to the same CI pipeline execution + NX_BRANCH: ${{ github.event.pull_request.number || github.ref_name }} + # Verbose logging for the first Windows runs — drop once it's green + NX_CLOUD_VERBOSE_LOGGING: 'true' + # Short tmp paths without username avoid e.g. empty-init storybook init + # running into Windows MAX_PATH limits. Matches lerna's pattern. + TEMP: 'C:\temp' + TMP: 'C:\temp' + # Git Bash path form of TEMP for the `bash -c` commands in the init-empty / + # init-features NX targets (nx.json). On Linux the default of /tmp kicks in. + STORYBOOK_INIT_TMPDIR: '/c/temp' + steps: + - name: Ensure C:\temp exists + run: mkdir -p /c/temp + - uses: actions/checkout@v4 + with: + filter: tree:0 + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'yarn' + - uses: nrwl/nx-set-shas@v4 + - run: npx nx-cloud@latest start-ci-run --distribute-on="manual" --stop-agents-after="$WINDOWS_TASKS" + - run: yarn install --immutable + - name: 'Run nx' + run: yarn nx run-many -t $WINDOWS_TASKS -c production -p="tag:library,tag:ci:windows" + - name: Stop all running agents + if: always() + run: npx nx-cloud stop-all-agents + nx-windows-agents: + name: Nx Cloud - Windows Agent ${{ matrix.agent }} + if: > + github.repository == 'storybookjs/storybook' && + ((github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository && + contains(github.event.pull_request.labels.*.name, 'ci:daily') + ) || (github.event_name == 'schedule')) + runs-on: windows-latest + # See nx-windows job: agents sometimes don't shut down cleanly. + # Timeout lets GH reclaim the runner instead of burning 6h. + timeout-minutes: 60 + defaults: + run: + shell: bash + strategy: + matrix: + agent: [1, 2, 3] + env: + NX_CI_EXECUTION_ENV: 'windows' + # Must match nx-windows primary so NX Cloud pairs them into the same pipeline execution + NX_BRANCH: ${{ github.event.pull_request.number || github.ref_name }} + NX_CLOUD_VERBOSE_LOGGING: 'true' + # Short tmp paths (same as primary) so any cache/install work the + # agent does stays under Windows MAX_PATH. + TEMP: 'C:\temp' + TMP: 'C:\temp' + # See nx-windows primary: this is the Git-Bash form agents use when + # running init-empty / init-features tasks. + STORYBOOK_INIT_TMPDIR: '/c/temp' + steps: + - name: Ensure C:\temp exists + run: mkdir -p /c/temp + - uses: actions/checkout@v4 + with: + filter: tree:0 + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'yarn' + - run: yarn install --immutable + - name: Start Nx Agent ${{ matrix.agent }} + run: npx nx-cloud start-agent + env: + NX_AGENT_NAME: windows-agent-${{ matrix.agent }} \ No newline at end of file diff --git a/.github/workflows/trigger-circle-ci-workflow.yml b/.github/workflows/trigger-circle-ci-workflow.yml new file mode 100644 index 000000000000..7b1cedda19f1 --- /dev/null +++ b/.github/workflows/trigger-circle-ci-workflow.yml @@ -0,0 +1,68 @@ +name: Trigger CircleCI workflow + +on: + # Use pull_request_target, as we don't need to check out the actual code of the fork in this script. + # And this is the only way to trigger the Circle CI API on forks as well. + pull_request_target: + types: [opened, synchronize, labeled, reopened] + push: + branches: + - next + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + get-branch: + if: github.repository_owner == 'storybookjs' + runs-on: ubuntu-latest + steps: + - id: get-branch + env: + # Stored as environment variable to prevent script injection + REF_NAME: ${{ github.ref_name }} + PR_REF_NAME: ${{ github.event.pull_request.head.ref }} + run: | + if [ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then + export BRANCH=pull/${{ github.event.pull_request.number }}/head + elif [ "${{ github.event_name }}" = "push" ]; then + export BRANCH="$REF_NAME" + else + export BRANCH="$PR_REF_NAME" + fi + echo "$BRANCH" + echo "branch=$BRANCH" >> $GITHUB_ENV + outputs: + branch: ${{ env.branch }} + + get-parameters: + if: github.repository_owner == 'storybookjs' + runs-on: ubuntu-latest + steps: + - if: github.event_name == 'pull_request_target' && (contains(github.event.pull_request.labels.*.name, 'ci:normal')) + run: echo "workflow=normal" >> $GITHUB_ENV + - if: github.event_name == 'pull_request_target' && (contains(github.event.pull_request.labels.*.name, 'ci:docs')) + run: echo "workflow=docs" >> $GITHUB_ENV + - if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'ci:merged') + run: echo "workflow=merged" >> $GITHUB_ENV + - if: github.event_name == 'pull_request_target' && (contains(github.event.pull_request.labels.*.name, 'ci:daily')) + run: echo "workflow=daily" >> $GITHUB_ENV + outputs: + workflow: ${{ env.workflow }} + ghBaseBranch: ${{ github.event.pull_request.base.ref }} + ghPrNumber: ${{ github.event.pull_request.number }} + + trigger-circle-ci-workflow: + runs-on: ubuntu-latest + needs: [get-branch, get-parameters] + if: github.repository_owner == 'storybookjs' && needs.get-parameters.outputs.workflow != '' + steps: + - name: Trigger Normal tests + uses: fjogeleit/http-request-action@v1 + with: + url: 'https://circleci.com/api/v2/project/gh/storybookjs/storybook/pipeline' + method: 'POST' + customHeaders: '{"Content-Type": "application/json", "Circle-Token": "${{ secrets.CIRCLE_CI_TOKEN }}"}' + data: '{ "branch": "${{needs.get-branch.outputs.branch}}", "parameters": ${{toJson(needs.get-parameters.outputs)}} }' diff --git a/.nx/workflows/agents.yaml b/.nx/workflows/agents.yaml index 01a4d6303643..8166fcdfcf25 100644 --- a/.nx/workflows/agents.yaml +++ b/.nx/workflows/agents.yaml @@ -73,6 +73,13 @@ common-env-vars: &common-env-vars STORYBOOK_NX_CLOUD_AGENT: true CI: true NODE_OPTIONS: --max_old_space_size=6144 + # Chromatic + nx-set-shas need real commit history — the nrwl checkout + # step defaults to GIT_CHECKOUT_DEPTH=1 (shallow), which makes chromatic + # fail with "Found only one commit" even when the build is perfect. + # Mirror CircleCI's `--depth 500` instead of `0` (all refs) because on PRs + # NX_COMMIT_SHA is GitHub's synthetic merge-ref which is reachable as a + # commit but not on any branch, so "+refs/heads/*" fetch can't find it. + GIT_CHECKOUT_DEPTH: '500' launch-templates: linux-js: diff --git a/.nx/workflows/distribution-config-daily.yaml b/.nx/workflows/distribution-config-daily.yaml index 8e16f6801f06..b9cdbc34d61e 100644 --- a/.nx/workflows/distribution-config-daily.yaml +++ b/.nx/workflows/distribution-config-daily.yaml @@ -22,11 +22,20 @@ assignment-rules: - targets: - check - lint - - pretty-docs - knip + - fmt run-on: - agent: linux-js parallelism: 6 + - targets: + - init-empty + - init-features + - bench-packages + - storybook-chromatic + - storybook-build + run-on: + - agent: linux-js + parallelism: 1 - targets: - '*' run-on: diff --git a/.nx/workflows/distribution-config.yaml b/.nx/workflows/distribution-config.yaml index 61d36251c5cc..1efa172eeded 100644 --- a/.nx/workflows/distribution-config.yaml +++ b/.nx/workflows/distribution-config.yaml @@ -23,9 +23,19 @@ assignment-rules: - check - lint - knip + - fmt run-on: - agent: linux-js parallelism: 6 + - targets: + - init-empty + - init-features + - bench-packages + - storybook-chromatic + - storybook-build + run-on: + - agent: linux-js + parallelism: 1 - targets: - '*' run-on: diff --git a/code/lib/codemod/src/index.ts b/code/lib/codemod/src/index.ts index 1ff9c23e8272..397cb1acc273 100644 --- a/code/lib/codemod/src/index.ts +++ b/code/lib/codemod/src/index.ts @@ -62,7 +62,10 @@ export async function runCodemod( } // Normalize the glob pattern to use forward slashes (required for glob patterns on Windows) - const files = await tinyglobby([normalize(glob), '!**/node_modules', '!**/dist']); + // Use absolute: true to avoid cross-drive relative path issues on Windows (e.g. cwd on D: but tmpdir on C:) + const files = await tinyglobby([normalize(glob), '!**/node_modules', '!**/dist'], { + absolute: true, + }); const extensions = new Set(files.map((file) => extname(file).slice(1))); const commaSeparatedExtensions = Array.from(extensions).join(','); diff --git a/code/project.json b/code/project.json index 41bb54fab1ca..ad65e6905989 100644 --- a/code/project.json +++ b/code/project.json @@ -21,9 +21,9 @@ "test": { "dependsOn": [{ "projects": ["*"], "target": "compile" }], "executor": "nx:run-commands", - "options": { "command": "yarn test" }, + "options": { "command": "bash -c 'yarn test'" }, "cache": true, - "inputs": ["default", "{workspaceRoot}/vitest.config.ts"], + "inputs": ["{workspaceRoot}/code/**/*", "{workspaceRoot}/vitest.config.ts"], "configurations": { "production": {} } }, "knip": { @@ -33,6 +33,28 @@ "cache": true, "inputs": ["default"], "configurations": { "production": {} } + }, + "storybook-build": { + "dependsOn": [{ "projects": ["*"], "target": "compile" }], + "executor": "nx:run-commands", + "options": { + "cwd": "{projectRoot}", + "command": "yarn storybook:ui:build" + }, + "cache": true, + "inputs": ["default", "^production"], + "outputs": ["{projectRoot}/storybook-static"], + "configurations": { "production": {} } + }, + "storybook-chromatic": { + "dependsOn": ["storybook-build"], + "executor": "nx:run-commands", + "options": { + "cwd": "{projectRoot}", + "command": "yarn storybook:ui:chromatic" + }, + "cache": true, + "configurations": { "production": {} } } } } diff --git a/code/sandbox/bench-react-vite-default-ts-nodocs/project.json b/code/sandbox/bench-react-vite-default-ts-nodocs/project.json index 04d9defb20a2..c38901400b04 100644 --- a/code/sandbox/bench-react-vite-default-ts-nodocs/project.json +++ b/code/sandbox/bench-react-vite-default-ts-nodocs/project.json @@ -23,7 +23,6 @@ "dir": "bench-react-vite-default-ts-nodocs" } }, - "chromatic": {}, "serve": {} }, "tags": ["ci:normal", "ci:merged", "ci:daily"] diff --git a/code/sandbox/bench-react-vite-default-ts/project.json b/code/sandbox/bench-react-vite-default-ts/project.json index 6661a3fdbb15..073ad966379c 100644 --- a/code/sandbox/bench-react-vite-default-ts/project.json +++ b/code/sandbox/bench-react-vite-default-ts/project.json @@ -23,7 +23,6 @@ "dir": "bench-react-vite-default-ts" } }, - "chromatic": {}, "serve": {} }, "tags": ["ci:normal", "ci:merged", "ci:daily"] diff --git a/code/sandbox/bench-react-webpack-18-ts/project.json b/code/sandbox/bench-react-webpack-18-ts/project.json index cf7187d76ec8..cb843c55b81f 100644 --- a/code/sandbox/bench-react-webpack-18-ts/project.json +++ b/code/sandbox/bench-react-webpack-18-ts/project.json @@ -23,7 +23,6 @@ "dir": "bench-react-webpack-18-ts" } }, - "chromatic": {}, "serve": {} }, "tags": ["ci:normal", "ci:merged", "ci:daily"] diff --git a/code/sandbox/react-vite-default-ts/project.json b/code/sandbox/react-vite-default-ts/project.json index 151bc6d0d5b9..79ecc2586deb 100644 --- a/code/sandbox/react-vite-default-ts/project.json +++ b/code/sandbox/react-vite-default-ts/project.json @@ -32,5 +32,5 @@ "test-runner": {}, "test-runner-dev": {} }, - "tags": ["ci:normal", "ci:merged", "ci:daily"] + "tags": ["ci:normal", "ci:merged", "ci:daily", "ci:windows"] } diff --git a/nx.json b/nx.json index 1a0dc6abf8ba..8534e92fb283 100644 --- a/nx.json +++ b/nx.json @@ -92,7 +92,13 @@ }, "chromatic": { "dependsOn": ["build", { "projects": ["scripts"], "target": "run-registry" }], - "command": "yarn task chromatic -s task --debug --no-link --template={projectName}", + "executor": "nx:run-commands", + "options": { + "command": "yarn task chromatic -s task --debug --no-link --template={projectName}", + "env": { + "STORYBOOK_SANDBOX_ROOT": "sandbox" + } + }, "cache": true, "inputs": ["^production"], "configurations": { "production": {} } @@ -183,6 +189,35 @@ "inputs": ["default", "^production"], "outputs": ["{workspaceRoot}/test-results"], "configurations": { "production": {} } + }, + "init-empty": { + "dependsOn": [{ "projects": ["scripts"], "target": "run-registry" }], + "executor": "nx:run-commands", + "options": { + "commands": [ + "bash -c 'DIR=\"${STORYBOOK_INIT_TMPDIR:-/tmp}/empty-$STORYBOOK_INIT_EMPTY_TYPE\" && rm -rf \"$DIR\" && mkdir -p \"$DIR\"'", + "bash -c 'DIR=\"${STORYBOOK_INIT_TMPDIR:-/tmp}/empty-$STORYBOOK_INIT_EMPTY_TYPE\" && cd \"$DIR\" && npm set registry http://localhost:6001 && npx storybook init --yes --package-manager npm'", + "bash -c 'DIR=\"${STORYBOOK_INIT_TMPDIR:-/tmp}/empty-$STORYBOOK_INIT_EMPTY_TYPE\" && cd \"$DIR\" && npm run storybook -- --smoke-test'" + ], + "parallel": false + }, + "cache": true, + "configurations": { "production": {} } + }, + "init-features": { + "dependsOn": [{ "projects": ["scripts"], "target": "run-registry" }], + "executor": "nx:run-commands", + "options": { + "commands": [ + "bash -c 'DIR=\"${STORYBOOK_INIT_TMPDIR:-/tmp}/sb-init-features\" && rm -rf \"$DIR\" && mkdir -p \"$DIR\"'", + "bash -c 'DIR=\"${STORYBOOK_INIT_TMPDIR:-/tmp}/sb-init-features\" && cd \"$DIR\" && npm set registry http://localhost:6001'", + "bash -c 'DIR=\"${STORYBOOK_INIT_TMPDIR:-/tmp}/sb-init-features\" && cd \"$DIR\" && npx create-storybook --yes --package-manager npm --features docs test a11y --loglevel=debug'", + "bash -c 'DIR=\"${STORYBOOK_INIT_TMPDIR:-/tmp}/sb-init-features\" && cd \"$DIR\" && npx vitest'" + ], + "parallel": false + }, + "cache": true, + "configurations": { "production": {} } } }, "namedInputs": { diff --git a/scripts/bench/bench-packages.ts b/scripts/bench/bench-packages.ts index 02334c4eadb4..4488165e2045 100644 --- a/scripts/bench/bench-packages.ts +++ b/scripts/bench/bench-packages.ts @@ -2,12 +2,12 @@ import path from 'node:path'; import { BigQuery } from '@google-cloud/bigquery'; import { InvalidArgumentError, program } from 'commander'; -import detectFreePort from 'detect-port'; import { mkdir, readdir, rm, stat, writeFile } from 'fs/promises'; import pLimit from 'p-limit'; import picocolors from 'picocolors'; import { x } from 'tinyexec'; import { dedent } from 'ts-dedent'; +import waitOn from 'wait-on'; import versions from '../../code/core/src/common/versions.ts'; import { maxConcurrentTasks } from '../utils/concurrency.ts'; @@ -446,15 +446,30 @@ const run = async () => { ) as PackageName[]; const options = program.opts<{ pullRequest?: number; baseBranch?: string; upload?: boolean }>(); - if (options.upload === true || typeof options.baseBranch === 'string') { - if (!GCP_CREDENTIALS.project_id) { - throw new Error( - 'GCP_CREDENTIALS env var is required to upload to BigQuery or compare against a base branch' - ); - } + // Gracefully disable upload/compare paths when GCP_CREDENTIALS isn't provisioned + // (e.g., on GitHub Actions forks or workflows where the secret is not yet mirrored + // from CircleCI). Bench still runs locally and writes results.json; it just won't + // upload to BigQuery or post a PR comparison comment. + const hasGcpCredentials = Boolean(GCP_CREDENTIALS.project_id); + if ((options.upload === true || typeof options.baseBranch === 'string') && !hasGcpCredentials) { + console.warn( + 'GCP_CREDENTIALS env var is not set; skipping BigQuery upload and base-branch comparison.' + ); + options.upload = false; + options.baseBranch = undefined; } - if ((await detectFreePort(REGISTRY_PORT)) === REGISTRY_PORT) { + // Wait for the local verdaccio registry (same pattern as scripts/prepare-sandbox.ts + // and scripts/tasks/run-registry.ts). On NX Cloud the registry is started as a + // `continuous` dependency, and on CircleCI/locally it's started by a prior step. + // Either way we race startup, so wait up to 30s before giving up. + try { + await waitOn({ + resources: [`http://localhost:${REGISTRY_PORT}`], + interval: 1000, + timeout: 30_000, + }); + } catch { throw new Error(dedent`The local verdaccio registry must be running in the background for package benching to work, and packages must be published to it in --no-link mode with 'yarn --task publish --no-link' Then run the registry with 'yarn --task run-registry --no-link'`); diff --git a/scripts/ci/init-empty.ts b/scripts/ci/init-empty.ts index 735cde223d51..735b465573db 100644 --- a/scripts/ci/init-empty.ts +++ b/scripts/ci/init-empty.ts @@ -138,13 +138,13 @@ export function defineEmptyInitWindows() { export const initEmptyNoOpJob = defineNoOpJob('init-empty', [build_linux]); -export function getInitEmpty(workflow: Workflow, options: { nxExperiment?: boolean } = {}) { +export function getInitEmpty(workflow: Workflow) { const initEmpty: JobOrNoOpJob[] = ['react-vite-ts'].map(defineEmptyInitFlow); if (isWorkflowOrAbove(workflow, 'merged')) { initEmpty.push(...['nextjs-ts', 'vue-vite-ts', 'lit-vite-ts'].map(defineEmptyInitFlow)); } - if (!options.nxExperiment && isWorkflowOrAbove(workflow, 'daily')) { + if (isWorkflowOrAbove(workflow, 'daily')) { initEmpty.push(defineEmptyInitWindows()); } if (isWorkflowOrAbove(workflow, 'normal')) { diff --git a/scripts/ci/sandboxes.ts b/scripts/ci/sandboxes.ts index dab8d3ba7266..6e6842c95814 100644 --- a/scripts/ci/sandboxes.ts +++ b/scripts/ci/sandboxes.ts @@ -139,8 +139,6 @@ function defineSandboxJob_dev({ ); } -let nxExperiment = false; - export function defineSandboxFlow(key: Key) { const id = toId(key); const data = sandboxTemplates.allTemplates[key as keyof typeof sandboxTemplates.allTemplates]; @@ -354,7 +352,7 @@ export function defineSandboxFlow(key: Key) { createJob, buildJob, devJob, - !nxExperiment && !skipTasks?.includes('chromatic') ? chromaticJob : undefined, + !skipTasks?.includes('chromatic') ? chromaticJob : undefined, !skipTasks?.includes('vitest-integration') ? vitestJob : undefined, !skipTasks?.includes('e2e-tests') ? e2eJob : undefined, @@ -518,13 +516,12 @@ const getListOfSandboxes = (workflow: Workflow) => { } }; -export function getSandboxes(workflow: Workflow, options: { nxExperiment?: boolean } = {}) { - nxExperiment = options.nxExperiment ?? false; +export function getSandboxes(workflow: Workflow) { const sandboxes = getListOfSandboxes(workflow).map(defineSandboxFlow); const list: JobOrNoOpJob[] = sandboxes.flatMap((sandbox) => sandbox.jobs); - if (!nxExperiment && isWorkflowOrAbove(workflow, 'daily')) { + if (isWorkflowOrAbove(workflow, 'daily')) { const windows_sandbox_build = defineWindowsSandboxBuild(sandboxes[0]); const windows_sandbox_dev = defineWindowsSandboxDev(sandboxes[0]); const testRunner = defineSandboxTestRunner(sandboxes[0]); diff --git a/scripts/ci/test-storybooks.ts b/scripts/ci/test-storybooks.ts index 863f89366294..3a0b00fa6e38 100644 --- a/scripts/ci/test-storybooks.ts +++ b/scripts/ci/test-storybooks.ts @@ -70,13 +70,6 @@ export function definePortableStoryTest(directory: string) { artifact.persist(join(working_directory, 'test-results'), 'playwright'), ] : []), - { - run: { - name: 'Run Cypress CT tests', - working_directory, - command: 'yarn cypress', - }, - }, ], }), [testStorybooksNoOpJob] diff --git a/scripts/create-nx-sandbox-projects.ts b/scripts/create-nx-sandbox-projects.ts index 33b9ca7508a2..907284f0bb7f 100644 --- a/scripts/create-nx-sandbox-projects.ts +++ b/scripts/create-nx-sandbox-projects.ts @@ -1,3 +1,4 @@ +import { execSync } from 'node:child_process'; import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; @@ -50,7 +51,11 @@ const projectJson = ( dir: name.replaceAll('/', '-'), }, }, - chromatic: {}, + ...(template.skipTasks && template.skipTasks.includes('chromatic') + ? {} + : { + chromatic: {}, + }), serve: {}, ...(template.skipTasks && template.skipTasks.includes('e2e-tests') ? {} @@ -72,6 +77,14 @@ const projectJson = ( : { 'test-runner-dev': {}, }), + // Note: init-empty / init-features are NOT emitted per-sandbox here. + // They live on dedicated virtual projects in test-storybooks/ci-jobs/* so that a + // single `nx run-many` per tier selects them with the right cadence via tags. + // + // test-runner / test-runner-dev ARE emitted per-sandbox so `yarn nx test-runner + // ` works locally, but they are NOT in ALL_TASKS for CI — the CI daily + // first-template test-runner is driven by test-storybooks/ci-jobs/test-runner-daily + // via its `daily-test-runner` target (different name to avoid clashing). }, tags, }); @@ -91,6 +104,8 @@ await Promise.all( ...(normal.includes(key as any) && !value.inDevelopment ? ['ci:normal'] : []), ...(merged.includes(key as any) && !value.inDevelopment ? ['ci:merged'] : []), ...(daily.includes(key as any) && !value.inDevelopment ? ['ci:daily'] : []), + // Windows CI uses the first sandbox (react-vite/default-ts) + ...(key === 'react-vite/default-ts' ? ['ci:windows'] : []), ]; ensureDirectoryExistence(full); console.log(full); @@ -109,3 +124,13 @@ function ensureDirectoryExistence(filePath: string): void { ensureDirectoryExistence(dir); mkdirSync(dir); } + +// Apply oxfmt so generated files match the repo format rules (e.g. short arrays on one line) +try { + execSync('yarn fmt:write code/sandbox', { + cwd: join(process.cwd(), '..'), + stdio: 'inherit', + }); +} catch (err) { + console.warn('oxfmt pass failed or not available; sandbox JSON may need manual formatting.'); +} diff --git a/scripts/prepare-sandbox.ts b/scripts/prepare-sandbox.ts index 09a3f00ca09c..f7790350c130 100644 --- a/scripts/prepare-sandbox.ts +++ b/scripts/prepare-sandbox.ts @@ -27,7 +27,27 @@ import { isNxTaskExecution } from './utils/nx.ts'; * No-op when not running under NX (i.e. when using `yarn task` directly), * or when the sandbox is already prepared (node_modules exist). */ -export async function prepareSandbox({ key, link }: { key: string; link: boolean }): Promise { +export async function prepareSandbox({ + key, + link, + immutable = true, +}: { + key: string; + link: boolean; + /** + * Whether to pass `--immutable` to yarn install. + * + * Defaults to `true`: downstream tasks like `build`, `serve`, `e2e-tests` + * expect the lockfile the sandbox was created with to be the one yarn + * resolves, so any drift means something's wrong. + * + * Set to `false` for tasks that restore the sandbox across a verdaccio + * republish (e.g. `chromatic`), where the cached yarn.lock references + * the snapshot-versioned @storybook/* packages from the original agent's + * verdaccio and the chromatic agent's verdaccio serves different shas. + */ + immutable?: boolean; +}): Promise { // When running via `yarn task`, the sandbox task already fully initializes // the sandbox (including node_modules), so no preparation is needed. if (!isNxTaskExecution()) { @@ -70,7 +90,11 @@ export async function prepareSandbox({ key, link }: { key: string; link: boolean // Restore node_modules — the NX cache deliberately excludes them to keep // the remote cache small. The yarn cache is shared, so this is fast. - await exec('yarn install --immutable', { cwd: sandboxDir }, { debug: true }); + await exec( + immutable ? 'yarn install --immutable' : 'yarn install', + { cwd: sandboxDir }, + { debug: true } + ); } // SvelteKit requires a sync step to generate types after install diff --git a/scripts/project.json b/scripts/project.json index 8a06c3d7b4f4..dd75baf8f705 100644 --- a/scripts/project.json +++ b/scripts/project.json @@ -19,6 +19,16 @@ "configurations": { "production": {} } }, "run-registry": {}, - "publish": {} + "publish": {}, + "bench-packages": { + "dependsOn": ["run-registry"], + "executor": "nx:run-commands", + "options": { + "cwd": "{projectRoot}", + "command": "yarn bench-packages --base-branch ${GH_BASE_BRANCH:-next} --pull-request ${GH_PR_NUMBER:-0} --upload" + }, + "cache": true, + "configurations": { "production": {} } + } } } diff --git a/scripts/tasks/chromatic.ts b/scripts/tasks/chromatic.ts index b59b6d35a982..96d7dd73955c 100644 --- a/scripts/tasks/chromatic.ts +++ b/scripts/tasks/chromatic.ts @@ -10,7 +10,13 @@ export const chromatic: Task = { return false; }, async run({ key, sandboxDir, builtSandboxDir, junitFilename }, { dryRun, debug, link }) { - await prepareSandbox({ key, link }); + // `immutable: false` because on NX the cached sandbox's yarn.lock + // references whichever snapshot-versioned @storybook/* packages the + // original agent published to verdaccio, but each chromatic agent + // republishes with a fresh sha — `--immutable` would always refuse. + // Matches CircleCI's chromatic job which doesn't reinstall at all + // (workspace persist handles node_modules there). + await prepareSandbox({ key, link, immutable: false }); const tokenEnvVarName = `CHROMATIC_TOKEN_${key.toUpperCase().replace(/\/|-|\./g, '_')}`; const token = process.env[tokenEnvVarName]; diff --git a/test-storybooks/ci-jobs/init-empty-lit-vite-ts/project.json b/test-storybooks/ci-jobs/init-empty-lit-vite-ts/project.json new file mode 100644 index 000000000000..dbdc1b4ab08d --- /dev/null +++ b/test-storybooks/ci-jobs/init-empty-lit-vite-ts/project.json @@ -0,0 +1,11 @@ +{ + "name": "ci-init-empty-lit-vite-ts", + "description": "CI-only virtual project. Runs `yarn task init-empty` for the lit-vite/default-ts template in merged+ and daily+ tiers. Mirrors the CircleCI init-empty-lit-vite-ts job.", + "projectType": "application", + "targets": { + "init-empty": { + "options": { "env": { "STORYBOOK_INIT_EMPTY_TYPE": "lit-vite-ts" } } + } + }, + "tags": ["ci:merged", "ci:daily"] +} diff --git a/test-storybooks/ci-jobs/init-empty-nextjs-ts/project.json b/test-storybooks/ci-jobs/init-empty-nextjs-ts/project.json new file mode 100644 index 000000000000..a1a82dfaa413 --- /dev/null +++ b/test-storybooks/ci-jobs/init-empty-nextjs-ts/project.json @@ -0,0 +1,11 @@ +{ + "name": "ci-init-empty-nextjs-ts", + "description": "CI-only virtual project. Runs `yarn task init-empty` for the nextjs/default-ts template in merged+ and daily+ tiers. Mirrors the CircleCI init-empty-nextjs-ts job.", + "projectType": "application", + "targets": { + "init-empty": { + "options": { "env": { "STORYBOOK_INIT_EMPTY_TYPE": "nextjs-ts" } } + } + }, + "tags": ["ci:merged", "ci:daily"] +} diff --git a/test-storybooks/ci-jobs/init-empty-react-vite-ts/project.json b/test-storybooks/ci-jobs/init-empty-react-vite-ts/project.json new file mode 100644 index 000000000000..67ac2a538071 --- /dev/null +++ b/test-storybooks/ci-jobs/init-empty-react-vite-ts/project.json @@ -0,0 +1,11 @@ +{ + "name": "ci-init-empty-react-vite-ts", + "description": "CI-only virtual project. Runs `yarn task init-empty` for the react-vite/default-ts template in every cadence tier (normal+, merged+, daily+). Mirrors the CircleCI init-empty-react-vite-ts job.", + "projectType": "application", + "targets": { + "init-empty": { + "options": { "env": { "STORYBOOK_INIT_EMPTY_TYPE": "react-vite-ts" } } + } + }, + "tags": ["ci:normal", "ci:merged", "ci:daily", "ci:windows"] +} diff --git a/test-storybooks/ci-jobs/init-empty-vue-vite-ts/project.json b/test-storybooks/ci-jobs/init-empty-vue-vite-ts/project.json new file mode 100644 index 000000000000..a9882aa60432 --- /dev/null +++ b/test-storybooks/ci-jobs/init-empty-vue-vite-ts/project.json @@ -0,0 +1,11 @@ +{ + "name": "ci-init-empty-vue-vite-ts", + "description": "CI-only virtual project. Runs `yarn task init-empty` for the vue3-vite/default-ts template in merged+ and daily+ tiers. Mirrors the CircleCI init-empty-vue-vite-ts job.", + "projectType": "application", + "targets": { + "init-empty": { + "options": { "env": { "STORYBOOK_INIT_EMPTY_TYPE": "vue-vite-ts" } } + } + }, + "tags": ["ci:merged", "ci:daily"] +} diff --git a/test-storybooks/ci-jobs/init-features/project.json b/test-storybooks/ci-jobs/init-features/project.json new file mode 100644 index 000000000000..581fd41271b3 --- /dev/null +++ b/test-storybooks/ci-jobs/init-features/project.json @@ -0,0 +1,11 @@ +{ + "name": "ci-init-features", + "description": "CI-only virtual project. Runs `yarn task init-features` (create-storybook --features docs test a11y) in every cadence tier. Mirrors the CircleCI init-features job.", + "projectType": "application", + "targets": { + "init-features": { + "options": { "env": { "STORYBOOK_INIT_EMPTY_TYPE": "react-vite-ts" } } + } + }, + "tags": ["ci:normal", "ci:merged", "ci:daily"] +} diff --git a/test-storybooks/ci-jobs/test-runner-daily/project.json b/test-storybooks/ci-jobs/test-runner-daily/project.json new file mode 100644 index 000000000000..603746f76776 --- /dev/null +++ b/test-storybooks/ci-jobs/test-runner-daily/project.json @@ -0,0 +1,25 @@ +{ + "name": "ci-test-runner-daily", + "description": "CI-only virtual project. Runs `yarn task test-runner` for react-vite/default-ts on daily runs only. Mirrors the CircleCI daily first-template test-runner extra (scripts/ci/sandboxes.ts defineSandboxTestRunner). Uses the distinct target name `daily-test-runner` so it doesn't collide with per-sandbox `test-runner` (which is kept for local `yarn nx test-runner ` use).", + "projectType": "application", + "targets": { + "daily-test-runner": { + "dependsOn": [ + { "projects": ["react-vite/default-ts"], "target": "sandbox" }, + { "projects": ["react-vite/default-ts"], "target": "build" } + ], + "executor": "nx:run-commands", + "options": { + "command": "yarn task test-runner --template react-vite/default-ts --no-link -s test-runner --junit", + "env": { + "STORYBOOK_SANDBOX_ROOT": "sandbox" + } + }, + "cache": true, + "inputs": ["^production"], + "outputs": ["{workspaceRoot}/test-results", "{workspaceRoot}/code/playwright-results"], + "configurations": { "production": {} } + } + }, + "tags": ["ci:daily"] +}