From 475ae0eae3ba4230962a86c34a6bbf0e070bf3b7 Mon Sep 17 00:00:00 2001 From: CodeDotJS Date: Tue, 6 Oct 2026 04:32:41 +0530 Subject: [PATCH] chore(deploy): delete the merged branch after production --- .cursor/rules/60-deployment.mdc | 2 +- .github/workflows/ci.yml | 44 ++++++++++++++++++++++++++++++++ docs/DEPLOYMENT.md | 45 +++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/.cursor/rules/60-deployment.mdc b/.cursor/rules/60-deployment.mdc index 0fece7c..108e27e 100644 --- a/.cursor/rules/60-deployment.mdc +++ b/.cursor/rules/60-deployment.mdc @@ -17,7 +17,7 @@ When the user says to deploy: 4. Wait for the `check` job in the CI workflow. It runs `npm ci`, `npm test`, and `npm run build`, rejects `dist` above 20,000 files or any file over 25 MiB, then uploads a preview. `npm run smoke` is not in the job. 5. Open the preview at `https://.detro.pages.dev`. A slash in the branch name becomes a hyphen. Confirm `/`, `/map`, `/city`, `/saved`, and `/help` load. If the check fails, stop and report the check and the URL. 6. Merge into `main` only after that job is green. Branch protection requires the `check` job. -7. The merge publishes production at `https://detro.pages.dev`. +7. The merge publishes production at `https://detro.pages.dev`. After that upload succeeds, CI deletes the merged feature branch. Do not delete `main`. If the production upload fails, the branch stays. The Pages project `detro` already exists and is Direct Upload. Do not create a second project, do not connect Pages to Git, and do not upload with Wrangler from this machine. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c192f9f..5fc3755 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,3 +49,47 @@ jobs: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy dist --project-name=detro --branch=${{ github.head_ref || github.ref_name }} gitHubToken: ${{ secrets.GITHUB_TOKEN }} + + delete-merged-branch: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: check + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - name: Delete the merged branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + repo="${GITHUB_REPOSITORY}" + sha="${GITHUB_SHA}" + branches=$(gh api "repos/${repo}/commits/${sha}/pulls" \ + | jq -r --arg repo "$repo" \ + '.[] | select(.base.ref == "main" and (.head.repo.full_name // $repo) == $repo) | .head.ref' \ + | sort -u) + if [ -z "${branches}" ]; then + message=$(gh api "repos/${repo}/commits/${sha}" --jq .commit.message) + if [[ "${message}" =~ Merge\ pull\ request\ #([0-9]+) ]]; then + branches=$(gh pr view "${BASH_REMATCH[1]}" --repo "$repo" --json headRefName --jq .headRefName) + fi + fi + if [ -z "${branches}" ]; then + echo "No merged branch on this commit" + exit 0 + fi + while IFS= read -r branch; do + [ -z "${branch}" ] && continue + if [ "${branch}" = "main" ]; then + echo "Refusing to delete main" + continue + fi + encoded=$(jq -nr --arg branch "$branch" '$branch | @uri') + if gh api "repos/${repo}/git/refs/heads/${encoded}" >/dev/null 2>&1; then + gh api --method DELETE "repos/${repo}/git/refs/heads/${encoded}" + echo "Deleted ${branch}" + else + echo "${branch} is already gone" + fi + done <<< "${branches}" diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index ed407c1..f7cd81e 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -84,6 +84,50 @@ jobs: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy dist --project-name=detro --branch=${{ github.head_ref || github.ref_name }} gitHubToken: ${{ secrets.GITHUB_TOKEN }} + + delete-merged-branch: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: check + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - name: Delete the merged branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + repo="${GITHUB_REPOSITORY}" + sha="${GITHUB_SHA}" + branches=$(gh api "repos/${repo}/commits/${sha}/pulls" \ + | jq -r --arg repo "$repo" \ + '.[] | select(.base.ref == "main" and (.head.repo.full_name // $repo) == $repo) | .head.ref' \ + | sort -u) + if [ -z "${branches}" ]; then + message=$(gh api "repos/${repo}/commits/${sha}" --jq .commit.message) + if [[ "${message}" =~ Merge\ pull\ request\ #([0-9]+) ]]; then + branches=$(gh pr view "${BASH_REMATCH[1]}" --repo "$repo" --json headRefName --jq .headRefName) + fi + fi + if [ -z "${branches}" ]; then + echo "No merged branch on this commit" + exit 0 + fi + while IFS= read -r branch; do + [ -z "${branch}" ] && continue + if [ "${branch}" = "main" ]; then + echo "Refusing to delete main" + continue + fi + encoded=$(jq -nr --arg branch "$branch" '$branch | @uri') + if gh api "repos/${repo}/git/refs/heads/${encoded}" >/dev/null 2>&1; then + gh api --method DELETE "repos/${repo}/git/refs/heads/${encoded}" + echo "Deleted ${branch}" + else + echo "${branch} is already gone" + fi + done <<< "${branches}" ``` A push to `main` publishes production. A pull request from this repository publishes a preview at `https://.detro.pages.dev`. A slash in the branch name becomes a hyphen. A pull request from a fork does not receive the secrets, so the deploy step must not run for it. `npm run smoke` is not in this job. @@ -104,6 +148,7 @@ Two free steps, and a deploy cannot start unless the tests pass. 2. The same job counts the files in `dist` and measures the largest file. It fails at 20,000 files or at a file over 25 MiB, which are the Pages free limits. 3. On a pull request from this repository, the job uploads `dist` with Wrangler as a Pages preview. The address looks like `https://.detro.pages.dev`. 4. On a push to `main`, after the same tests, the job uploads `dist` as production. +5. After that production upload succeeds, a second job deletes the merged feature branch. It does not run on a pull request, and it does not delete `main`. If the production upload fails, the branch stays. Cloudflare Pages is a Direct Upload project. It does not also build from Git. A Git-connected build would deploy even when the tests failed, and it would spend the 500 builds a month. A Wrangler upload does not use that build quota. GitHub-hosted runners are free for a public repository, and a private repository includes 2,000 minutes a month, which this test-and-build job will not use up.