From 6f8bc35e7b25b7a78e3831c5e2dda09f1905a783 Mon Sep 17 00:00:00 2001 From: hazre Date: Tue, 10 Mar 2026 00:15:05 +0100 Subject: [PATCH 01/10] ci(knope): ditch knope bot in favor of our own ci workflows --- .github/workflows/prepare-release.yml | 121 ++++++++++++++++++++++++ .github/workflows/release.yml | 37 ++++++++ .github/workflows/require-changeset.yml | 100 ++++++++++++++++++++ knope.toml | 22 +---- 4 files changed, 263 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/prepare-release.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/require-changeset.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 000000000..c17be9391 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,121 @@ +name: Create Release PR + +on: + push: + branches: [main] + +permissions: {} + +jobs: + load-packages: + if: "!contains(github.event.head_commit.message, 'chore: prepare release')" + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.parse.outputs.matrix }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Parse packages from knope.toml + id: parse + shell: python3 {0} + run: | + import tomllib, json, os + + with open("knope.toml", "rb") as f: + config = tomllib.load(f) + + packages = config.get("packages", {}) + matrix = [ + {"package": name, "changelog": pkg["changelog"]} + for name, pkg in packages.items() + ] + + with open(os.environ["GITHUB_OUTPUT"], "a") as out: + out.write(f"matrix={json.dumps(matrix)}\n") + + prepare-release: + needs: load-packages + if: needs.load-packages.outputs.matrix != '[]' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + strategy: + # Run each package independently so one with no changes doesn't block others + fail-fast: false + matrix: + include: ${{ fromJSON(needs.load-packages.outputs.matrix) }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: true + + - uses: fregante/setup-git-user@024bc0b8e177d7e77203b48dab6fb45666854b35 # v2.0.2 + + - uses: knope-dev/action@407e9ef7c272d2dd53a4e71e39a7839e29933c48 # v2.1.0 + with: + version: 0.22.1 + + - name: Switch to release branch + shell: bash + run: git switch -c release/${{ matrix.package }} + + - name: Prepare Release + id: knope + shell: bash + run: | + if knope prepare-release --package ${{ matrix.package }} --verbose; then + echo "released=true" >> "$GITHUB_OUTPUT" + else + echo "released=false" >> "$GITHUB_OUTPUT" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Commit and push release branch + if: steps.knope.outputs.released == 'true' + shell: bash + run: | + git commit -m "chore: prepare release ${{ matrix.package }}" + git push --force --set-upstream origin release/${{ matrix.package }} + + - name: Read version and changelog + id: meta + if: steps.knope.outputs.released == 'true' + shell: bash + run: | + # Read the top version header from the changelog knope just wrote. + VERSION=$(awk '/^## [0-9]/{print $2; exit}' ${{ matrix.changelog }}) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + CHANGELOG=$(awk "/^## $VERSION/{found=1; next} found && /^## /{exit} found{print}" ${{ matrix.changelog }}) + { + echo "changelog<> "$GITHUB_OUTPUT" + + - name: Create or update release PR + if: steps.knope.outputs.released == 'true' + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PACKAGE: ${{ matrix.package }} + VERSION: ${{ steps.meta.outputs.version }} + CHANGELOG: ${{ steps.meta.outputs.changelog }} + run: | + BRANCH="release/${PACKAGE}" + TITLE="chore: prepare release ${PACKAGE} ${VERSION}" + BODY="> [!IMPORTANT] + > Merging this PR will create a new release. + + ${CHANGELOG}" + + if gh pr view "$BRANCH" --json number -q '.number' &>/dev/null; then + gh pr edit "$BRANCH" --title "$TITLE" --body "$BODY" + else + gh pr create --head "$BRANCH" --base main --title "$TITLE" --body "$BODY" + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..5d8a10832 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release + +on: + pull_request: + types: [closed] + branches: [main] + +permissions: {} + +jobs: + release: + # Matches any release/ branch pattern + if: startsWith(github.head_ref, 'release/') && github.event.pull_request.merged == true + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Resolve package name from branch + id: branch + shell: bash + run: | + # Strips the "release/" prefix to get the package name, e.g. release/sable -> sable + echo "package=${GITHUB_HEAD_REF#release/}" >> "$GITHUB_OUTPUT" + + - uses: knope-dev/action@407e9ef7c272d2dd53a4e71e39a7839e29933c48 # v2.1.0 + with: + version: 0.22.1 + + - name: Create Release + run: knope release --package ${{ steps.branch.outputs.package }} --verbose + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/require-changeset.yml b/.github/workflows/require-changeset.yml new file mode 100644 index 000000000..396a55bbe --- /dev/null +++ b/.github/workflows/require-changeset.yml @@ -0,0 +1,100 @@ +name: Require Changeset + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + branches: [dev] + +permissions: {} + +jobs: + require-changeset: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Check for internal label + id: labels + shell: bash + env: + LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} + run: | + if echo "$LABELS" | grep -q '"internal"'; then + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: steps.labels.outputs.skip == 'false' + with: + persist-credentials: false + + - name: Check for changeset + if: steps.labels.outputs.skip == 'false' + id: check + shell: bash + run: | + count=$(find .changeset -maxdepth 1 -name '*.md' ! -name 'README.md' | wc -l) + if [[ "$count" -eq 0 ]]; then + echo "found=false" >> "$GITHUB_OUTPUT" + else + echo "found=true" >> "$GITHUB_OUTPUT" + echo "Found $count changeset file(s)." + fi + + - name: Manage changeset comment + if: steps.labels.outputs.skip == 'false' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + CHANGESET_FOUND: ${{ steps.check.outputs.found }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const marker = ''; + const found = process.env.CHANGESET_FOUND === 'true'; + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + + // Always delete the old comment first so the new one appears fresh after each commit + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number, + }); + const existing = comments.find( + (c) => c.user.type === 'Bot' && c.body.includes(marker), + ); + if (existing) { + await github.rest.issues.deleteComment({ + owner, repo, comment_id: existing.id, + }); + } + + if (!found) { + const body = `${marker} + ### ⚠️ Missing changeset + + This pull request does not include a changeset. Please add one before requesting review so the change is properly documented and included in the release notes. + + **How to add a changeset:** + + 1. Run \`npm run document-change\` (interactive) and commit the generated file, or + 2. Manually create \`.changeset/.md\`: + + \`\`\`md + --- + sable: patch + --- + + Short user-facing summary of the change. + \`\`\` + + Replace \`patch\` with \`major\`, \`minor\`, \`patch\`, \`docs\`, or \`note\` as appropriate. + + 📖 Read more in [CONTRIBUTING.md](https://github.com/SableClient/Sable/blob/dev/CONTRIBUTING.md#release-notes-and-versioning-knope). + + > If this PR is internal/maintenance with no user-facing impact, a maintainer can add the \`internal\` label to skip this check.`; + + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + core.setFailed('No changeset found. Add a changeset file or apply the "internal" label to skip.'); + } diff --git a/knope.toml b/knope.toml index f18550d86..6276f386c 100644 --- a/knope.toml +++ b/knope.toml @@ -1,4 +1,3 @@ - [packages.sable] versioned_files = ["package.json", "package-lock.json"] changelog = "CHANGELOG.md" @@ -12,19 +11,15 @@ extra_changelog_sections = [ ignore_conventional_commits = true [[workflows]] -name = "release" -help_text = "Prepare and create a new release (update changelog, bump version, create release)" +name = "prepare-release" +help_text = "Bump version and update changelog for the next release (used by CI)" [[workflows.steps]] type = "PrepareRelease" -[[workflows.steps]] -type = "Command" -command = 'git commit -m "chore: prepare release $version"' - -[[workflows.steps]] -type = "Command" -command = "git push" +[[workflows]] +name = "release" +help_text = "Create a GitHub release for the sable package (used by CI after PR merge)" [[workflows.steps]] type = "Release" @@ -40,13 +35,6 @@ type = "CreateChangeFile" owner = "SableClient" repo = "Sable" -[bot.releases] -enabled = true -#pull_request.title = "$version changelog" - -[bot.checks] -skip_labels = ["internal"] - [release_notes] change_templates = [ "* $summary", From c201b9360884fad02c05030940934b8b1972a50f Mon Sep 17 00:00:00 2001 From: hazre Date: Tue, 10 Mar 2026 00:15:05 +0100 Subject: [PATCH 02/10] ci: add direct upload cloudflare previews This should mitigate using cf build minutes hopefully --- .github/workflows/cloudflare-web-preview.yml | 138 +++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 .github/workflows/cloudflare-web-preview.yml diff --git a/.github/workflows/cloudflare-web-preview.yml b/.github/workflows/cloudflare-web-preview.yml new file mode 100644 index 000000000..b70407db3 --- /dev/null +++ b/.github/workflows/cloudflare-web-preview.yml @@ -0,0 +1,138 @@ +name: Cloudflare Worker Preview Deploy + +on: + pull_request: + +concurrency: + group: cloudflare-worker-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + deploy: + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Prepare preview metadata + id: metadata + shell: bash + run: | + preview_message="$(git log -1 --pretty=%s)" + preview_message="$(printf '%s' "$preview_message" | head -c 100)" + + { + echo 'preview_message<> "$GITHUB_OUTPUT" + + - name: Setup app and build + uses: ./.github/actions/setup + with: + build: 'true' + + - name: Upload Worker preview + id: deploy + uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 + env: + PREVIEW_MESSAGE: ${{ steps.metadata.outputs.preview_message }} + with: + apiToken: ${{ secrets.TF_CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.TF_VAR_ACCOUNT_ID }} + command: > + versions upload + -c dist/wrangler.json + --preview-alias pr-${{ github.event.pull_request.number }} + --message "$PREVIEW_MESSAGE" + + - name: Resolve preview URL + id: preview + env: + DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }} + COMMAND_OUTPUT: ${{ steps.deploy.outputs.command-output }} + PR_NUMBER: ${{ github.event.pull_request.number }} + shell: bash + run: | + alias="pr-${PR_NUMBER}" + preview_url="" + alias_url_pattern="^https?://${alias}-[^[:space:]]+$" + + if printf '%s\n' "$DEPLOYMENT_URL" | grep -Eq "$alias_url_pattern"; then + preview_url="$DEPLOYMENT_URL" + else + preview_url="$(printf '%s\n' "$COMMAND_OUTPUT" | grep -Eo "https?://${alias}-[^[:space:]\"'<>)]+" | head -n 1 || true)" + fi + + if ! printf '%s\n' "$preview_url" | grep -Eq "$alias_url_pattern"; then + echo "Failed to resolve aliased Worker preview URL." >&2 + exit 1 + fi + + { + echo "preview_alias=${alias}" + echo "preview_url=${preview_url}" + } >> "$GITHUB_OUTPUT" + + - name: Write deployment summary + env: + PREVIEW_ALIAS: ${{ steps.preview.outputs.preview_alias }} + PREVIEW_URL: ${{ steps.preview.outputs.preview_url }} + shell: bash + run: | + { + echo "## Cloudflare Worker Preview" + echo + echo "- Alias: \`${PREVIEW_ALIAS}\`" + echo "- Preview URL: ${PREVIEW_URL}" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Publish preview URL + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + MARKER: '' + PREVIEW_URL: ${{ steps.preview.outputs.preview_url }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const marker = process.env.MARKER; + const previewUrl = process.env.PREVIEW_URL; + if (!previewUrl) { + core.setFailed("Missing preview URL from Cloudflare deploy step."); + return; + } + + const body = `${marker}\nWorker preview: ${previewUrl}`; + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + }); + const existing = comments.find( + (comment) => comment.user.type === "Bot" && comment.body.includes(marker), + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + return; + } + + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); From d9f0e1cd9332e8d7c8967363015a1611c61c7013 Mon Sep 17 00:00:00 2001 From: hazre Date: Tue, 10 Mar 2026 00:15:05 +0100 Subject: [PATCH 03/10] ci: fix double quality checks --- .github/workflows/quality-checks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 9a37398e6..dc534338b 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -3,6 +3,7 @@ name: Quality checks on: pull_request: push: + branches: [dev] jobs: format: From 0381c0a05b6f75a423eca5af954c2fb5d14b1f42 Mon Sep 17 00:00:00 2001 From: hazre Date: Tue, 10 Mar 2026 00:15:05 +0100 Subject: [PATCH 04/10] ci: use setup action in docker publish workflow --- .github/workflows/docker-publish.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index ea43d4be4..88e4c4d6e 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -66,18 +66,16 @@ jobs: id: vars run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - name: Setup app + uses: ./.github/actions/setup with: - node-version: '24' - cache: 'npm' + install-command: npm ci --ignore-scripts - name: Build site env: VITE_BUILD_HASH: ${{ steps.vars.outputs.short_sha }} VITE_IS_RELEASE_TAG: ${{ steps.release_tag.outputs.is_release }} run: | - npm ci --ignore-scripts NODE_OPTIONS=--max_old_space_size=4096 npm run build - name: Set up QEMU From 8b874ec2e8df7de78c611129c3157ef6d00a6e19 Mon Sep 17 00:00:00 2001 From: hazre Date: Tue, 10 Mar 2026 00:15:05 +0100 Subject: [PATCH 05/10] ci: make dependabot handle everything --- .github/dependabot.yml | 37 +++++++++++++++++++++++++++++-------- .github/renovate.json | 14 -------------- 2 files changed, 29 insertions(+), 22 deletions(-) delete mode 100644 .github/renovate.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d4055675d..e6ba80eef 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,11 +7,12 @@ updates: default-days: 7 directory: / schedule: - interval: weekly - day: 'tuesday' - time: '01:00' - timezone: 'Asia/Kolkata' + interval: daily open-pull-requests-limit: 5 + labels: + - internal + commit-message: + prefix: chore groups: github-actions: patterns: @@ -22,12 +23,32 @@ updates: default-days: 7 directory: / schedule: - interval: weekly - day: 'tuesday' - time: '01:00' - timezone: 'Asia/Kolkata' + interval: daily open-pull-requests-limit: 5 + labels: + - internal + commit-message: + prefix: chore groups: docker: patterns: - '*' + + - package-ecosystem: npm + cooldown: + default-days: 1 + directory: / + schedule: + interval: daily + open-pull-requests-limit: 10 + labels: + - internal + commit-message: + prefix: chore + groups: + # Group all non-major updates together to reduce noise + npm-minor-patch: + update-types: + - minor + - patch + # Major updates are ungrouped so they get individual review diff --git a/.github/renovate.json b/.github/renovate.json deleted file mode 100644 index 62b0cf2a9..000000000 --- a/.github/renovate.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:recommended", ":dependencyDashboardApproval"], - "labels": ["Dependencies"], - "packageRules": [ - { - "matchUpdateTypes": ["lockFileMaintenance"] - } - ], - "lockFileMaintenance": { - "enabled": true - }, - "dependencyDashboard": true -} From 9157d04db4204760d93a8d743975e03113538906 Mon Sep 17 00:00:00 2001 From: hazre Date: Tue, 10 Mar 2026 00:15:05 +0100 Subject: [PATCH 06/10] chore: harden npm dependencies to avoid supply chain attacks --- .npmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index 567dd1f3d..e4e8189be 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,3 @@ legacy-peer-deps=true -save-exact=true engine-strict=true +min-release-age=1440 \ No newline at end of file From 15e14b93635abf42f803a2c2cc3f642aafe2e7b4 Mon Sep 17 00:00:00 2001 From: hazre Date: Tue, 10 Mar 2026 01:40:51 +0100 Subject: [PATCH 07/10] style: fix formatting --- .github/workflows/cloudflare-web-preview.yml | 276 +++++++++---------- .github/workflows/prepare-release.yml | 242 ++++++++-------- .github/workflows/release.yml | 74 ++--- .github/workflows/require-changeset.yml | 200 +++++++------- 4 files changed, 396 insertions(+), 396 deletions(-) diff --git a/.github/workflows/cloudflare-web-preview.yml b/.github/workflows/cloudflare-web-preview.yml index b70407db3..a012ca390 100644 --- a/.github/workflows/cloudflare-web-preview.yml +++ b/.github/workflows/cloudflare-web-preview.yml @@ -1,138 +1,138 @@ -name: Cloudflare Worker Preview Deploy - -on: - pull_request: - -concurrency: - group: cloudflare-worker-preview-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - deploy: - if: github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Prepare preview metadata - id: metadata - shell: bash - run: | - preview_message="$(git log -1 --pretty=%s)" - preview_message="$(printf '%s' "$preview_message" | head -c 100)" - - { - echo 'preview_message<> "$GITHUB_OUTPUT" - - - name: Setup app and build - uses: ./.github/actions/setup - with: - build: 'true' - - - name: Upload Worker preview - id: deploy - uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 - env: - PREVIEW_MESSAGE: ${{ steps.metadata.outputs.preview_message }} - with: - apiToken: ${{ secrets.TF_CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.TF_VAR_ACCOUNT_ID }} - command: > - versions upload - -c dist/wrangler.json - --preview-alias pr-${{ github.event.pull_request.number }} - --message "$PREVIEW_MESSAGE" - - - name: Resolve preview URL - id: preview - env: - DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }} - COMMAND_OUTPUT: ${{ steps.deploy.outputs.command-output }} - PR_NUMBER: ${{ github.event.pull_request.number }} - shell: bash - run: | - alias="pr-${PR_NUMBER}" - preview_url="" - alias_url_pattern="^https?://${alias}-[^[:space:]]+$" - - if printf '%s\n' "$DEPLOYMENT_URL" | grep -Eq "$alias_url_pattern"; then - preview_url="$DEPLOYMENT_URL" - else - preview_url="$(printf '%s\n' "$COMMAND_OUTPUT" | grep -Eo "https?://${alias}-[^[:space:]\"'<>)]+" | head -n 1 || true)" - fi - - if ! printf '%s\n' "$preview_url" | grep -Eq "$alias_url_pattern"; then - echo "Failed to resolve aliased Worker preview URL." >&2 - exit 1 - fi - - { - echo "preview_alias=${alias}" - echo "preview_url=${preview_url}" - } >> "$GITHUB_OUTPUT" - - - name: Write deployment summary - env: - PREVIEW_ALIAS: ${{ steps.preview.outputs.preview_alias }} - PREVIEW_URL: ${{ steps.preview.outputs.preview_url }} - shell: bash - run: | - { - echo "## Cloudflare Worker Preview" - echo - echo "- Alias: \`${PREVIEW_ALIAS}\`" - echo "- Preview URL: ${PREVIEW_URL}" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Publish preview URL - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - MARKER: '' - PREVIEW_URL: ${{ steps.preview.outputs.preview_url }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const marker = process.env.MARKER; - const previewUrl = process.env.PREVIEW_URL; - if (!previewUrl) { - core.setFailed("Missing preview URL from Cloudflare deploy step."); - return; - } - - const body = `${marker}\nWorker preview: ${previewUrl}`; - const { owner, repo } = context.repo; - const issue_number = context.issue.number; - const comments = await github.paginate(github.rest.issues.listComments, { - owner, - repo, - issue_number, - }); - const existing = comments.find( - (comment) => comment.user.type === "Bot" && comment.body.includes(marker), - ); - - if (existing) { - await github.rest.issues.updateComment({ - owner, - repo, - comment_id: existing.id, - body, - }); - return; - } - - await github.rest.issues.createComment({ - owner, - repo, - issue_number, - body, - }); +name: Cloudflare Worker Preview Deploy + +on: + pull_request: + +concurrency: + group: cloudflare-worker-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + deploy: + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Prepare preview metadata + id: metadata + shell: bash + run: | + preview_message="$(git log -1 --pretty=%s)" + preview_message="$(printf '%s' "$preview_message" | head -c 100)" + + { + echo 'preview_message<> "$GITHUB_OUTPUT" + + - name: Setup app and build + uses: ./.github/actions/setup + with: + build: 'true' + + - name: Upload Worker preview + id: deploy + uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 + env: + PREVIEW_MESSAGE: ${{ steps.metadata.outputs.preview_message }} + with: + apiToken: ${{ secrets.TF_CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.TF_VAR_ACCOUNT_ID }} + command: > + versions upload + -c dist/wrangler.json + --preview-alias pr-${{ github.event.pull_request.number }} + --message "$PREVIEW_MESSAGE" + + - name: Resolve preview URL + id: preview + env: + DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }} + COMMAND_OUTPUT: ${{ steps.deploy.outputs.command-output }} + PR_NUMBER: ${{ github.event.pull_request.number }} + shell: bash + run: | + alias="pr-${PR_NUMBER}" + preview_url="" + alias_url_pattern="^https?://${alias}-[^[:space:]]+$" + + if printf '%s\n' "$DEPLOYMENT_URL" | grep -Eq "$alias_url_pattern"; then + preview_url="$DEPLOYMENT_URL" + else + preview_url="$(printf '%s\n' "$COMMAND_OUTPUT" | grep -Eo "https?://${alias}-[^[:space:]\"'<>)]+" | head -n 1 || true)" + fi + + if ! printf '%s\n' "$preview_url" | grep -Eq "$alias_url_pattern"; then + echo "Failed to resolve aliased Worker preview URL." >&2 + exit 1 + fi + + { + echo "preview_alias=${alias}" + echo "preview_url=${preview_url}" + } >> "$GITHUB_OUTPUT" + + - name: Write deployment summary + env: + PREVIEW_ALIAS: ${{ steps.preview.outputs.preview_alias }} + PREVIEW_URL: ${{ steps.preview.outputs.preview_url }} + shell: bash + run: | + { + echo "## Cloudflare Worker Preview" + echo + echo "- Alias: \`${PREVIEW_ALIAS}\`" + echo "- Preview URL: ${PREVIEW_URL}" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Publish preview URL + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + MARKER: '' + PREVIEW_URL: ${{ steps.preview.outputs.preview_url }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const marker = process.env.MARKER; + const previewUrl = process.env.PREVIEW_URL; + if (!previewUrl) { + core.setFailed("Missing preview URL from Cloudflare deploy step."); + return; + } + + const body = `${marker}\nWorker preview: ${previewUrl}`; + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + }); + const existing = comments.find( + (comment) => comment.user.type === "Bot" && comment.body.includes(marker), + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + return; + } + + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index c17be9391..0d60007f5 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -1,121 +1,121 @@ -name: Create Release PR - -on: - push: - branches: [main] - -permissions: {} - -jobs: - load-packages: - if: "!contains(github.event.head_commit.message, 'chore: prepare release')" - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.parse.outputs.matrix }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Parse packages from knope.toml - id: parse - shell: python3 {0} - run: | - import tomllib, json, os - - with open("knope.toml", "rb") as f: - config = tomllib.load(f) - - packages = config.get("packages", {}) - matrix = [ - {"package": name, "changelog": pkg["changelog"]} - for name, pkg in packages.items() - ] - - with open(os.environ["GITHUB_OUTPUT"], "a") as out: - out.write(f"matrix={json.dumps(matrix)}\n") - - prepare-release: - needs: load-packages - if: needs.load-packages.outputs.matrix != '[]' - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - strategy: - # Run each package independently so one with no changes doesn't block others - fail-fast: false - matrix: - include: ${{ fromJSON(needs.load-packages.outputs.matrix) }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: true - - - uses: fregante/setup-git-user@024bc0b8e177d7e77203b48dab6fb45666854b35 # v2.0.2 - - - uses: knope-dev/action@407e9ef7c272d2dd53a4e71e39a7839e29933c48 # v2.1.0 - with: - version: 0.22.1 - - - name: Switch to release branch - shell: bash - run: git switch -c release/${{ matrix.package }} - - - name: Prepare Release - id: knope - shell: bash - run: | - if knope prepare-release --package ${{ matrix.package }} --verbose; then - echo "released=true" >> "$GITHUB_OUTPUT" - else - echo "released=false" >> "$GITHUB_OUTPUT" - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Commit and push release branch - if: steps.knope.outputs.released == 'true' - shell: bash - run: | - git commit -m "chore: prepare release ${{ matrix.package }}" - git push --force --set-upstream origin release/${{ matrix.package }} - - - name: Read version and changelog - id: meta - if: steps.knope.outputs.released == 'true' - shell: bash - run: | - # Read the top version header from the changelog knope just wrote. - VERSION=$(awk '/^## [0-9]/{print $2; exit}' ${{ matrix.changelog }}) - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - CHANGELOG=$(awk "/^## $VERSION/{found=1; next} found && /^## /{exit} found{print}" ${{ matrix.changelog }}) - { - echo "changelog<> "$GITHUB_OUTPUT" - - - name: Create or update release PR - if: steps.knope.outputs.released == 'true' - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PACKAGE: ${{ matrix.package }} - VERSION: ${{ steps.meta.outputs.version }} - CHANGELOG: ${{ steps.meta.outputs.changelog }} - run: | - BRANCH="release/${PACKAGE}" - TITLE="chore: prepare release ${PACKAGE} ${VERSION}" - BODY="> [!IMPORTANT] - > Merging this PR will create a new release. - - ${CHANGELOG}" - - if gh pr view "$BRANCH" --json number -q '.number' &>/dev/null; then - gh pr edit "$BRANCH" --title "$TITLE" --body "$BODY" - else - gh pr create --head "$BRANCH" --base main --title "$TITLE" --body "$BODY" - fi +name: Create Release PR + +on: + push: + branches: [main] + +permissions: {} + +jobs: + load-packages: + if: "!contains(github.event.head_commit.message, 'chore: prepare release')" + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.parse.outputs.matrix }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Parse packages from knope.toml + id: parse + shell: python3 {0} + run: | + import tomllib, json, os + + with open("knope.toml", "rb") as f: + config = tomllib.load(f) + + packages = config.get("packages", {}) + matrix = [ + {"package": name, "changelog": pkg["changelog"]} + for name, pkg in packages.items() + ] + + with open(os.environ["GITHUB_OUTPUT"], "a") as out: + out.write(f"matrix={json.dumps(matrix)}\n") + + prepare-release: + needs: load-packages + if: needs.load-packages.outputs.matrix != '[]' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + strategy: + # Run each package independently so one with no changes doesn't block others + fail-fast: false + matrix: + include: ${{ fromJSON(needs.load-packages.outputs.matrix) }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: true + + - uses: fregante/setup-git-user@024bc0b8e177d7e77203b48dab6fb45666854b35 # v2.0.2 + + - uses: knope-dev/action@407e9ef7c272d2dd53a4e71e39a7839e29933c48 # v2.1.0 + with: + version: 0.22.1 + + - name: Switch to release branch + shell: bash + run: git switch -c release/${{ matrix.package }} + + - name: Prepare Release + id: knope + shell: bash + run: | + if knope prepare-release --package ${{ matrix.package }} --verbose; then + echo "released=true" >> "$GITHUB_OUTPUT" + else + echo "released=false" >> "$GITHUB_OUTPUT" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Commit and push release branch + if: steps.knope.outputs.released == 'true' + shell: bash + run: | + git commit -m "chore: prepare release ${{ matrix.package }}" + git push --force --set-upstream origin release/${{ matrix.package }} + + - name: Read version and changelog + id: meta + if: steps.knope.outputs.released == 'true' + shell: bash + run: | + # Read the top version header from the changelog knope just wrote. + VERSION=$(awk '/^## [0-9]/{print $2; exit}' ${{ matrix.changelog }}) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + CHANGELOG=$(awk "/^## $VERSION/{found=1; next} found && /^## /{exit} found{print}" ${{ matrix.changelog }}) + { + echo "changelog<> "$GITHUB_OUTPUT" + + - name: Create or update release PR + if: steps.knope.outputs.released == 'true' + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PACKAGE: ${{ matrix.package }} + VERSION: ${{ steps.meta.outputs.version }} + CHANGELOG: ${{ steps.meta.outputs.changelog }} + run: | + BRANCH="release/${PACKAGE}" + TITLE="chore: prepare release ${PACKAGE} ${VERSION}" + BODY="> [!IMPORTANT] + > Merging this PR will create a new release. + + ${CHANGELOG}" + + if gh pr view "$BRANCH" --json number -q '.number' &>/dev/null; then + gh pr edit "$BRANCH" --title "$TITLE" --body "$BODY" + else + gh pr create --head "$BRANCH" --base main --title "$TITLE" --body "$BODY" + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d8a10832..694517a3d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,37 +1,37 @@ -name: Release - -on: - pull_request: - types: [closed] - branches: [main] - -permissions: {} - -jobs: - release: - # Matches any release/ branch pattern - if: startsWith(github.head_ref, 'release/') && github.event.pull_request.merged == true - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Resolve package name from branch - id: branch - shell: bash - run: | - # Strips the "release/" prefix to get the package name, e.g. release/sable -> sable - echo "package=${GITHUB_HEAD_REF#release/}" >> "$GITHUB_OUTPUT" - - - uses: knope-dev/action@407e9ef7c272d2dd53a4e71e39a7839e29933c48 # v2.1.0 - with: - version: 0.22.1 - - - name: Create Release - run: knope release --package ${{ steps.branch.outputs.package }} --verbose - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +name: Release + +on: + pull_request: + types: [closed] + branches: [main] + +permissions: {} + +jobs: + release: + # Matches any release/ branch pattern + if: startsWith(github.head_ref, 'release/') && github.event.pull_request.merged == true + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Resolve package name from branch + id: branch + shell: bash + run: | + # Strips the "release/" prefix to get the package name, e.g. release/sable -> sable + echo "package=${GITHUB_HEAD_REF#release/}" >> "$GITHUB_OUTPUT" + + - uses: knope-dev/action@407e9ef7c272d2dd53a4e71e39a7839e29933c48 # v2.1.0 + with: + version: 0.22.1 + + - name: Create Release + run: knope release --package ${{ steps.branch.outputs.package }} --verbose + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/require-changeset.yml b/.github/workflows/require-changeset.yml index 396a55bbe..cc9415e86 100644 --- a/.github/workflows/require-changeset.yml +++ b/.github/workflows/require-changeset.yml @@ -1,100 +1,100 @@ -name: Require Changeset - -on: - pull_request: - types: [opened, synchronize, reopened, labeled, unlabeled] - branches: [dev] - -permissions: {} - -jobs: - require-changeset: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Check for internal label - id: labels - shell: bash - env: - LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} - run: | - if echo "$LABELS" | grep -q '"internal"'; then - echo "skip=true" >> "$GITHUB_OUTPUT" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - if: steps.labels.outputs.skip == 'false' - with: - persist-credentials: false - - - name: Check for changeset - if: steps.labels.outputs.skip == 'false' - id: check - shell: bash - run: | - count=$(find .changeset -maxdepth 1 -name '*.md' ! -name 'README.md' | wc -l) - if [[ "$count" -eq 0 ]]; then - echo "found=false" >> "$GITHUB_OUTPUT" - else - echo "found=true" >> "$GITHUB_OUTPUT" - echo "Found $count changeset file(s)." - fi - - - name: Manage changeset comment - if: steps.labels.outputs.skip == 'false' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - CHANGESET_FOUND: ${{ steps.check.outputs.found }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const marker = ''; - const found = process.env.CHANGESET_FOUND === 'true'; - const { owner, repo } = context.repo; - const issue_number = context.issue.number; - - // Always delete the old comment first so the new one appears fresh after each commit - const comments = await github.paginate(github.rest.issues.listComments, { - owner, repo, issue_number, - }); - const existing = comments.find( - (c) => c.user.type === 'Bot' && c.body.includes(marker), - ); - if (existing) { - await github.rest.issues.deleteComment({ - owner, repo, comment_id: existing.id, - }); - } - - if (!found) { - const body = `${marker} - ### ⚠️ Missing changeset - - This pull request does not include a changeset. Please add one before requesting review so the change is properly documented and included in the release notes. - - **How to add a changeset:** - - 1. Run \`npm run document-change\` (interactive) and commit the generated file, or - 2. Manually create \`.changeset/.md\`: - - \`\`\`md - --- - sable: patch - --- - - Short user-facing summary of the change. - \`\`\` - - Replace \`patch\` with \`major\`, \`minor\`, \`patch\`, \`docs\`, or \`note\` as appropriate. - - 📖 Read more in [CONTRIBUTING.md](https://github.com/SableClient/Sable/blob/dev/CONTRIBUTING.md#release-notes-and-versioning-knope). - - > If this PR is internal/maintenance with no user-facing impact, a maintainer can add the \`internal\` label to skip this check.`; - - await github.rest.issues.createComment({ owner, repo, issue_number, body }); - core.setFailed('No changeset found. Add a changeset file or apply the "internal" label to skip.'); - } +name: Require Changeset + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + branches: [dev] + +permissions: {} + +jobs: + require-changeset: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Check for internal label + id: labels + shell: bash + env: + LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} + run: | + if echo "$LABELS" | grep -q '"internal"'; then + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: steps.labels.outputs.skip == 'false' + with: + persist-credentials: false + + - name: Check for changeset + if: steps.labels.outputs.skip == 'false' + id: check + shell: bash + run: | + count=$(find .changeset -maxdepth 1 -name '*.md' ! -name 'README.md' | wc -l) + if [[ "$count" -eq 0 ]]; then + echo "found=false" >> "$GITHUB_OUTPUT" + else + echo "found=true" >> "$GITHUB_OUTPUT" + echo "Found $count changeset file(s)." + fi + + - name: Manage changeset comment + if: steps.labels.outputs.skip == 'false' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + CHANGESET_FOUND: ${{ steps.check.outputs.found }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const marker = ''; + const found = process.env.CHANGESET_FOUND === 'true'; + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + + // Always delete the old comment first so the new one appears fresh after each commit + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number, + }); + const existing = comments.find( + (c) => c.user.type === 'Bot' && c.body.includes(marker), + ); + if (existing) { + await github.rest.issues.deleteComment({ + owner, repo, comment_id: existing.id, + }); + } + + if (!found) { + const body = `${marker} + ### ⚠️ Missing changeset + + This pull request does not include a changeset. Please add one before requesting review so the change is properly documented and included in the release notes. + + **How to add a changeset:** + + 1. Run \`npm run document-change\` (interactive) and commit the generated file, or + 2. Manually create \`.changeset/.md\`: + + \`\`\`md + --- + sable: patch + --- + + Short user-facing summary of the change. + \`\`\` + + Replace \`patch\` with \`major\`, \`minor\`, \`patch\`, \`docs\`, or \`note\` as appropriate. + + 📖 Read more in [CONTRIBUTING.md](https://github.com/SableClient/Sable/blob/dev/CONTRIBUTING.md#release-notes-and-versioning-knope). + + > If this PR is internal/maintenance with no user-facing impact, a maintainer can add the \`internal\` label to skip this check.`; + + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + core.setFailed('No changeset found. Add a changeset file or apply the "internal" label to skip.'); + } From d85a9cc2ec27f26b2894d0d77445d78bcf2da48f Mon Sep 17 00:00:00 2001 From: hazre Date: Tue, 10 Mar 2026 01:46:02 +0100 Subject: [PATCH 08/10] ci: make require changeset workflow smarter --- .github/workflows/require-changeset.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/require-changeset.yml b/.github/workflows/require-changeset.yml index cc9415e86..1f38bcca6 100644 --- a/.github/workflows/require-changeset.yml +++ b/.github/workflows/require-changeset.yml @@ -29,19 +29,23 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 if: steps.labels.outputs.skip == 'false' with: + fetch-depth: 0 persist-credentials: false - - name: Check for changeset + - name: Check for changeset added by this PR if: steps.labels.outputs.skip == 'false' id: check shell: bash + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} run: | - count=$(find .changeset -maxdepth 1 -name '*.md' ! -name 'README.md' | wc -l) + # Only count changeset files this PR added, ignoring pre-existing ones on the base branch. + count=$(git diff --name-only --diff-filter=A "origin/${BASE_REF}...HEAD" | grep -E '^\.changeset/.+\.md$' | grep -cv README.md || true) if [[ "$count" -eq 0 ]]; then echo "found=false" >> "$GITHUB_OUTPUT" else echo "found=true" >> "$GITHUB_OUTPUT" - echo "Found $count changeset file(s)." + echo "Found $count new changeset file(s)." fi - name: Manage changeset comment From c5c7546563337cdfb0e944339cc04e819f86dab1 Mon Sep 17 00:00:00 2001 From: hazre Date: Tue, 10 Mar 2026 01:54:38 +0100 Subject: [PATCH 09/10] ci: make preview build comments more pretty --- .github/workflows/cloudflare-web-preview.yml | 58 +++++++++----------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/.github/workflows/cloudflare-web-preview.yml b/.github/workflows/cloudflare-web-preview.yml index a012ca390..ec0ebdae2 100644 --- a/.github/workflows/cloudflare-web-preview.yml +++ b/.github/workflows/cloudflare-web-preview.yml @@ -80,59 +80,53 @@ jobs: echo "preview_url=${preview_url}" } >> "$GITHUB_OUTPUT" - - name: Write deployment summary - env: - PREVIEW_ALIAS: ${{ steps.preview.outputs.preview_alias }} - PREVIEW_URL: ${{ steps.preview.outputs.preview_url }} - shell: bash - run: | - { - echo "## Cloudflare Worker Preview" - echo - echo "- Alias: \`${PREVIEW_ALIAS}\`" - echo "- Preview URL: ${PREVIEW_URL}" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Publish preview URL + - name: Publish preview URL and write summary uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: MARKER: '' PREVIEW_URL: ${{ steps.preview.outputs.preview_url }} + PREVIEW_ALIAS: ${{ steps.preview.outputs.preview_alias }} + SHORT_SHA: ${{ github.event.pull_request.head.sha }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const marker = process.env.MARKER; const previewUrl = process.env.PREVIEW_URL; + const previewAlias = process.env.PREVIEW_ALIAS; + const shortSha = process.env.SHORT_SHA?.slice(0, 7); + const now = new Date().toUTCString().replace(':00 GMT', ' UTC'); + if (!previewUrl) { core.setFailed("Missing preview URL from Cloudflare deploy step."); return; } - const body = `${marker}\nWorker preview: ${previewUrl}`; + const tableRow = "| ✅ Deployment successful! | " + previewUrl + " | " + shortSha + " | `" + previewAlias + "` | " + now + " |"; + const comment = [ + marker, + `## Deploying with  Cloudflare Workers  Cloudflare Workers`, + ``, + `| Status | Preview URL | Commit | Alias | Updated (UTC) |`, + `| - | - | - | - | - |`, + tableRow, + ].join("\n"); + + // Write to step summary (marker stripped — not needed there) + await core.summary.addRaw(comment.replace(marker + "\n", "")).write(); + + // Always delete and recreate so the comment appears fresh after each push const { owner, repo } = context.repo; const issue_number = context.issue.number; const comments = await github.paginate(github.rest.issues.listComments, { - owner, - repo, - issue_number, + owner, repo, issue_number, }); const existing = comments.find( - (comment) => comment.user.type === "Bot" && comment.body.includes(marker), + (c) => c.user.type === "Bot" && c.body.includes(marker), ); - if (existing) { - await github.rest.issues.updateComment({ - owner, - repo, - comment_id: existing.id, - body, + await github.rest.issues.deleteComment({ + owner, repo, comment_id: existing.id, }); - return; } - await github.rest.issues.createComment({ - owner, - repo, - issue_number, - body, - }); + await github.rest.issues.createComment({ owner, repo, issue_number, body: comment }); From dfcac585eb838bdb9798f79d48b204a397e0289a Mon Sep 17 00:00:00 2001 From: hazre Date: Tue, 10 Mar 2026 02:26:54 +0100 Subject: [PATCH 10/10] ci: fix default branch name --- .github/workflows/prepare-release.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 0d60007f5..b10ca16d9 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -2,7 +2,7 @@ name: Create Release PR on: push: - branches: [main] + branches: [dev] permissions: {} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 694517a3d..938d4cbf9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release on: pull_request: types: [closed] - branches: [main] + branches: [dev] permissions: {}