55 tags :
66 - " v*"
77
8+ permissions :
9+ contents : write
10+ packages : write
11+
812jobs :
9- build-and-push :
10- name : Build & Push Docker Images
13+ # ── Job 1: Validate tag format ─────────────────────────────────────
14+ validate-tag :
15+ name : Validate Tag
16+ runs-on : ubuntu-latest
17+ outputs :
18+ version : ${{ steps.parse.outputs.version }}
19+ is_prerelease : ${{ steps.parse.outputs.is_prerelease }}
20+ steps :
21+ - name : Parse and validate tag
22+ id : parse
23+ run : |
24+ TAG="${GITHUB_REF#refs/tags/}"
25+ echo "Tag: $TAG"
26+
27+ if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
28+ echo "::error::Invalid tag format: $TAG (expected v<major>.<minor>.<patch>[-prerelease])"
29+ exit 1
30+ fi
31+
32+ VERSION="${TAG#v}"
33+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
34+
35+ if [[ "$VERSION" == *-* ]]; then
36+ echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
37+ echo "Pre-release detected: $VERSION"
38+ else
39+ echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
40+ echo "Stable release: $VERSION"
41+ fi
42+
43+ # ── Job 2: Full CI gate ─────────────────────────────────────────────
44+ ci :
45+ name : CI Gate
46+ needs : [validate-tag]
1147 runs-on : ubuntu-latest
12- permissions :
13- contents : read
14- packages : write
48+ steps :
49+ - uses : actions/checkout@v6
50+
51+ - name : Set up Node.js
52+ uses : actions/setup-node@v6
53+ with :
54+ node-version : " 24"
55+
56+ - name : Install pnpm
57+ uses : pnpm/action-setup@v4
1558
59+ - name : Get pnpm store directory
60+ id : pnpm-cache
61+ run : echo "store=$(pnpm store path)" >> "$GITHUB_OUTPUT"
62+
63+ - name : Cache pnpm store
64+ uses : actions/cache@v5
65+ with :
66+ path : ${{ steps.pnpm-cache.outputs.store }}
67+ key : ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
68+ restore-keys : |
69+ ${{ runner.os }}-pnpm-
70+
71+ - name : Install dependencies
72+ run : pnpm install
73+
74+ - name : Format check
75+ run : pnpm format:check
76+
77+ - name : Generate Prisma client
78+ run : pnpm db:generate
79+
80+ - name : Type check
81+ run : pnpm typecheck
82+
83+ - name : Lint
84+ run : pnpm lint
85+
86+ - name : Unit tests
87+ run : pnpm test
88+
89+ - name : Production build
90+ run : pnpm build
91+
92+ # ── Job 3: Generate changelog ───────────────────────────────────────
93+ generate-changelog :
94+ name : Generate Changelog
95+ needs : [validate-tag]
96+ runs-on : ubuntu-latest
97+ outputs :
98+ changelog_file : changelog-${{ needs.validate-tag.outputs.version }}.md
99+ steps :
100+ - uses : actions/checkout@v6
101+ with :
102+ fetch-depth : 0
103+
104+ - name : Generate changelog
105+ run : |
106+ chmod +x .github/scripts/generate-changelog.sh
107+ .github/scripts/generate-changelog.sh \
108+ "${{ needs.validate-tag.outputs.version }}" \
109+ "changelog-${{ needs.validate-tag.outputs.version }}.md"
110+
111+ - name : Upload changelog artifact
112+ uses : actions/upload-artifact@v4
113+ with :
114+ name : changelog
115+ path : changelog-${{ needs.validate-tag.outputs.version }}.md
116+
117+ # ── Job 4: Build and push Docker images ─────────────────────────────
118+ docker-images :
119+ name : Docker Images
120+ needs : [validate-tag, ci]
121+ runs-on : ubuntu-latest
122+ strategy :
123+ matrix :
124+ include :
125+ - app : api
126+ dockerfile : apps/api/Dockerfile
127+ - app : web
128+ dockerfile : apps/web/Dockerfile
16129 steps :
17130 - uses : actions/checkout@v6
18131
@@ -26,30 +139,77 @@ jobs:
26139 username : ${{ github.actor }}
27140 password : ${{ secrets.GITHUB_TOKEN }}
28141
29- - name : Extract version from tag
30- id : version
31- run : echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
142+ - name : Generate image tags
143+ id : tags
144+ run : |
145+ VERSION="${{ needs.validate-tag.outputs.version }}"
146+ IS_PRE="${{ needs.validate-tag.outputs.is_prerelease }}"
147+ IMAGE="ghcr.io/${{ github.repository }}/${{ matrix.app }}"
148+
149+ TAGS="${IMAGE}:${VERSION}"
150+
151+ if [[ "$IS_PRE" == "false" ]]; then
152+ MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
153+ TAGS="${TAGS},${IMAGE}:${MAJOR_MINOR},${IMAGE}:latest"
154+ fi
32155
33- - name : Build and push API image
156+ echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
157+
158+ - name : Build and push
34159 uses : docker/build-push-action@v6
35160 with :
36161 context : .
37- file : apps/api/Dockerfile
162+ file : ${{ matrix.dockerfile }}
38163 push : true
39- tags : |
40- ghcr.io/${{ github.repository }}/api:${{ steps.version.outputs.version }}
41- ghcr.io/${{ github.repository }}/api:latest
42- cache-from : type=gha
43- cache-to : type=gha,mode=max
164+ tags : ${{ steps.tags.outputs.tags }}
165+ cache-from : type=gha,scope=${{ matrix.app }}
166+ cache-to : type=gha,mode=max,scope=${{ matrix.app }}
44167
45- - name : Build and push Web image
46- uses : docker/build-push-action@v6
168+ # ── Job 5: Create GitHub Release + update CHANGELOG.md ─────────────
169+ create-release :
170+ name : Create Release
171+ needs : [validate-tag, ci, generate-changelog, docker-images]
172+ runs-on : ubuntu-latest
173+ steps :
174+ - uses : actions/checkout@v6
47175 with :
48- context : .
49- file : apps/web/Dockerfile
50- push : true
51- tags : |
52- ghcr.io/${{ github.repository }}/web:${{ steps.version.outputs.version }}
53- ghcr.io/${{ github.repository }}/web:latest
54- cache-from : type=gha
55- cache-to : type=gha,mode=max
176+ fetch-depth : 0
177+ token : ${{ secrets.GITHUB_TOKEN }}
178+
179+ - name : Download changelog artifact
180+ uses : actions/download-artifact@v4
181+ with :
182+ name : changelog
183+
184+ - name : Create GitHub Release
185+ uses : softprops/action-gh-release@v2
186+ with :
187+ body_path : changelog-${{ needs.validate-tag.outputs.version }}.md
188+ prerelease : ${{ needs.validate-tag.outputs.is_prerelease == 'true' }}
189+ generate_release_notes : false
190+
191+ - name : Update CHANGELOG.md
192+ run : |
193+ VERSION="${{ needs.validate-tag.outputs.version }}"
194+ CHANGELOG_FILE="changelog-${VERSION}.md"
195+ MARKER="<!-- This file is auto-maintained by the release workflow on each tag. -->"
196+
197+ if grep -q "$MARKER" CHANGELOG.md; then
198+ # Insert new entry after the marker line
199+ sed -i "/$MARKER/r $CHANGELOG_FILE" CHANGELOG.md
200+ # Add a blank line after the marker before the new content
201+ sed -i "/$MARKER/a\\" CHANGELOG.md
202+ else
203+ # Append to end of file
204+ echo "" >> CHANGELOG.md
205+ cat "$CHANGELOG_FILE" >> CHANGELOG.md
206+ fi
207+
208+ - name : Push CHANGELOG.md update
209+ run : |
210+ git config user.name "github-actions[bot]"
211+ git config user.email "github-actions[bot]@users.noreply.github.com"
212+ git add CHANGELOG.md
213+ git diff --cached --quiet && exit 0
214+ git commit -m "docs: update CHANGELOG.md for v${{ needs.validate-tag.outputs.version }} [skip ci]"
215+ git push origin HEAD:main
0 commit comments