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