1name: Nightly Release
2
3on:
4 push:
5 branches:
6 - master
7 workflow_dispatch:
8
9permissions:
10 contents: write
11
12jobs:
13 nightly:
14 runs-on: macos-latest
15 steps:
16 - name: Checkout
17 uses: actions/checkout@v6
18 with:
19 fetch-depth: 0
20
21 - name: Set up Go
22 uses: actions/setup-go@v6
23 with:
24 go-version: "1.26.4"
25
26 - name: Set up Zig
27 uses: goto-bus-stop/setup-zig@v2
28
29 - name: Set up libpcsclite for Linux cross-compilation
30 run: |
31 PCSC_DIR="$RUNNER_TEMP/pcsclite"
32 mkdir -p "$PCSC_DIR/include" "$PCSC_DIR/lib/pkgconfig"
33
34 # Download pcsc-lite headers from upstream
35 PCSC_URL="https://raw.githubusercontent.com/LudovicRousseau/PCSC/master/src/PCSC"
36 for header in winscard.h wintypes.h; do
37 curl -fsSL "$PCSC_URL/$header" -o "$PCSC_DIR/include/$header"
38 done
39 # pcsclite.h is generated from pcsclite.h.in — download and substitute the version placeholder
40 curl -fsSL "$PCSC_URL/pcsclite.h.in" -o "$PCSC_DIR/include/pcsclite.h"
41 sed -i '' 's/@VERSION@/1.9.0/' "$PCSC_DIR/include/pcsclite.h"
42
43 # Build stub library — the real libpcsclite is loaded at runtime on the user's system,
44 # but the linker needs symbols to resolve during cross-compilation.
45 cat > "$RUNNER_TEMP/pcsclite_stub.c" << 'STUB'
46 #include <stddef.h>
47 typedef long LONG;
48 typedef unsigned long DWORD;
49 typedef void *LPCVOID;
50 typedef char *LPSTR;
51 typedef const char *LPCSTR;
52 typedef unsigned char *LPBYTE;
53 typedef unsigned char BYTE;
54 typedef LONG SCARDCONTEXT;
55 typedef LONG SCARDHANDLE;
56 typedef struct { DWORD dwProtocol; DWORD cbPciLength; } SCARD_IO_REQUEST;
57 typedef struct { LPCSTR szReader; DWORD dwCurrentState; DWORD dwEventState; DWORD cbAtr; unsigned char rgbAtr[36]; void *pvUserData; } SCARD_READERSTATE;
58 LONG SCardEstablishContext(DWORD s, LPCVOID r1, LPCVOID r2, SCARDCONTEXT *c) { return 0; }
59 LONG SCardReleaseContext(SCARDCONTEXT c) { return 0; }
60 LONG SCardIsValidContext(SCARDCONTEXT c) { return 0; }
61 LONG SCardCancel(SCARDCONTEXT c) { return 0; }
62 LONG SCardConnect(SCARDCONTEXT c, LPCSTR r, DWORD s, DWORD p, SCARDHANDLE *h, DWORD *ap) { return 0; }
63 LONG SCardReconnect(SCARDHANDLE h, DWORD s, DWORD p, DWORD d, DWORD *ap) { return 0; }
64 LONG SCardDisconnect(SCARDHANDLE h, DWORD d) { return 0; }
65 LONG SCardBeginTransaction(SCARDHANDLE h) { return 0; }
66 LONG SCardEndTransaction(SCARDHANDLE h, DWORD d) { return 0; }
67 LONG SCardStatus(SCARDHANDLE h, LPSTR r, DWORD *rl, DWORD *s, DWORD *p, LPBYTE a, DWORD *al) { return 0; }
68 LONG SCardTransmit(SCARDHANDLE h, const SCARD_IO_REQUEST *si, const BYTE *s, DWORD sl, SCARD_IO_REQUEST *ri, BYTE *r, DWORD *rl) { return 0; }
69 LONG SCardControl(SCARDHANDLE h, DWORD c, LPCVOID i, DWORD il, void *o, DWORD ol, DWORD *br) { return 0; }
70 LONG SCardGetAttrib(SCARDHANDLE h, DWORD a, LPBYTE b, DWORD *bl) { return 0; }
71 LONG SCardSetAttrib(SCARDHANDLE h, DWORD a, const BYTE *b, DWORD bl) { return 0; }
72 LONG SCardListReaders(SCARDCONTEXT c, LPCSTR g, LPSTR r, DWORD *rl) { return 0; }
73 LONG SCardListReaderGroups(SCARDCONTEXT c, LPSTR g, DWORD *gl) { return 0; }
74 LONG SCardGetStatusChange(SCARDCONTEXT c, DWORD t, SCARD_READERSTATE *s, DWORD n) { return 0; }
75 LONG SCardFreeMemory(SCARDCONTEXT c, LPCVOID m) { return 0; }
76 const char *pcsc_stringify_error(LONG e) { return "stub"; }
77 STUB
78
79 zig cc -c -target x86_64-linux-musl -o "$RUNNER_TEMP/pcsclite_stub_amd64.o" "$RUNNER_TEMP/pcsclite_stub.c"
80 zig cc -c -target aarch64-linux-musl -o "$RUNNER_TEMP/pcsclite_stub_arm64.o" "$RUNNER_TEMP/pcsclite_stub.c"
81
82 mkdir -p "$PCSC_DIR/lib/amd64" "$PCSC_DIR/lib/arm64"
83 ar rcs "$PCSC_DIR/lib/amd64/libpcsclite.a" "$RUNNER_TEMP/pcsclite_stub_amd64.o"
84 ar rcs "$PCSC_DIR/lib/arm64/libpcsclite.a" "$RUNNER_TEMP/pcsclite_stub_arm64.o"
85
86 # Create pkg-config file — Libs will be overridden per-arch via CGO_LDFLAGS in goreleaser
87 cat > "$PCSC_DIR/lib/pkgconfig/libpcsclite.pc" << EOF
88 Name: libpcsclite
89 Description: PC/SC Lite
90 Version: 1.9.0
91 Cflags: -I$PCSC_DIR/include
92 Libs: -L$PCSC_DIR/lib/amd64 -lpcsclite
93 EOF
94
95 echo "PKG_CONFIG_PATH=$PCSC_DIR/lib/pkgconfig" >> $GITHUB_ENV
96 echo "PCSC_DIR=$PCSC_DIR" >> $GITHUB_ENV
97
98 - name: Get macOS SDK path
99 id: macos_sdk
100 run: echo "path=$(xcrun --show-sdk-path)" >> $GITHUB_OUTPUT
101
102 - name: Build with GoReleaser (snapshot)
103 uses: goreleaser/goreleaser-action@v7
104 with:
105 version: latest
106 args: release --snapshot --clean --config .goreleaser.nightly.yml
107 env:
108 SDK_PATH: ${{ steps.macos_sdk.outputs.path }}
109
110 - name: Delete existing nightly release
111 env:
112 GH_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
113 run: |
114 gh release delete nightlyv0 --yes --cleanup-tag || true
115
116 - name: Create nightly release
117 env:
118 GH_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
119 run: |
120 SHORT_SHA="$(git rev-parse --short HEAD)"
121 LATEST_TAG="$(git describe --tags --abbrev=0 --exclude='nightlyv*' --exclude='preview*' 2>/dev/null || echo '')"
122
123 NOTES_FILE="$RUNNER_TEMP/release_notes.md"
124 {
125 echo "> [!WARNING]"
126 echo "> This is an automated nightly build from the latest \`master\` commit (\`$SHORT_SHA\`). It may be unstable — use stable releases for production."
127 echo ""
128 echo "### Install"
129 echo ""
130 echo '- **Homebrew:** `brew install floatpane/matcha/matcha-nightly`'
131 echo '- **Snapcraft:** `snap install matcha --beta`'
132
133 if [ -n "$LATEST_TAG" ]; then
134 echo ""
135 echo "### Changes since $LATEST_TAG"
136 echo ""
137 git log --pretty=format:"- %s (%h)" "$LATEST_TAG..HEAD"
138 echo ""
139 fi
140 } > "$NOTES_FILE"
141
142 gh release create nightlyv0 dist/*.tar.gz dist/*.zip dist/checksums.txt \
143 --title "Nightly Build ($SHORT_SHA)" \
144 --notes-file "$NOTES_FILE" \
145 --prerelease \
146 --target master
147
148 - name: Update Homebrew tap
149 env:
150 GH_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
151 run: |
152 VERSION="nightly-$(git rev-parse --short HEAD)"
153 DARWIN_AMD64_SHA=$(shasum -a 256 dist/matcha_nightly_darwin_amd64.tar.gz | cut -d ' ' -f 1)
154 DARWIN_ARM64_SHA=$(shasum -a 256 dist/matcha_nightly_darwin_arm64.tar.gz | cut -d ' ' -f 1)
155 LINUX_AMD64_SHA=$(shasum -a 256 dist/matcha_nightly_linux_amd64.tar.gz | cut -d ' ' -f 1)
156 LINUX_ARM64_SHA=$(shasum -a 256 dist/matcha_nightly_linux_arm64.tar.gz | cut -d ' ' -f 1)
157 BASE_URL="https://github.com/floatpane/matcha/releases/download/nightlyv0"
158
159 cat > /tmp/matcha-nightly.rb << EOF
160 class MatchaNightly < Formula
161 desc "A beautiful and functional email client for your terminal (nightly)"
162 homepage "https://matcha.email"
163 version "$VERSION"
164
165 on_macos do
166 if Hardware::CPU.intel?
167 url "$BASE_URL/matcha_nightly_darwin_amd64.tar.gz"
168 sha256 "$DARWIN_AMD64_SHA"
169 else
170 url "$BASE_URL/matcha_nightly_darwin_arm64.tar.gz"
171 sha256 "$DARWIN_ARM64_SHA"
172 end
173 end
174
175 on_linux do
176 if Hardware::CPU.intel?
177 url "$BASE_URL/matcha_nightly_linux_amd64.tar.gz"
178 sha256 "$LINUX_AMD64_SHA"
179 else
180 url "$BASE_URL/matcha_nightly_linux_arm64.tar.gz"
181 sha256 "$LINUX_ARM64_SHA"
182 end
183 end
184
185 def install
186 bin.install "matcha"
187 end
188
189 test do
190 system "#{bin}/matcha", "--version"
191 end
192 end
193 EOF
194
195 # Clone the tap repo and push the nightly formula
196 git clone "https://x-access-token:${GH_TOKEN}@github.com/floatpane/homebrew-matcha.git" /tmp/homebrew-matcha
197 cp /tmp/matcha-nightly.rb /tmp/homebrew-matcha/matcha-nightly.rb
198 cd /tmp/homebrew-matcha
199 git config user.name "Floatpane Bot"
200 git config user.email "us@floatpane.com"
201 git add matcha-nightly.rb
202 git diff --cached --quiet || (git commit -m "Update matcha-nightly to $VERSION" && git push)
203
204 - name: Update Nix flake tap
205 env:
206 GH_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
207 run: |
208 VERSION="nightly-$(git rev-parse --short HEAD)"
209 DARWIN_AMD64_SHA=$(shasum -a 256 dist/matcha_nightly_darwin_amd64.tar.gz | cut -d ' ' -f 1)
210 DARWIN_ARM64_SHA=$(shasum -a 256 dist/matcha_nightly_darwin_arm64.tar.gz | cut -d ' ' -f 1)
211 LINUX_AMD64_SHA=$(shasum -a 256 dist/matcha_nightly_linux_amd64.tar.gz | cut -d ' ' -f 1)
212 LINUX_ARM64_SHA=$(shasum -a 256 dist/matcha_nightly_linux_arm64.tar.gz | cut -d ' ' -f 1)
213 BASE_URL="https://github.com/floatpane/matcha/releases/download/nightlyv0"
214
215 # Convert hex sha256 to nix SRI base64 form
216 to_sri() { printf 'sha256-%s' "$(printf '%s' "$1" | xxd -r -p | base64)"; }
217 DARWIN_AMD64_SRI=$(to_sri "$DARWIN_AMD64_SHA")
218 DARWIN_ARM64_SRI=$(to_sri "$DARWIN_ARM64_SHA")
219 LINUX_AMD64_SRI=$(to_sri "$LINUX_AMD64_SHA")
220 LINUX_ARM64_SRI=$(to_sri "$LINUX_ARM64_SHA")
221
222 mkdir -p /tmp/nix-pkg
223 cat > /tmp/nix-pkg/default.nix << EOF
224 { stdenvNoCC, fetchurl, lib }:
225 let
226 sources = {
227 x86_64-darwin = {
228 url = "$BASE_URL/matcha_nightly_darwin_amd64.tar.gz";
229 hash = "$DARWIN_AMD64_SRI";
230 };
231 aarch64-darwin = {
232 url = "$BASE_URL/matcha_nightly_darwin_arm64.tar.gz";
233 hash = "$DARWIN_ARM64_SRI";
234 };
235 x86_64-linux = {
236 url = "$BASE_URL/matcha_nightly_linux_amd64.tar.gz";
237 hash = "$LINUX_AMD64_SRI";
238 };
239 aarch64-linux = {
240 url = "$BASE_URL/matcha_nightly_linux_arm64.tar.gz";
241 hash = "$LINUX_ARM64_SRI";
242 };
243 };
244 src = sources.\${stdenvNoCC.hostPlatform.system}
245 or (throw "matcha-nightly: unsupported system \${stdenvNoCC.hostPlatform.system}");
246 in
247 stdenvNoCC.mkDerivation {
248 pname = "matcha-nightly";
249 version = "$VERSION";
250 src = fetchurl src;
251 sourceRoot = ".";
252 installPhase = ''
253 runHook preInstall
254 install -Dm755 matcha \$out/bin/matcha
255 runHook postInstall
256 '';
257 meta = {
258 description = "Beautiful and functional email client for the terminal (nightly)";
259 homepage = "https://matcha.email";
260 license = lib.licenses.mit;
261 mainProgram = "matcha";
262 platforms = builtins.attrNames sources;
263 };
264 }
265 EOF
266
267 cat > /tmp/nix-pkg/flake.nix << 'EOF'
268 {
269 description = "matcha email client — nightly";
270 inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
271 outputs = { self, nixpkgs }:
272 let
273 systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
274 forAll = nixpkgs.lib.genAttrs systems;
275 in {
276 packages = forAll (system: {
277 default = nixpkgs.legacyPackages.${system}.callPackage ./default.nix { };
278 });
279 };
280 }
281 EOF
282
283 git clone "https://x-access-token:${GH_TOKEN}@github.com/floatpane/nix-matcha.git" /tmp/nix-matcha
284 cd /tmp/nix-matcha
285 git checkout nightly 2>/dev/null || git checkout -b nightly
286 mkdir -p nightly
287 cp /tmp/nix-pkg/default.nix nightly/default.nix
288 cp /tmp/nix-pkg/flake.nix nightly/flake.nix
289 git config user.name "Floatpane Bot"
290 git config user.email "us@floatpane.com"
291 git add nightly/default.nix nightly/flake.nix
292 git diff --cached --quiet || (git commit -m "matcha-nightly: bump to $VERSION" && git push -u origin nightly)
293
294 snapcraft:
295 runs-on: ${{ matrix.runner }}
296 needs: nightly
297 strategy:
298 matrix:
299 include:
300 - arch: amd64
301 runner: ubuntu-latest
302 - arch: arm64
303 runner: ubuntu-24.04-arm
304 steps:
305 - name: Checkout
306 uses: actions/checkout@v6
307 with:
308 fetch-depth: 0
309
310 - name: Install Snapcraft and LXD
311 run: |
312 sudo snap install snapcraft --classic
313 sudo snap install lxd
314 sudo lxd init --auto
315 sudo iptables -P FORWARD ACCEPT
316 sudo usermod -aG lxd $USER
317
318 - name: Build snap
319 run: |
320 touch .nightly
321 sg lxd -c 'snapcraft pack --use-lxd --build-for=${{ matrix.arch }}'
322
323 - name: Upload snap
324 env:
325 SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
326 run: snapcraft upload --release=beta *.snap