From 02846d5868bc45e626660c09295e5c0838a41395 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 10 Jun 2026 15:35:30 -0400 Subject: [PATCH 1/7] Build and tag release assets in CI instead of composer-dist-plugin Replaces the pixelfear/composer-dist-plugin install-time dist fetch with a workflow_dispatch release that builds the assets into an off-branch commit and tags it, so the compiled dist ships inside the Packagist zip. Co-Authored-By: Claude Opus 4.8 --- .gitattributes | 12 +++++ .github/workflows/release.yml | 85 ++++++++++++++++++++++++++++------- composer.json | 22 +-------- scripts/build-release.sh | 13 +----- 4 files changed, 85 insertions(+), 47 deletions(-) diff --git a/.gitattributes b/.gitattributes index 4082132ee33..4b6ebdc754e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,17 @@ * text=auto *.php eol=lf + +# Compiled CP assets — shipped in the Composer zip, but marked generated for GitHub tooling +/resources/dist/**/* linguist-generated=true +/resources/dist-dev/**/* linguist-generated=true +/resources/dist-frontend/**/* linguist-generated=true +/resources/dist-package/**/* linguist-generated=true + +# Source JS/CSS/TS — not needed by consumers; export-ignore keeps the Composer zip lean +/resources/js export-ignore +/resources/css export-ignore +/packages export-ignore + /.github export-ignore /tests export-ignore .babelrc export-ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 25dbfc50015..1fa961b8626 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,22 +1,61 @@ name: Create Release on: # zizmor: ignore[concurrency-limits] - push: - tags: - - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g. v6.2.0)' + required: true + type: string + source-ref: + description: 'Branch to build and tag from' + required: true + default: '6.x' + type: choice + options: + - 6.x + - 5.x + - master permissions: {} jobs: - build: # zizmor: ignore[anonymous-definition] + release: # zizmor: ignore[anonymous-definition] runs-on: ubuntu-latest - permissions: - contents: write # create GitHub release and upload assets + environment: release + permissions: {} # all writes go through the App token; GITHUB_TOKEN needs no scopes steps: - - name: Checkout code + - name: Validate version format + env: + VERSION: ${{ inputs.version }} + run: | + if ! printf '%s' "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+'; then + echo "::error::Version must match v#.#.# format (got: $VERSION)" + exit 1 + fi + + - name: Get release bot token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + + - name: Checkout source ref uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - persist-credentials: false + ref: ${{ inputs.source-ref }} + token: ${{ steps.app-token.outputs.token }} + persist-credentials: true # zizmor: ignore[artipacked] App token is needed to push the tag + + - name: Assert tag does not already exist + env: + VERSION: ${{ inputs.version }} + run: | + if git ls-remote --tags origin "$VERSION" | grep -q .; then + echo "::error::Tag $VERSION already exists on remote. Aborting." + exit 1 + fi - name: Use Node.js 20.19.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -30,25 +69,41 @@ jobs: - name: Build release assets run: bash ./scripts/build-release.sh + - name: Create the build commit + env: + VERSION: ${{ inputs.version }} + run: | + git config user.name "statamic-release-bot[bot]" + git config user.email "statamic-release-bot[bot]@users.noreply.github.com" + git add --force \ + resources/dist \ + resources/dist-dev \ + resources/dist-frontend \ + resources/dist-package + git commit -m "Build assets for $VERSION" + + - name: Tag and push the build commit + env: + VERSION: ${{ inputs.version }} + run: | + git tag "$VERSION" + git push origin "$VERSION" + - name: Get Changelog id: changelog uses: statamic/changelog-action@5d112d0d790cdeeb5adca3e584e37edc474ab51b # v1.0.2 with: - version: ${{ github.ref }} + version: ${{ inputs.version }} - name: Create release env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} RELEASE_VERSION: ${{ steps.changelog.outputs.version }} RELEASE_NOTES: ${{ steps.changelog.outputs.text }} run: | gh release create "$RELEASE_VERSION" \ --title "$RELEASE_VERSION" \ - --notes "$RELEASE_NOTES" \ - ./resources/dist.tar.gz \ - ./resources/dist-dev.tar.gz \ - ./resources/dist-frontend.tar.gz \ - ./resources/dist-package.tar.gz + --notes "$RELEASE_NOTES" - name: Deploy Storybook to Forge continue-on-error: true diff --git a/composer.json b/composer.json index b287fe28eb7..910d28b14e1 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,6 @@ "maennchen/zipstream-php": "^3.1", "michelf/php-smartypants": "^1.8.1", "nesbot/carbon": "^3.0", - "pixelfear/composer-dist-plugin": "^0.1.4", "pragmarx/google2fa": "^8.0 || ^9.0", "rebing/graphql-laravel": "^9.15", "rhukster/dom-sanitizer": "^1.0.10", @@ -63,29 +62,10 @@ "preferred-install": "dist", "sort-packages": true, "allow-plugins": { - "composer/package-versions-deprecated": true, - "pixelfear/composer-dist-plugin": true + "composer/package-versions-deprecated": true } }, "extra": { - "download-dist": [ - { - "url": "https://github.com/statamic/cms/releases/download/{$version}/dist.tar.gz", - "path": "resources/dist" - }, - { - "url": "https://github.com/statamic/cms/releases/download/{$version}/dist-dev.tar.gz", - "path": "resources/dist-dev" - }, - { - "url": "https://github.com/statamic/cms/releases/download/{$version}/dist-frontend.tar.gz", - "path": "resources/dist-frontend" - }, - { - "url": "https://github.com/statamic/cms/releases/download/{$version}/dist-package.tar.gz", - "path": "resources/dist-package" - } - ], "laravel": { "providers": [ "Statamic\\Providers\\StatamicServiceProvider" diff --git a/scripts/build-release.sh b/scripts/build-release.sh index dc9f6cacad8..e3af82e2c51 100644 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -10,16 +10,7 @@ npm run build npm run build-dev npm run frontend-build -# Create tarballs for the Laravel package -cd resources -tar -czvf dist.tar.gz dist -tar -czvf dist-dev.tar.gz dist-dev -tar -czvf dist-frontend.tar.gz dist-frontend -cd .. - -# Create a tarball for @statamic/cms +# Populate resources/dist-package from packages/cms cp resources/css/ui.css packages/cms/src/ui.css -cd packages/cms -tar -czvf ../../resources/dist-package.tar.gz * -cd ../.. +rsync -a --delete packages/cms/ resources/dist-package/ From c583b4eb243f03054cd880bad4806b71db3943da Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 10 Jun 2026 15:44:57 -0400 Subject: [PATCH 2/7] Scope the release bot token to contents Narrows the minted App installation token to only the contents permission it needs, instead of inheriting all installation permissions. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fa961b8626..5a58b984e37 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,6 +40,7 @@ jobs: with: app-id: ${{ secrets.RELEASE_APP_ID }} private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write - name: Checkout source ref uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 From 99d8b5e15b4f181db72ab3416b981d25bb419417 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 22 Jun 2026 20:11:43 -0400 Subject: [PATCH 3/7] Align release workflow with validated test-repo flow Bare version input with auto-v tagging, branch/major validation, default-branch latest gating, prerelease detection, runtime bot identity, and Releases environment. Drops source-ref in favour of the dispatched branch. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 78 +++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8d8eae8e46..d9353947db3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,57 +4,56 @@ on: # zizmor: ignore[concurrency-limits] workflow_dispatch: inputs: version: - description: 'Release version (e.g. v6.2.0)' + description: 'Release version, without the v prefix (e.g. 6.2.0)' required: true type: string - source-ref: - description: 'Branch to build and tag from' - required: true - default: '6.x' - type: choice - options: - - 6.x - - 5.x - - master permissions: {} jobs: - release: # zizmor: ignore[anonymous-definition] + release: + name: Create Release runs-on: ubuntu-latest - environment: release + environment: Releases permissions: {} # all writes go through the App token; GITHUB_TOKEN needs no scopes steps: - - name: Validate version format + - name: Resolve and validate version env: - VERSION: ${{ inputs.version }} + INPUT_VERSION: ${{ inputs.version }} + BRANCH: ${{ github.ref_name }} run: | - if ! printf '%s' "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+'; then - echo "::error::Version must match v#.#.# format (got: $VERSION)" + version="${INPUT_VERSION#v}" + + if ! printf '%s' "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then + echo "::error::Version must look like 6.2.0 (got: $INPUT_VERSION)" + exit 1 + fi + + if [ "${version%%.*}" != "${BRANCH%%.*}" ]; then + echo "::error::Version $version does not belong on the $BRANCH branch" exit 1 fi + echo "TAG=v$version" >> "$GITHUB_ENV" + - name: Get release bot token id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.RELEASE_APP_ID }} + client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }} private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} permission-contents: write - - name: Checkout source ref + - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - ref: ${{ inputs.source-ref }} token: ${{ steps.app-token.outputs.token }} persist-credentials: true # zizmor: ignore[artipacked] App token is needed to push the tag - name: Assert tag does not already exist - env: - VERSION: ${{ inputs.version }} run: | - if git ls-remote --tags origin "$VERSION" | grep -q .; then - echo "::error::Tag $VERSION already exists on remote. Aborting." + if git ls-remote --tags origin "$TAG" | grep -q .; then + echo "::error::Tag $TAG already exists on remote. Aborting." exit 1 fi @@ -72,39 +71,46 @@ jobs: - name: Create the build commit env: - VERSION: ${{ inputs.version }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} run: | - git config user.name "statamic-release-bot[bot]" - git config user.email "statamic-release-bot[bot]@users.noreply.github.com" + USER_ID="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" + git config user.name "${APP_SLUG}[bot]" + git config user.email "${USER_ID}+${APP_SLUG}[bot]@users.noreply.github.com" git add --force \ resources/dist \ resources/dist-dev \ resources/dist-frontend \ resources/dist-package - git commit -m "Build assets for $VERSION" + git commit -m "Build assets for $TAG" - name: Tag and push the build commit - env: - VERSION: ${{ inputs.version }} run: | - git tag "$VERSION" - git push origin "$VERSION" + git tag "$TAG" + git push origin "$TAG" - name: Get Changelog id: changelog uses: statamic/changelog-action@5d112d0d790cdeeb5adca3e584e37edc474ab51b # v1.0.2 with: - version: ${{ inputs.version }} + version: ${{ env.TAG }} - name: Create release env: GH_TOKEN: ${{ steps.app-token.outputs.token }} - RELEASE_VERSION: ${{ steps.changelog.outputs.version }} RELEASE_NOTES: ${{ steps.changelog.outputs.text }} + IS_DEFAULT_BRANCH: ${{ github.ref_name == github.event.repository.default_branch }} run: | - gh release create "$RELEASE_VERSION" \ - --title "$RELEASE_VERSION" \ - --notes "$RELEASE_NOTES" + latest="$IS_DEFAULT_BRANCH" + prerelease=false + case "$TAG" in + *-*) prerelease=true; latest=false ;; + esac + gh release create "$TAG" \ + --title "$TAG" \ + --notes "$RELEASE_NOTES" \ + --latest="$latest" \ + --prerelease="$prerelease" - name: Deploy Storybook to Forge continue-on-error: true From 270eb22d825360e2230120e7b5f4a61e9c1b1f60 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 22 Jun 2026 20:18:07 -0400 Subject: [PATCH 4/7] wip --- .gitattributes | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.gitattributes b/.gitattributes index 4b6ebdc754e..974ba461b9d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,18 +1,13 @@ * text=auto *.php eol=lf - -# Compiled CP assets — shipped in the Composer zip, but marked generated for GitHub tooling /resources/dist/**/* linguist-generated=true /resources/dist-dev/**/* linguist-generated=true /resources/dist-frontend/**/* linguist-generated=true /resources/dist-package/**/* linguist-generated=true - -# Source JS/CSS/TS — not needed by consumers; export-ignore keeps the Composer zip lean +/.github export-ignore /resources/js export-ignore /resources/css export-ignore /packages export-ignore - -/.github export-ignore /tests export-ignore .babelrc export-ignore .gitattributes export-ignore From 6dc16ec6db5ec20028cb666464356c3e164313d6 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 22 Jun 2026 20:20:33 -0400 Subject: [PATCH 5/7] wip --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index 974ba461b9d..887616f4e4f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,11 @@ * text=auto *.php eol=lf + /resources/dist/**/* linguist-generated=true /resources/dist-dev/**/* linguist-generated=true /resources/dist-frontend/**/* linguist-generated=true /resources/dist-package/**/* linguist-generated=true + /.github export-ignore /resources/js export-ignore /resources/css export-ignore From ac249fec51ca64ed6065235f4992652b65dc5cb1 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 22 Jun 2026 20:48:42 -0400 Subject: [PATCH 6/7] Anchor release version validation against the whole string Switches to a bash regex match so a trailing newline can't slip extra lines into GITHUB_ENV, while still permitting prerelease suffixes. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d9353947db3..4c996a1bba0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: run: | version="${INPUT_VERSION#v}" - if ! printf '%s' "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then echo "::error::Version must look like 6.2.0 (got: $INPUT_VERSION)" exit 1 fi From fbaf414e737f4ee04df8f16e1d18507cf160e8ad Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Mon, 22 Jun 2026 20:48:52 -0400 Subject: [PATCH 7/7] Fail the tag-existence check on ls-remote errors Captures the ls-remote output first so a transient failure aborts the run instead of being read as "tag absent". Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4c996a1bba0..dd0827e9f0f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,10 +52,8 @@ jobs: - name: Assert tag does not already exist run: | - if git ls-remote --tags origin "$TAG" | grep -q .; then - echo "::error::Tag $TAG already exists on remote. Aborting." - exit 1 - fi + existing="$(git ls-remote --tags origin "$TAG")" + [ -z "$existing" ] || { echo "::error::Tag $TAG already exists on remote. Aborting."; exit 1; } - name: Use Node.js 20.19.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0