From daec622216c33a3d19b975c7ef53320ff3182807 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Fri, 11 Sep 2026 19:01:20 +0100 Subject: [PATCH 1/3] chore: add Prepare Release and Publish Release workflows Replaces the monolithic release workflow with a two-step process: - Prepare Release: bumps version, updates changelog, opens PR, creates GitHub draft release - Publish Release: promotes draft, tags master/main, publishes to distribution, posts Slack Ref: SDK release process team agreement 2026-09-11 --- .github/workflows/prepare-release.yml | 99 +++++++++++++++++++++++++++ .github/workflows/publish-release.yml | 76 ++++++++++++++++++++ .github/workflows/publish.yml | 29 -------- 3 files changed, 175 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/prepare-release.yml create mode 100644 .github/workflows/publish-release.yml delete mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 00000000..dba0b499 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,99 @@ +name: Prepare Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version number (e.g., 2.3.0 or 2.3.0-beta1)' + required: true + type: string + ticket: + description: 'Jira ticket number (e.g., 1234 for SDK-1234)' + required: true + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + prepare-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Validate version format + run: | + if ! [[ "${{ github.event.inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then + echo "::error::Invalid version format. Use semantic versioning (e.g., 2.3.0 or 2.3.0-beta1)" + exit 1 + fi + + - name: Update Changelog + id: update_changelog + run: | + version="${{ github.event.inputs.version }}" + changelog_file="CHANGELOG.md" + + unreleased_content=$(awk '/^## \[Unreleased\]/{p=1;next} /^## \[/{p=0} p' "$changelog_file") + + if [ -z "$unreleased_content" ]; then + echo "No unreleased changes found in $changelog_file" + exit 1 + fi + + temp_file=$(mktemp) + { + head -n 4 "$changelog_file" + echo "## [Unreleased]" + echo "" + echo "## [$version]" + echo "$unreleased_content" | tail -n +2 + echo "" + sed -n '/^## \[[0-9]/,$p' "$changelog_file" + } > "$temp_file" + mv "$temp_file" "$changelog_file" + + echo "notes<> "$GITHUB_OUTPUT" + echo "$unreleased_content" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + - name: Bump package.json version + run: npm --no-git-tag-version --allow-same-version version "${{ github.event.inputs.version }}" + + - name: Create Pull Request + uses: peter-evans/create-pull-request@4e1beaa7521e8b457b572c090b25bd3db56bf1c5 # v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + title: "SDK-${{ github.event.inputs.ticket }}: Prepare for Release ${{ github.event.inputs.version }}" + body: | + # Prepare for Release ${{ github.event.inputs.version }} + + ## SDK Release Checklist + - [ ] CHANGELOG.md updated (Unreleased section moved under the new version) + - [ ] Version bumped in package.json + - [ ] README.md reviewed (if needed) + - [ ] All tests passing + - [ ] Documentation updated (if needed) + branch: "release/SDK-${{ github.event.inputs.ticket }}-${{ github.event.inputs.version }}" + commit-message: "[SDK-${{ github.event.inputs.ticket }}]: Prepare for release ${{ github.event.inputs.version }}" + labels: release + delete-branch: true + + - name: Create Draft GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + version="${{ github.event.inputs.version }}" + notes="${{ steps.update_changelog.outputs.notes }}" + + if gh release view "$version" &>/dev/null; then + echo "Draft release $version already exists, skipping." + else + gh release create "$version" \ + --draft \ + --title "$version" \ + --notes "${notes:-See CHANGELOG.md for release notes.}" + fi diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 00000000..dd76d5d7 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,76 @@ +name: Publish Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 2.3.0)' + required: true + type: string + +env: + VERSION: ${{ github.event.inputs.version }} + +permissions: + contents: write + id-token: write + +jobs: + publish-release: + runs-on: ubuntu-latest + environment: npm + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Verify release is ready + run: | + if ! grep -qF "## [$VERSION]" CHANGELOG.md; then + echo "::error::CHANGELOG.md has no entry for [$VERSION]. Merge the prepare-release PR to main before running this workflow." + exit 1 + fi + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22' + cache: 'yarn' + registry-url: 'https://registry.npmjs.org' + + - name: Update npm for OIDC support + run: npm install -g npm@latest + + - run: yarn install --frozen-lockfile + + - run: yarn test + + - run: yarn build + + - run: yarn build:node + + - name: Publish to npm + run: npm publish --provenance + + - name: Publish draft GitHub release and tag main + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release edit "$VERSION" \ + --draft=false \ + --target "$(git rev-parse HEAD)" \ + --latest + + - name: Slack notification + run: | + release_url="https://github.com/${{ github.repository }}/releases/tag/$VERSION" + payload=$(jq -n \ + --arg text ":javascript: *Web SDK ${VERSION}* has been released. <${release_url}|View release notes>." \ + '{"text": $text}') + curl -sS -X POST -H 'Content-type: application/json' --data "$payload" "${{ secrets.SLACK_WEBHOOK }}" + + - name: Slack failure notification + if: failure() + run: | + run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}" + payload=$(jq -n \ + --arg text ":alert: Web SDK release ${VERSION} failed (attempt ${{ github.run_attempt }}). Run: ${run_url}" \ + '{"text": $text}') + curl -sS -X POST -H 'Content-type: application/json' --data "$payload" "${{ secrets.SLACK_WEBHOOK }}" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 593aabf2..00000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: publish -on: - release: - types: - - published - -jobs: - publish: - environment: npm - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Use Node.js 22 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: '22' - cache: 'yarn' - registry-url: 'https://registry.npmjs.org' - - name: Update npm for OIDC support - run: npm install -g npm@latest - - run: yarn install --frozen-lockfile - - run: yarn test - - run: yarn build - - run: yarn build:node - - name: Publish to npm - run: npm publish --provenance From fcc113f95d6890b1a4b1e594203c7a23db70d9c2 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Wed, 16 Sep 2026 16:34:21 +0100 Subject: [PATCH 2/3] fix: pin checkout to default branch, use notes-file, anchor CHANGELOG grep - Add ref: master/main to actions/checkout in all prepare/publish workflows so workflows always operate on the default branch regardless of dispatch ref - Replace shell-injection-prone ${{ steps...outputs.notes }} pattern with --notes-file using $RUNNER_TEMP/release-notes.md (safe from backticks/quotes in changelog content) - Treat empty [Unreleased] section as a hard error in prepare-release - Fix CHANGELOG verification grep: grep -qE "^## \[VERSION\]" (anchored, prevents substring matches and prefix collisions like 3.1.0 vs 3.1.0-rc1) --- .github/workflows/prepare-release.yml | 12 ++++++------ .github/workflows/publish-release.yml | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index dba0b499..21fd78f0 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -22,6 +22,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + ref: main token: ${{ secrets.GITHUB_TOKEN }} - name: Validate version format @@ -40,10 +41,13 @@ jobs: unreleased_content=$(awk '/^## \[Unreleased\]/{p=1;next} /^## \[/{p=0} p' "$changelog_file") if [ -z "$unreleased_content" ]; then - echo "No unreleased changes found in $changelog_file" + echo "::error::No unreleased changes found in $changelog_file. Add release notes to the [Unreleased] section before running this workflow." exit 1 fi + notes_file="$RUNNER_TEMP/release-notes.md" + printf '%s' "$unreleased_content" > "$notes_file" + temp_file=$(mktemp) { head -n 4 "$changelog_file" @@ -56,10 +60,6 @@ jobs: } > "$temp_file" mv "$temp_file" "$changelog_file" - echo "notes<> "$GITHUB_OUTPUT" - echo "$unreleased_content" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - name: Bump package.json version run: npm --no-git-tag-version --allow-same-version version "${{ github.event.inputs.version }}" @@ -95,5 +95,5 @@ jobs: gh release create "$version" \ --draft \ --title "$version" \ - --notes "${notes:-See CHANGELOG.md for release notes.}" + --notes-file "$RUNNER_TEMP/release-notes.md" fi diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index dd76d5d7..b4541bc6 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -21,10 +21,12 @@ jobs: environment: npm steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: main - name: Verify release is ready run: | - if ! grep -qF "## [$VERSION]" CHANGELOG.md; then + if ! grep -qE "^## \[$VERSION\]" CHANGELOG.md; then echo "::error::CHANGELOG.md has no entry for [$VERSION]. Merge the prepare-release PR to main before running this workflow." exit 1 fi From 553d5cfb72dbef98a84910df698a59ccbc9623ab Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Thu, 17 Sep 2026 15:53:38 +0100 Subject: [PATCH 3/3] chore: use iterable-sdk-release GitHub App for all release auth Replace SDK_RELEASE_TOKEN (iOS) and GITHUB_TOKEN (all repos) with a short-lived installation token from the iterable-sdk-release GitHub App, generated via actions/create-github-app-token@v1. Benefits: - App token triggers CI on PRs it creates (GITHUB_TOKEN cannot) - 1h TTL vs long-lived PAT - Workflow-scoped permissions so we can push .github/workflows/ files Required credentials (repo variable + secret, or set at org level): vars.ITERABLE_SDK_RELEASE_APP_ID secrets.ITERABLE_SDK_RELEASE_APP_PRIVATE_KEY --- .github/workflows/prepare-release.yml | 12 +++++++++--- .github/workflows/publish-release.yml | 9 ++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 21fd78f0..792cac35 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -20,10 +20,16 @@ jobs: prepare-release: runs-on: ubuntu-latest steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.ITERABLE_SDK_RELEASE_APP_ID }} + private-key: ${{ secrets.ITERABLE_SDK_RELEASE_APP_PRIVATE_KEY }} + - uses: actions/checkout@v4 with: ref: main - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.app-token.outputs.token }} - name: Validate version format run: | @@ -66,7 +72,7 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@4e1beaa7521e8b457b572c090b25bd3db56bf1c5 # v5 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.app-token.outputs.token }} title: "SDK-${{ github.event.inputs.ticket }}: Prepare for Release ${{ github.event.inputs.version }}" body: | # Prepare for Release ${{ github.event.inputs.version }} @@ -84,7 +90,7 @@ jobs: - name: Create Draft GitHub Release env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | version="${{ github.event.inputs.version }}" notes="${{ steps.update_changelog.outputs.notes }}" diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index b4541bc6..5c710e31 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -20,9 +20,16 @@ jobs: runs-on: ubuntu-latest environment: npm steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.ITERABLE_SDK_RELEASE_APP_ID }} + private-key: ${{ secrets.ITERABLE_SDK_RELEASE_APP_PRIVATE_KEY }} + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: ref: main + token: ${{ steps.app-token.outputs.token }} - name: Verify release is ready run: | @@ -53,7 +60,7 @@ jobs: - name: Publish draft GitHub release and tag main env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | gh release edit "$VERSION" \ --draft=false \