diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index e221acbcc25..35363b7526b 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -36,7 +36,6 @@ runs: key: ${{ runner.os }}-npm-${{ hashFiles('.nvmrc', 'package-lock.json') }} - if: steps.restore-node-modules-cache.outputs.cache-hit != 'true' name: Install NPM dependencies - working-directory: . shell: bash run: NODE_ENV=development npm ci - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 diff --git a/.github/actions/test-package/action.yml b/.github/actions/test-package/action.yml index ae61002a879..9d25a7db1a7 100644 --- a/.github/actions/test-package/action.yml +++ b/.github/actions/test-package/action.yml @@ -11,11 +11,9 @@ runs: using: composite steps: - name: Install Playwright + if: ${{ inputs.package_name == 'scratch-render' }} shell: bash - run: | - if [[ ${{ inputs.package_name }} == "scratch-render" ]]; then - npx playwright install --with-deps chromium - fi + run: npx playwright install --with-deps chromium - name: Test working-directory: ./packages/${{ inputs.package_name }} shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e92871ab04..76a488ec7cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,17 @@ on: pull_request: push: # WARNING: Renovate sometimes automerges without PR, so we MUST build and test renovate/** branches workflow_call: + inputs: + force-full-build: + description: 'Build all packages regardless of path filters' + type: boolean + default: false workflow_dispatch: + inputs: + force-full-build: + description: 'Build all packages regardless of path filters' + type: boolean + default: false concurrency: group: "${{ github.workflow }} @ ${{ github.event.compare || github.head_ref || github.ref }}" @@ -29,22 +39,40 @@ jobs: env: # `env:` values are printed to the log even without using them in `run:` GH_CONTEXT: ${{ toJson(github) }} + SCRATCH_ENV: ${{ vars.SCRATCH_ENV || '' }} run: | cat <' }} + Scratch environment: $SCRATCH_ENV EOF - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4 - id: filter + id: paths-filter + if: ${{ !inputs.force-full-build }} with: filters: ./.github/path-filters.yml + - name: Resolve changed packages + id: filter + run: | + if [ "${{ inputs.force-full-build }}" = "true" ]; then + echo "any-workspace=true" >> "$GITHUB_OUTPUT" + PACKAGES=$(ls -1d packages/*/ | xargs -n1 basename | jq -Rcn '[inputs]') + echo "changes=$PACKAGES" >> "$GITHUB_OUTPUT" + else + echo "any-workspace=${{ steps.paths-filter.outputs.any-workspace }}" >> "$GITHUB_OUTPUT" + PACKAGES=$(echo '${{ steps.paths-filter.outputs.changes }}' | jq -c '[.[] | select(. != "global" and . != "any-workspace")]') + echo "changes=$PACKAGES" >> "$GITHUB_OUTPUT" + fi + - if: ${{ steps.filter.outputs.any-workspace == 'true' }} uses: ./.github/actions/install-dependencies + # IMPORTANT: always build all packages or none - never a subset. + # The publish workflow reuses these artifacts, so partial artifacts would cause publish + # failures for omitted packages. - name: Build packages if: ${{ steps.filter.outputs.any-workspace == 'true' }} run: npm run build @@ -54,6 +82,7 @@ jobs: uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 with: name: build + retention-days: 90 path: | packages/**/build packages/**/dist @@ -62,7 +91,7 @@ jobs: test: runs-on: ubuntu-latest needs: build - if: ${{ needs.build.outputs.any-workspace == 'true' }} + if: ${{ needs.build.outputs.any-workspace == 'true' && needs.build.outputs.packages != '[]' }} permissions: checks: write pull-requests: write @@ -72,9 +101,6 @@ jobs: fail-fast: false matrix: package: ${{ fromJSON(needs.build.outputs.packages) }} - exclude: - - package: global - - package: any-workspace name: Test ${{ matrix.package }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 @@ -157,7 +183,7 @@ jobs: name: Test Results runs-on: ubuntu-latest needs: test - if: ${{ !cancelled() }} + if: ${{ !cancelled() && needs.test.result != 'skipped' }} steps: - run: | case "${{ needs.test.result }}" in @@ -165,13 +191,12 @@ jobs: echo "Tests passed successfully." exit 0 ;; - skipped) - echo "Tests were unnecessary for these changes, so they were skipped." - echo "If this is unexpected, check the path filters." - exit 0 + cancelled) + echo "Tests were cancelled." + exit 1 ;; *) - echo "Tests failed." + echo "Tests failed. Result: ${{ needs.test.result }}" exit 1 ;; esac diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 40f65c56efb..edb4aadc92f 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -3,6 +3,7 @@ on: [pull_request] concurrency: group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.sha }}" + cancel-in-progress: true jobs: commitlint: diff --git a/.github/workflows/ghpages-cleanup.yml b/.github/workflows/ghpages-cleanup.yml index accdafee58f..16b22d5d4f4 100644 --- a/.github/workflows/ghpages-cleanup.yml +++ b/.github/workflows/ghpages-cleanup.yml @@ -5,6 +5,10 @@ on: - cron: 0 0 * * 6 # midnight on Saturdays workflow_dispatch: +concurrency: + group: ghpages-cleanup + cancel-in-progress: false # queue rather than cancel: avoid half-cleaned states + jobs: cleanup: permissions: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ad2cd1662ce..8211c447c8b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,42 +5,118 @@ on: types: [published] jobs: - ci: + find-artifacts: + name: Find CI artifacts + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + outputs: + ci-run-id: ${{ steps.find.outputs.run-id }} + needs-build: ${{ steps.find.outputs.needs-build }} + steps: + - name: Find successful CI run for release target + id: find + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TARGET: ${{ github.event.release.target_commitish }} + shell: bash + run: | + # Resolve branch name or tag to a commit SHA + SHA=$(gh api "repos/$GITHUB_REPOSITORY/commits/$TARGET" --jq '.sha' 2>/dev/null || echo "$TARGET") + echo "Resolved target '$TARGET' to SHA: $SHA" + + # Find the most recent successful CI run for this SHA that has a build artifact + RUN_ID=$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs" \ + -F "head_sha=$SHA" \ + -F "status=success" \ + -F "per_page=10" \ + --jq '.workflow_runs[].id') || { + echo "::error::Failed to query CI workflow runs. If ci.yml was renamed, update the reference in the find-artifacts job in publish.yml." + exit 1 + } + + USABLE_RUN_ID="" + for ID in $RUN_ID; do + HAS_ARTIFACT=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$ID/artifacts" \ + --jq '[.artifacts[] | select(.name == "build" and .expired == false)] | length') + if [ "$HAS_ARTIFACT" -gt 0 ]; then + USABLE_RUN_ID="$ID" + break + fi + done + + if [ -z "$USABLE_RUN_ID" ]; then + echo "No successful CI run with a valid build artifact found for SHA $SHA. A fresh build will be triggered." + echo "run-id=" >> "$GITHUB_OUTPUT" + echo "needs-build=true" >> "$GITHUB_OUTPUT" + else + echo "Found suitable CI run: $USABLE_RUN_ID" + echo "run-id=$USABLE_RUN_ID" >> "$GITHUB_OUTPUT" + echo "needs-build=false" >> "$GITHUB_OUTPUT" + fi + + build: + name: Build (no prior CI run found) + needs: find-artifacts + if: ${{ needs.find-artifacts.outputs.needs-build == 'true' }} uses: ./.github/workflows/ci.yml + with: + force-full-build: true + cd: + name: Publish to npm needs: - - ci + - find-artifacts + - build + # Run if find-artifacts succeeded AND (build succeeded OR build was skipped because artifacts already exist) + if: ${{ !cancelled() && needs.find-artifacts.result == 'success' && (needs.build.result == 'success' || needs.build.result == 'skipped') }} runs-on: ubuntu-latest + permissions: + actions: read # to download artifacts from the CI run + contents: write # to push the version commit and tag + issues: write # to comment on issues when a fix is released + pull-requests: write # to comment on PRs when a fix is released + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - name: Debug info + # https://docs.github.com/en/actions/reference/security/secure-use#use-an-intermediate-environment-variable + env: + # `env:` values are printed to the log even without using them in `run:` + RELEASE_TAG_NAME: ${{ github.event.release.tag_name }} + RELEASE_TARGET_COMMITISH: ${{ github.event.release.target_commitish }} run: | cat <> "$GITHUB_OUTPUT" + echo "NPM_TAG=${npm_tag}" >> "$GITHUB_ENV" + - name: Check NPM tag run: | - if [ -z "${{ steps.npm_tag.outputs.npm_tag }}" ]; then + if [ -z "$NPM_TAG" ]; then echo "Refusing to publish with empty NPM tag." exit 1 fi @@ -63,39 +139,40 @@ jobs: - uses: ./.github/actions/install-dependencies + - name: Download build artifacts (from previous CI run) + if: ${{ needs.build.result == 'skipped' }} + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: build + path: packages + run-id: ${{ needs.find-artifacts.outputs.ci-run-id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download build artifacts (from fallback build in this run) + if: ${{ needs.build.result == 'success' }} + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: build + path: packages + - name: Update the version in the package files shell: bash + env: + GIT_TAG: ${{ github.event.release.tag_name }} run: | - GIT_TAG="${{github.event.release.tag_name}}" NEW_VERSION="${GIT_TAG/v/}" npm version "$NEW_VERSION" --no-git-tag-version git add package* && git commit -m "chore(release): $NEW_VERSION [skip ci]" - # Install dependencies after the version update so that - # the build outputs refer to the newest version of inner packages - - uses: ./.github/actions/install-dependencies - - name: Publish scratch-svg-renderer - run: | - npm run build --workspace @scratch/scratch-svg-renderer - npm publish --access=public --tag="${{steps.npm_tag.outputs.npm_tag}}" --workspace=@scratch/scratch-svg-renderer - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + run: npm publish --access=public --tag="$NPM_TAG" --ignore-scripts --workspace=@scratch/scratch-svg-renderer - name: Publish scratch-render - run: | - npm run build --workspace @scratch/scratch-render - npm publish --access=public --tag="${{steps.npm_tag.outputs.npm_tag}}" --workspace=@scratch/scratch-render - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + run: npm publish --access=public --tag="$NPM_TAG" --ignore-scripts --workspace=@scratch/scratch-render - name: Publish scratch-vm - run: | - npm run build --workspace @scratch/scratch-vm - npm publish --access=public --tag="${{steps.npm_tag.outputs.npm_tag}}" --workspace=@scratch/scratch-vm - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + run: npm publish --access=public --tag="$NPM_TAG" --ignore-scripts --workspace=@scratch/scratch-vm - name: Publish scratch-gui run: | @@ -103,28 +180,17 @@ jobs: jq 'del(.exports["./standalone"])' ./packages/scratch-gui/package.json | npx sponge ./packages/scratch-gui/package.json - npm run build:dist --workspace @scratch/scratch-gui - npm publish --access=public --tag="${{steps.npm_tag.outputs.npm_tag}}" --workspace=@scratch/scratch-gui + npm publish --access=public --tag="$NPM_TAG" --ignore-scripts --workspace=@scratch/scratch-gui mv ./packages/scratch-gui/package-copy.json ./packages/scratch-gui/package.json - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - name: Publish scratch-gui-standalone run: | bash ./scripts/prepare-standalone-gui.sh - - npm --workspace=@scratch/scratch-gui-standalone run clean && npm --workspace=@scratch/scratch-gui-standalone run build:dist-standalone - npm publish --access=public --tag="${{steps.npm_tag.outputs.npm_tag}}" --workspace=@scratch/scratch-gui-standalone - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + npm publish --access=public --tag="$NPM_TAG" --ignore-scripts --workspace=@scratch/scratch-gui-standalone - name: Publish task-herder - run: | - npm run build --workspace @scratch/task-herder - npm publish --access=public --tag="${{steps.npm_tag.outputs.npm_tag}}" --workspace=@scratch/task-herder - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + run: npm publish --access=public --tag="$NPM_TAG" --ignore-scripts --workspace=@scratch/task-herder - name: Publish scratch-media-lib-scripts run: | @@ -135,11 +201,12 @@ jobs: - name: Push to develop shell: bash + env: + TAG_NAME: ${{ github.event.release.tag_name }} run: | git fetch origin develop - TAG_NAME="${{github.event.release.tag_name}}" - LAST_COMMIT_ID="$(git rev-parse $TAG_NAME)" + LAST_COMMIT_ID="$(git rev-parse "$TAG_NAME")" DEVELOP_COMMIT_ID="$(git rev-parse origin/develop)" if [ "$LAST_COMMIT_ID" = "$DEVELOP_COMMIT_ID" ]; then @@ -148,9 +215,19 @@ jobs: echo "Not pushing to develop because the tag we're operating on is behind" fi + - name: Comment on resolved issues and merged PRs + if: ${{ !github.event.release.prerelease }} + uses: apexskier/github-release-commenter@e7813a9625eabd79a875b4bc4046cfcae377ab34 # v1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + comment-template: | + :tada: This is included in release {release_link}. + # See https://stackoverflow.com/a/24849501 - name: Change connected commit on release shell: bash + env: + TAG_NAME: ${{ github.event.release.tag_name }} run: | - git tag -f "${{github.event.release.tag_name}}" HEAD - git push -f origin "refs/tags/${{github.event.release.tag_name}}" + git tag -f "$TAG_NAME" HEAD + git push -f origin "refs/tags/$TAG_NAME" diff --git a/packages/scratch-gui/package.json b/packages/scratch-gui/package.json index e6068627fe2..196037bb1c6 100644 --- a/packages/scratch-gui/package.json +++ b/packages/scratch-gui/package.json @@ -57,6 +57,7 @@ "i18n:push": "tx-push-src scratch-editor interface translations/en.json", "i18n:src": "rimraf ./translations/messages/src && babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/src ./translations/", "prepare": "node scripts/prepare.mjs", + "prepublishOnly": "echo \"Please publish through CI only.\" && exit 1", "prune": "./prune-gh-pages.sh", "start": "webpack serve", "test": "npm run test:lint && npm run test:unit && npm run test:integration", diff --git a/packages/scratch-render/package.json b/packages/scratch-render/package.json index ed657cecdf2..b9469485327 100644 --- a/packages/scratch-render/package.json +++ b/packages/scratch-render/package.json @@ -36,7 +36,7 @@ "docs": "typedoc", "lint": "eslint", "prepublish-watch": "npm run watch", - "prepublish": "npm run build", + "prepublishOnly": "echo \"Please publish through CI only.\" && exit 1", "start": "webpack-dev-server", "tap": "tap test/unit test/integration", "test": "npm run lint && npm run tap", diff --git a/packages/scratch-svg-renderer/package.json b/packages/scratch-svg-renderer/package.json index 49acdd82610..46aec8e9d0f 100644 --- a/packages/scratch-svg-renderer/package.json +++ b/packages/scratch-svg-renderer/package.json @@ -33,6 +33,7 @@ "scripts": { "build": "npm run clean && webpack", "clean": "rimraf dist playground", + "prepublishOnly": "echo \"Please publish through CI only.\" && exit 1", "start": "webpack-dev-server", "test": "npm run test:lint && npm run test:unit", "test:lint": "eslint", diff --git a/packages/scratch-vm/package.json b/packages/scratch-vm/package.json index d14f9a18af4..dc7fc4eaf62 100644 --- a/packages/scratch-vm/package.json +++ b/packages/scratch-vm/package.json @@ -42,7 +42,7 @@ "i18n:push": "tx-push-src scratch-editor extensions translations/core/en.json", "i18n:src": "mkdirp translations/core && format-message extract --out-file translations/core/en.json src/extensions/**/index.js", "lint": "eslint && format-message lint src/**/*.js", - "prepublish": "in-publish && npm run build || not-in-publish", + "prepublishOnly": "echo \"Please publish through CI only.\" && exit 1", "start": "webpack serve", "tap": "tap ./test/{unit,integration}/*.js", "tap:integration": "tap ./test/integration/*.js", diff --git a/packages/task-herder/package.json b/packages/task-herder/package.json index 98c29661efd..63fea26d8f8 100644 --- a/packages/task-herder/package.json +++ b/packages/task-herder/package.json @@ -36,6 +36,7 @@ "dev": "vite", "format": "prettier --write . && eslint --fix", "lint": "eslint && prettier --check .", + "prepublishOnly": "echo \"Please publish through CI only.\" && exit 1", "preview": "vite preview", "test": "npm run lint && vitest run --coverage" },