From a83df4c184e42cbf5cd9ea8477ae88b74212ed9f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 15:04:59 +0000 Subject: [PATCH 1/4] ci: validate sandbox build and auto-open sandbox->main promotion PR - Triggers on push to sandbox (i.e. whenever a PR merges into it) - Runs npm ci + npm run build as the required gate; lint is informational only - If the build passes and sandbox has new commits, opens (or updates) a sandbox -> main PR summarizing what's being promoted, and requests review from the repo owner as the notification - Never merges the PR itself - a human must review and merge manually Co-authored-by: Thomas Bohn --- .github/workflows/promote-sandbox-to-main.yml | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 .github/workflows/promote-sandbox-to-main.yml diff --git a/.github/workflows/promote-sandbox-to-main.yml b/.github/workflows/promote-sandbox-to-main.yml new file mode 100644 index 0000000..3702170 --- /dev/null +++ b/.github/workflows/promote-sandbox-to-main.yml @@ -0,0 +1,135 @@ +name: Validate sandbox and promote to main + +# Runs after any merge into `sandbox` (a merged PR triggers a push to sandbox). +# 1. Validates that sandbox still builds successfully. +# 2. If valid, opens (or updates) a sandbox -> main pull request for a human to review and merge. +# This workflow NEVER merges the PR itself - promotion to production always requires a manual merge. +on: + push: + branches: + - sandbox + workflow_dispatch: {} + +permissions: + contents: read + pull-requests: write + +concurrency: + group: promote-sandbox-to-main + cancel-in-progress: false + +jobs: + validate: + name: Validate sandbox build + runs-on: ubuntu-latest + outputs: + ahead-count: ${{ steps.diff.outputs.ahead-count }} + steps: + - name: Checkout sandbox + uses: actions/checkout@v4 + with: + ref: sandbox + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Build (required gate) + run: npm run build + + - name: Lint (informational only, does not block promotion) + id: lint + continue-on-error: true + run: npm run lint + + - name: Check how far ahead sandbox is of main + id: diff + run: | + git fetch origin main --depth=1 2>/dev/null || git fetch origin main + AHEAD=$(git rev-list --count origin/main..HEAD) + echo "sandbox is $AHEAD commit(s) ahead of main" + echo "ahead-count=$AHEAD" >> "$GITHUB_OUTPUT" + + - name: Record lint outcome + run: | + echo "Lint step outcome: ${{ steps.lint.outcome }}" + + promote: + name: Open/update promotion PR + needs: validate + # Only runs if `validate` succeeded (default GitHub Actions behavior for + # jobs with `needs`) AND sandbox actually has commits main doesn't have yet. + if: success() && needs.validate.outputs.ahead-count != '0' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build PR description file + env: + VALIDATED_SHA: ${{ github.sha }} + WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + { + echo "## Sandbox validated and ready for production" + echo + echo "This PR was opened automatically after \`sandbox\` was validated by" + echo "[this workflow run]($WORKFLOW_RUN_URL)." + echo + echo "- Build (\`npm run build\`): passed" + echo "- Commit validated: \`$VALIDATED_SHA\`" + echo + echo "### Commits included in this promotion" + echo + git log origin/main..origin/sandbox --pretty=format:'- %s (%h)' + echo + echo + echo "### Review checklist" + echo + echo "- [ ] Build passed in CI (see above)" + echo "- [ ] Changes reviewed and look correct for production" + echo "- [ ] Ready to deploy (merging this PR triggers the GitHub Pages deploy workflow)" + echo + echo "**This PR requires manual human review and must be merged by a person.**" + echo "This automation will never merge it." + } > promotion-pr-body.md + + - name: Create or update promotion PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VALIDATED_SHA: ${{ github.sha }} + run: | + EXISTING_PR=$(gh pr list --base main --head sandbox --state open --json number --jq '.[0].number') + + if [ -n "$EXISTING_PR" ]; then + echo "Updating existing promotion PR #$EXISTING_PR" + gh pr edit "$EXISTING_PR" --body-file promotion-pr-body.md + gh pr comment "$EXISTING_PR" --body "Sandbox re-validated at \`$VALIDATED_SHA\` - build passed. Ready for review." + echo "pr-number=$EXISTING_PR" >> "$GITHUB_OUTPUT" + else + echo "Creating new promotion PR" + gh pr create \ + --base main \ + --head sandbox \ + --title "Promote sandbox to main ($(date -u +%Y-%m-%d))" \ + --body-file promotion-pr-body.md + fi + + - name: Request review from repo owner (notification) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO_OWNER: ${{ github.repository_owner }} + continue-on-error: true + run: | + PR_NUMBER=$(gh pr list --base main --head sandbox --state open --json number --jq '.[0].number') + if [ -n "$PR_NUMBER" ]; then + gh pr edit "$PR_NUMBER" --add-reviewer "$REPO_OWNER" || true + fi From aa3dbe2cafc25b28bb465fd48b2f00a6b0d9c3cb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 15:05:09 +0000 Subject: [PATCH 2/4] docs: add sandbox-to-main-promotion rule and cross-link from medium-blog-sync - New rule documents the mostly-automated promotion flow (CI validates the build and opens a human-reviewed sandbox -> main PR) plus a manual fallback and guardrails (never auto-merge, never push directly to main) - medium-blog-sync now notes that its PR should target sandbox, and that production promotion is handled by the new rule/workflow afterward Co-authored-by: Thomas Bohn --- .cursor/rules/medium-blog-sync.mdc | 4 ++ .cursor/rules/sandbox-to-main-promotion.mdc | 72 +++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 .cursor/rules/sandbox-to-main-promotion.mdc diff --git a/.cursor/rules/medium-blog-sync.mdc b/.cursor/rules/medium-blog-sync.mdc index 485d646..7a6e4e2 100644 --- a/.cursor/rules/medium-blog-sync.mdc +++ b/.cursor/rules/medium-blog-sync.mdc @@ -36,6 +36,10 @@ Use this workflow when the user asks to sync Medium articles, add new Medium pos 6. Commit and push the updated files, using a descriptive message such as `content: sync N new Medium post(s)` (adjust wording if only the homepage selection or refreshed posts changed, e.g. `content: refresh synced Medium posts`). +7. Open a PR into `sandbox` (not `main`) with these changes. Once that PR is + merged, production promotion is handled automatically — see the + `sandbox-to-main-promotion` rule. Do not push to `main` yourself as part of + this flow. ## Guardrails diff --git a/.cursor/rules/sandbox-to-main-promotion.mdc b/.cursor/rules/sandbox-to-main-promotion.mdc new file mode 100644 index 0000000..acb72d0 --- /dev/null +++ b/.cursor/rules/sandbox-to-main-promotion.mdc @@ -0,0 +1,72 @@ +--- +description: Promote validated sandbox changes to main/production after a build passes, via a human-reviewed PR. +alwaysApply: false +--- + +# Sandbox → Main Promotion + +Use this workflow when the user asks to ship/release/deploy to production, promote `sandbox` to `main`, +or check whether recent `sandbox` changes (e.g. from the Medium blog sync flow) are ready for production. + +## How this actually works (mostly automated) + +Promotion to production is primarily handled by CI/CD, not by an agent: + +1. A PR is merged into `sandbox` (e.g. the Medium sync PR from the + `medium-blog-sync` rule, or any other feature PR). +2. That merge is a push to `sandbox`, which triggers + `.github/workflows/promote-sandbox-to-main.yml`: + - Installs deps and runs `npm run build` as a hard gate (lint runs too, but + is informational only and never blocks promotion). + - If the build passes and `sandbox` has commits `main` doesn't have yet, it + opens (or updates, if one is already open) a `sandbox → main` pull request, + summarizing the commits being promoted, and requests review from the repo + owner (this is the notification — GitHub emails/notifies the requested + reviewer). + - If the build fails, no PR is created/updated, and the failed workflow run + itself is the signal that something needs to be fixed on `sandbox` first. +3. A human reviews the promotion PR and merges it manually. Merging it is a + push to `main`, which triggers `.github/workflows/deploy.yml` and deploys to + GitHub Pages. + +**In the common case, no agent action is required after a PR merges into +`sandbox`** — the workflow above handles validation, PR creation, and +notification automatically. + +## When an agent should act directly + +Use this manual flow only if asked to promote directly, to check status, or if +CI/CD isn't available/didn't run: + +1. Confirm the relevant PR(s) actually merged into `sandbox` + (`git log origin/sandbox` / `gh pr view `). +2. Validate locally exactly like CI does: + - `npm install` (if `node_modules/` is missing) + - `npm run build` (must pass — this is the hard gate) +3. Check for an existing open promotion PR before creating a new one: + - `gh pr list --base main --head sandbox --state open` + - If one exists, update it (new commits/description) rather than opening a + duplicate. +4. If none exists and the build passed, open a `sandbox → main` PR (not draft, + since it needs human review) summarizing the commits being promoted + (`git log origin/main..origin/sandbox --oneline`). +5. Request review from the repo owner as the notification mechanism. + +## Guardrails + +- Never merge the promotion PR yourself, and never enable auto-merge on it — + promotion to production always requires a manual human merge, per explicit + user instruction. +- Never push directly to `main`. All production changes go through a + `sandbox → main` pull request. +- Never force-push `sandbox` or `main`. +- Do not open a promotion PR if the build fails, or if `sandbox` has no commits + ahead of `main` (nothing to promote). +- Treat `npm run build` as the required gate and `npm run lint` as informational + only — this repo has pre-existing lint findings that are unrelated to + promotion readiness (see `CLOUD.md`'s "main priority is to keep the site + building successfully"). +- If you change the promotion automation itself + (`.github/workflows/promote-sandbox-to-main.yml`), keep the "build gate + + human-merged PR" contract intact; do not introduce auto-merge behavior + without an explicit user request. From d4c290345be1f9d6807e4d29db464cb3f2cea4ee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 15:16:54 +0000 Subject: [PATCH 3/4] ci: add failure path + fix PR-creation permission bug in promotion workflow Root cause of the failed run on PR #28's merge: GitHub Actions is not permitted to create pull requests in this repo (Settings > Actions > General > Workflow permissions > 'Allow GitHub Actions to create and approve pull requests' is disabled by default). - Detect that specific GraphQL error and open/update a tracking GitHub Issue with the exact remediation steps, instead of failing silently in the logs - Add the same notify-on-failure/auto-resolve-on-success pattern for build failures (separate tracking issue) - Support an optional PROMOTION_PR_TOKEN repo secret (PAT) as a drop-in alternative to enabling the repo-wide Actions permission - Restructure the pr create/update error handling so 'set -e' doesn't abort before the failure can be captured and reported Co-authored-by: Thomas Bohn --- .github/workflows/promote-sandbox-to-main.yml | 144 ++++++++++++++++-- 1 file changed, 135 insertions(+), 9 deletions(-) diff --git a/.github/workflows/promote-sandbox-to-main.yml b/.github/workflows/promote-sandbox-to-main.yml index 3702170..9a7dab9 100644 --- a/.github/workflows/promote-sandbox-to-main.yml +++ b/.github/workflows/promote-sandbox-to-main.yml @@ -4,6 +4,12 @@ name: Validate sandbox and promote to main # 1. Validates that sandbox still builds successfully. # 2. If valid, opens (or updates) a sandbox -> main pull request for a human to review and merge. # This workflow NEVER merges the PR itself - promotion to production always requires a manual merge. +# +# Failure path: if the build fails, or the PR can't be created/updated (most +# commonly because "Allow GitHub Actions to create and approve pull requests" +# is disabled under Settings > Actions > General > Workflow permissions), this +# workflow opens/updates a tracking GitHub Issue so a human is notified even +# though the job itself still fails (visible as a red X in the Actions tab). on: push: branches: @@ -13,11 +19,21 @@ on: permissions: contents: read pull-requests: write + issues: write concurrency: group: promote-sandbox-to-main cancel-in-progress: false +env: + # Falls back to the default token, but automatically uses a PAT instead if + # one is added later as a repo secret named PROMOTION_PR_TOKEN - useful if + # you'd rather not enable "Allow GitHub Actions to create and approve pull + # requests" repo-wide. + GH_TOKEN: ${{ secrets.PROMOTION_PR_TOKEN || secrets.GITHUB_TOKEN }} + BUILD_FAILURE_ISSUE_TITLE: "Sandbox promotion automation needs attention: build failed" + PR_FAILURE_ISSUE_TITLE: "Sandbox promotion automation needs attention: could not open/update PR" + jobs: validate: name: Validate sandbox build @@ -60,6 +76,42 @@ jobs: run: | echo "Lint step outcome: ${{ steps.lint.outcome }}" + # Runs only when a previous required step (checkout/install/build) failed. + - name: Report build failure + if: failure() + env: + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + { + echo "The sandbox build failed in [this workflow run]($RUN_URL), so no" + echo "sandbox -> main promotion PR was created or updated." + echo + echo "Check the run logs for the actual error, fix it on \`sandbox\`, and this" + echo "workflow will automatically re-run on the next push (or trigger it manually" + echo "via Actions > Validate sandbox and promote to main > Run workflow)." + } > build-failure-body.md + + EXISTING=$(gh issue list --state open --json number,title \ + --jq ".[] | select(.title == \"$BUILD_FAILURE_ISSUE_TITLE\") | .number" | head -n1) + + if [ -n "$EXISTING" ]; then + gh issue comment "$EXISTING" --body-file build-failure-body.md + else + gh issue create --title "$BUILD_FAILURE_ISSUE_TITLE" --body-file build-failure-body.md --label bug + fi + + # Runs when the build succeeds, to auto-resolve a previously filed failure issue. + - name: Close stale build-failure issue if now fixed + if: success() + run: | + EXISTING=$(gh issue list --state open --json number,title \ + --jq ".[] | select(.title == \"$BUILD_FAILURE_ISSUE_TITLE\") | .number" | head -n1) + + if [ -n "$EXISTING" ]; then + gh issue comment "$EXISTING" --body "Build passed again at \`${{ github.sha }}\`. Closing." + gh issue close "$EXISTING" + fi + promote: name: Open/update promotion PR needs: validate @@ -103,29 +155,91 @@ jobs: } > promotion-pr-body.md - name: Create or update promotion PR + id: promote_pr env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VALIDATED_SHA: ${{ github.sha }} run: | EXISTING_PR=$(gh pr list --base main --head sandbox --state open --json number --jq '.[0].number') + DELIM="PROMOTION_ERR_$$" if [ -n "$EXISTING_PR" ]; then echo "Updating existing promotion PR #$EXISTING_PR" - gh pr edit "$EXISTING_PR" --body-file promotion-pr-body.md - gh pr comment "$EXISTING_PR" --body "Sandbox re-validated at \`$VALIDATED_SHA\` - build passed. Ready for review." + if ! ERROR_OUTPUT=$(gh pr edit "$EXISTING_PR" --body-file promotion-pr-body.md 2>&1); then + echo "$ERROR_OUTPUT" + { + echo "promotion-error<<$DELIM" + echo "$ERROR_OUTPUT" + echo "$DELIM" + } >> "$GITHUB_OUTPUT" + exit 1 + fi + gh pr comment "$EXISTING_PR" --body "Sandbox re-validated at \`$VALIDATED_SHA\` - build passed. Ready for review." || true echo "pr-number=$EXISTING_PR" >> "$GITHUB_OUTPUT" else echo "Creating new promotion PR" - gh pr create \ - --base main \ - --head sandbox \ - --title "Promote sandbox to main ($(date -u +%Y-%m-%d))" \ - --body-file promotion-pr-body.md + if ! ERROR_OUTPUT=$(gh pr create --base main --head sandbox --title "Promote sandbox to main ($(date -u +%Y-%m-%d))" --body-file promotion-pr-body.md 2>&1); then + echo "$ERROR_OUTPUT" + { + echo "promotion-error<<$DELIM" + echo "$ERROR_OUTPUT" + echo "$DELIM" + } >> "$GITHUB_OUTPUT" + exit 1 + fi + echo "$ERROR_OUTPUT" + fi + + # Runs only when the PR create/update step above failed. + - name: Report promotion failure + if: failure() && steps.promote_pr.outcome == 'failure' + env: + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + PROMOTION_ERROR: ${{ steps.promote_pr.outputs.promotion-error }} + run: | + { + echo "The sandbox -> main promotion PR could not be created or updated" + echo "automatically in [this workflow run]($RUN_URL)." + echo + if echo "$PROMOTION_ERROR" | grep -qi "not permitted to create or approve pull requests"; then + echo "**Likely cause:** GitHub Actions is not allowed to create pull requests" + echo "in this repository." + echo + echo "**Fix:** go to **Settings > Actions > General > Workflow permissions**," + echo "enable **\"Allow GitHub Actions to create and approve pull requests\"**," + echo "save, then re-run this workflow (Actions tab > this workflow > Re-run" + echo "jobs), or just push again to \`sandbox\`." + echo + echo "Alternatively, add a repo secret named \`PROMOTION_PR_TOKEN\` containing a" + echo "personal access token with \"Pull requests: Read and write\" permission -" + echo "this workflow will automatically use it instead of the default token." + echo + fi + echo "
Error output" + echo + echo '```' + echo "$PROMOTION_ERROR" + echo '```' + echo + echo "
" + echo + echo "You can also create the PR manually in the meantime:" + echo + echo '```' + echo "gh pr create --base main --head sandbox --title \"Promote sandbox to main\"" + echo '```' + } > promotion-failure-body.md + + EXISTING=$(gh issue list --state open --json number,title \ + --jq ".[] | select(.title == \"$PR_FAILURE_ISSUE_TITLE\") | .number" | head -n1) + + if [ -n "$EXISTING" ]; then + gh issue comment "$EXISTING" --body-file promotion-failure-body.md + else + gh issue create --title "$PR_FAILURE_ISSUE_TITLE" --body-file promotion-failure-body.md --label bug fi - name: Request review from repo owner (notification) env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO_OWNER: ${{ github.repository_owner }} continue-on-error: true run: | @@ -133,3 +247,15 @@ jobs: if [ -n "$PR_NUMBER" ]; then gh pr edit "$PR_NUMBER" --add-reviewer "$REPO_OWNER" || true fi + + # Runs when PR create/update succeeds, to auto-resolve a previously filed failure issue. + - name: Close stale promotion-failure issue if now fixed + if: steps.promote_pr.outcome == 'success' + run: | + EXISTING=$(gh issue list --state open --json number,title \ + --jq ".[] | select(.title == \"$PR_FAILURE_ISSUE_TITLE\") | .number" | head -n1) + + if [ -n "$EXISTING" ]; then + gh issue comment "$EXISTING" --body "Promotion PR created/updated successfully at \`${{ github.sha }}\`. Closing." + gh issue close "$EXISTING" + fi From 66741da49b1ea3d3f24e5fcf20f1e83fc9cde0c8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Jul 2026 15:17:02 +0000 Subject: [PATCH 4/4] docs: document promotion workflow failure path and remediation Explains the two tracking-issue failure modes (build failed, PR create/update failed), the auto-close-on-success behavior, and the specific repo setting that needs to be enabled for automated PR creation to work. Co-authored-by: Thomas Bohn --- .cursor/rules/sandbox-to-main-promotion.mdc | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.cursor/rules/sandbox-to-main-promotion.mdc b/.cursor/rules/sandbox-to-main-promotion.mdc index acb72d0..b6fc498 100644 --- a/.cursor/rules/sandbox-to-main-promotion.mdc +++ b/.cursor/rules/sandbox-to-main-promotion.mdc @@ -33,6 +33,31 @@ Promotion to production is primarily handled by CI/CD, not by an agent: `sandbox`** — the workflow above handles validation, PR creation, and notification automatically. +## Failure path + +The workflow is expected to fail loudly and notify a human rather than fail +silently: + +- **Build fails** → the workflow opens/updates a GitHub Issue titled + "Sandbox promotion automation needs attention: build failed" with a link to + the run. No promotion PR is touched. The issue auto-closes the next time the + build passes. +- **PR create/update fails** → the workflow opens/updates a GitHub Issue titled + "Sandbox promotion automation needs attention: could not open/update PR" + with the captured error and remediation steps. The issue auto-closes the + next time the PR create/update succeeds. +- The most common cause of a PR-creation failure is the repo setting + **Settings → Actions → General → Workflow permissions → "Allow GitHub + Actions to create and approve pull requests"** being disabled (this is off + by default on new repos). The workflow detects this specific GraphQL error + and includes the exact fix in the issue body. As a fallback that doesn't + require flipping that repo-wide setting, add a repo secret named + `PROMOTION_PR_TOKEN` (a PAT with "Pull requests: Read and write" permission) + — the workflow automatically prefers it over the default token. +- If asked to investigate a failed run, check the Actions tab for the run logs + first, then check for an open tracking Issue with one of the titles above — + don't just re-run blindly without understanding why it failed. + ## When an agent should act directly Use this manual flow only if asked to promote directly, to check status, or if