11name : Release
22
33on :
4+ push :
5+ tags :
6+ - ' v*'
47 workflow_dispatch :
58
6- concurrency :
7- group : release
8- cancel-in-progress : false
9-
109permissions :
1110 contents : write
12- issues : write
13- pull-requests : write
1411
1512jobs :
16- release :
13+ # ── Validate tag matches package.json ─────────
14+ validate :
1715 runs-on : ubuntu-latest
18- if : github.ref == 'refs/heads/main'
16+ if : startsWith(github.ref, 'refs/tags/')
17+ steps :
18+ - name : Checkout
19+ uses : actions/checkout@v4
20+
21+ - name : Validate tag matches package.json
22+ env :
23+ GIT_REF : ${{ github.ref }}
24+ run : |
25+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
26+ TAG_VERSION=${GIT_REF#refs/tags/v}
27+
28+ if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then
29+ echo "::error::Tag version (${TAG_VERSION}) does not match package.json (${PACKAGE_VERSION})"
30+ exit 1
31+ fi
32+
33+ echo "✓ Tag v${TAG_VERSION} matches package.json ${PACKAGE_VERSION}"
34+
35+ # ── Build for all platforms ───────────────────
36+ build :
37+ needs : [validate]
38+ if : always() && (needs.validate.result == 'success' || needs.validate.result == 'skipped')
39+ strategy :
40+ matrix :
41+ include :
42+ - os : macos-latest
43+ platform : mac
44+ - os : windows-latest
45+ platform : win
46+ - os : ubuntu-latest
47+ platform : linux
48+
49+ runs-on : ${{ matrix.os }}
50+
1951 steps :
2052 - name : Checkout
2153 uses : actions/checkout@v4
22- with :
23- fetch-depth : 0
24- token : ${{ secrets.GH_TOKEN }}
2554
2655 - name : Setup pnpm
2756 uses : pnpm/action-setup@v4
@@ -32,14 +61,139 @@ jobs:
3261 node-version : ' 22'
3362 cache : ' pnpm'
3463
64+ - name : Force HTTPS for GitHub git dependencies
65+ shell : bash
66+ run : git config --global 'url.https://github.com/.insteadOf' 'git@github.com:'
67+
3568 - name : Install dependencies
36- run : pnpm install --frozen-lockfile
69+ run : pnpm install
70+
71+ - name : Build packages
72+ run : pnpm build
73+
74+ - name : Build Electron app
75+ working-directory : apps/desktop
76+ run : pnpm build
77+
78+ - name : Build distributables (macOS)
79+ if : matrix.platform == 'mac'
80+ working-directory : apps/desktop
81+ env :
82+ GH_TOKEN : ${{ secrets.GH_TOKEN }}
83+ CSC_LINK : ${{ secrets.CSC_LINK }}
84+ CSC_KEY_PASSWORD : ${{ secrets.CSC_KEY_PASSWORD }}
85+ APPLE_ID : ${{ secrets.APPLE_ID }}
86+ APPLE_APP_SPECIFIC_PASSWORD : ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
87+ APPLE_TEAM_ID : ${{ secrets.APPLE_TEAM_ID }}
88+ run : pnpm dist:mac --publish always
3789
38- - name : Run semantic-release
90+ - name : Build distributables (Windows)
91+ if : matrix.platform == 'win'
92+ working-directory : apps/desktop
3993 env :
40- GITHUB_TOKEN : ${{ secrets.GH_TOKEN }}
41- GIT_AUTHOR_NAME : github-actions[bot]
42- GIT_AUTHOR_EMAIL : github-actions[bot]@users.noreply.github.com
43- GIT_COMMITTER_NAME : github-actions[bot]
44- GIT_COMMITTER_EMAIL : github-actions[bot]@users.noreply.github.com
45- run : npx semantic-release
94+ GH_TOKEN : ${{ secrets.GH_TOKEN }}
95+ run : pnpm dist:win --publish always
96+
97+ - name : Build distributables (Linux)
98+ if : matrix.platform == 'linux'
99+ working-directory : apps/desktop
100+ env :
101+ GH_TOKEN : ${{ secrets.GH_TOKEN }}
102+ run : pnpm dist:linux --publish always
103+
104+ - name : Upload artifacts
105+ uses : actions/upload-artifact@v4
106+ with :
107+ name : ${{ matrix.platform }}-build
108+ path : |
109+ apps/desktop/release/*.dmg
110+ apps/desktop/release/*.zip
111+ apps/desktop/release/*.exe
112+ apps/desktop/release/*.AppImage
113+ apps/desktop/release/*.deb
114+ apps/desktop/release/latest*.yml
115+ apps/desktop/release/*.blockmap
116+ if-no-files-found : ignore
117+
118+ release :
119+ needs : build
120+ runs-on : ubuntu-latest
121+ if : startsWith(github.ref, 'refs/tags/')
122+
123+ steps :
124+ - name : Download all artifacts
125+ uses : actions/download-artifact@v4
126+ with :
127+ path : artifacts
128+
129+ - name : Create Release
130+ uses : softprops/action-gh-release@v2
131+ with :
132+ draft : true
133+ files : |
134+ artifacts/**/*.dmg
135+ artifacts/**/*.zip
136+ artifacts/**/*.exe
137+ artifacts/**/*.AppImage
138+ artifacts/**/*.deb
139+ artifacts/**/latest*.yml
140+ artifacts/**/*.blockmap
141+ env :
142+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
143+
144+ tweet :
145+ needs : release
146+ runs-on : ubuntu-latest
147+ if : startsWith(github.ref, 'refs/tags/')
148+
149+ steps :
150+ - name : Extract version from tag
151+ id : version
152+ run : echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
153+
154+ - name : Fetch release body
155+ id : release
156+ env :
157+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
158+ run : |
159+ BODY=$(gh release view "${{ steps.version.outputs.tag }}" --repo "${{ github.repository }}" --json body --jq '.body // ""')
160+ echo "body<<RELEASE_EOF" >> "$GITHUB_OUTPUT"
161+ echo "$BODY" >> "$GITHUB_OUTPUT"
162+ echo "RELEASE_EOF" >> "$GITHUB_OUTPUT"
163+
164+ - name : Compose tweet
165+ id : tweet
166+ run : |
167+ TAG="${{ steps.version.outputs.tag }}"
168+ BODY="${{ steps.release.outputs.body }}"
169+ LINK="https://github.com/${{ github.repository }}/releases/tag/${TAG}"
170+ SUFFIX=$'\n\n'"📥 ${LINK}"$'\n'"#readied #markdown #devtools"
171+ HEADER="🚀 Readied ${TAG}"$'\n\n'
172+
173+ # Calculate available space for release body
174+ # 280 max - header - suffix = space for body
175+ MAX=280
176+ FIXED_LEN=$(( ${#HEADER} + ${#SUFFIX} ))
177+ BODY_MAX=$(( MAX - FIXED_LEN ))
178+
179+ # Strip markdown formatting, take first lines, trim to fit
180+ CLEAN=$(echo "$BODY" | sed 's/^##* //' | sed 's/\*\*//g' | sed '/^$/d' | head -8)
181+
182+ if [ ${#CLEAN} -gt $BODY_MAX ]; then
183+ CLEAN="${CLEAN:0:$(( BODY_MAX - 1 ))}…"
184+ fi
185+
186+ TWEET="${HEADER}${CLEAN}${SUFFIX}"
187+
188+ echo "text<<TWEET_EOF" >> "$GITHUB_OUTPUT"
189+ echo "$TWEET" >> "$GITHUB_OUTPUT"
190+ echo "TWEET_EOF" >> "$GITHUB_OUTPUT"
191+
192+ - name : Post tweet
193+ uses : dart-actions/tweet@v1
194+ with :
195+ consumer-key : ${{ secrets.TWITTER_API_KEY }}
196+ consumer-secret : ${{ secrets.TWITTER_API_SECRET }}
197+ access-token : ${{ secrets.TWITTER_ACCESS_TOKEN }}
198+ access-token-secret : ${{ secrets.TWITTER_ACCESS_SECRET }}
199+ text : ${{ steps.tweet.outputs.text }}
0 commit comments