| 1 | name: Squad Promote |
| 2 | |
| 3 | on: |
| 4 | workflow_dispatch: |
| 5 | inputs: |
| 6 | dry_run: |
| 7 | description: 'Dry run — show what would happen without pushing' |
| 8 | required: false |
| 9 | default: 'false' |
| 10 | type: choice |
| 11 | options: ['false', 'true'] |
| 12 | |
| 13 | permissions: |
| 14 | contents: write |
| 15 | |
| 16 | jobs: |
| 17 | dev-to-preview: |
| 18 | name: Promote dev → preview |
| 19 | runs-on: ubuntu-latest |
| 20 | steps: |
| 21 | - uses: actions/checkout@v4 |
| 22 | with: |
| 23 | fetch-depth: 0 |
| 24 | token: ${{ secrets.GITHUB_TOKEN }} |
| 25 | |
| 26 | - name: Configure git |
| 27 | run: | |
| 28 | git config user.name "github-actions[bot]" |
| 29 | git config user.email "github-actions[bot]@users.noreply.github.com" |
| 30 | |
| 31 | - name: Fetch all branches |
| 32 | run: git fetch --all |
| 33 | |
| 34 | - name: Show current state (dry run info) |
| 35 | run: | |
| 36 | echo "=== dev HEAD ===" && git log origin/dev -1 --oneline |
| 37 | echo "=== preview HEAD ===" && git log origin/preview -1 --oneline |
| 38 | echo "=== Files that would be stripped ===" |
| 39 | git diff origin/preview..origin/dev --name-only | grep -E "^(\.(ai-team|squad|ai-team-templates)|team-docs/|docs/proposals/)" || echo "(none)" |
| 40 | |
| 41 | - name: Merge dev → preview (strip forbidden paths) |
| 42 | if: ${{ inputs.dry_run == 'false' }} |
| 43 | run: | |
| 44 | git checkout preview |
| 45 | git merge origin/dev --no-commit --no-ff -X theirs || true |
| 46 | |
| 47 | # Strip forbidden paths from merge commit |
| 48 | git rm -rf --cached --ignore-unmatch \ |
| 49 | .ai-team/ \ |
| 50 | .squad/ \ |
| 51 | .ai-team-templates/ \ |
| 52 | team-docs/ \ |
| 53 | "docs/proposals/" || true |
| 54 | |
| 55 | # Commit if there are staged changes |
| 56 | if ! git diff --cached --quiet; then |
| 57 | git commit -m "chore: promote dev → preview (v$(node -e "console.log(require('./package.json').version)"))" |
| 58 | git push origin preview |
| 59 | echo "✅ Pushed preview branch" |
| 60 | else |
| 61 | echo "ℹ️ Nothing to commit — preview is already up to date" |
| 62 | fi |
| 63 | |
| 64 | - name: Dry run complete |
| 65 | if: ${{ inputs.dry_run == 'true' }} |
| 66 | run: echo "🔍 Dry run complete — no changes pushed." |
| 67 | |
| 68 | preview-to-main: |
| 69 | name: Promote preview → main (release) |
| 70 | needs: dev-to-preview |
| 71 | runs-on: ubuntu-latest |
| 72 | steps: |
| 73 | - uses: actions/checkout@v4 |
| 74 | with: |
| 75 | fetch-depth: 0 |
| 76 | token: ${{ secrets.GITHUB_TOKEN }} |
| 77 | |
| 78 | - name: Configure git |
| 79 | run: | |
| 80 | git config user.name "github-actions[bot]" |
| 81 | git config user.email "github-actions[bot]@users.noreply.github.com" |
| 82 | |
| 83 | - name: Fetch all branches |
| 84 | run: git fetch --all |
| 85 | |
| 86 | - name: Show current state |
| 87 | run: | |
| 88 | echo "=== preview HEAD ===" && git log origin/preview -1 --oneline |
| 89 | echo "=== main HEAD ===" && git log origin/main -1 --oneline |
| 90 | echo "=== Version ===" && node -e "console.log('v' + require('./package.json').version)" |
| 91 | |
| 92 | - name: Validate preview is release-ready |
| 93 | run: | |
| 94 | git checkout preview |
| 95 | VERSION=$(node -e "console.log(require('./package.json').version)") |
| 96 | if ! grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then |
| 97 | echo "::error::Version $VERSION not found in CHANGELOG.md — update before releasing" |
| 98 | exit 1 |
| 99 | fi |
| 100 | echo "✅ Version $VERSION has CHANGELOG entry" |
| 101 | |
| 102 | # Verify no forbidden files on preview |
| 103 | FORBIDDEN=$(git ls-files | grep -E "^(\.(ai-team|squad|ai-team-templates)/|team-docs/|docs/proposals/)" || true) |
| 104 | if [ -n "$FORBIDDEN" ]; then |
| 105 | echo "::error::Forbidden files found on preview: $FORBIDDEN" |
| 106 | exit 1 |
| 107 | fi |
| 108 | echo "✅ No forbidden files on preview" |
| 109 | |
| 110 | - name: Merge preview → main |
| 111 | if: ${{ inputs.dry_run == 'false' }} |
| 112 | run: | |
| 113 | git checkout main |
| 114 | git merge origin/preview --no-ff -m "chore: promote preview → main (v$(node -e "console.log(require('./package.json').version)"))" |
| 115 | git push origin main |
| 116 | echo "✅ Pushed main — squad-release.yml will tag and publish the release" |
| 117 | |
| 118 | - name: Dry run complete |
| 119 | if: ${{ inputs.dry_run == 'true' }} |
| 120 | run: echo "🔍 Dry run complete — no changes pushed." |