1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
name: Release
on:
push:
tags:
- 'v*'
# A tag is immutable, so a re-tag should cancel any in-flight build for the
# same ref instead of running both to completion.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
binary: vision-squeezer-mcp
asset: vision-squeezer-mcp-macos-arm64
- os: macos-15-intel
target: x86_64-apple-darwin
binary: vision-squeezer-mcp
asset: vision-squeezer-mcp-macos-x86_64
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
binary: vision-squeezer-mcp
asset: vision-squeezer-mcp-linux-x86_64
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
binary: vision-squeezer-mcp
asset: vision-squeezer-mcp-linux-arm64
- os: windows-latest
target: x86_64-pc-windows-msvc
binary: vision-squeezer-mcp.exe
asset: vision-squeezer-mcp-windows-x86_64.exe
- os: windows-latest
target: aarch64-pc-windows-msvc
binary: vision-squeezer-mcp.exe
asset: vision-squeezer-mcp-windows-arm64.exe
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- name: Install cross-compiler (Linux arm64)
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Build
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
run: cargo build --release --bin vision-squeezer-mcp --target ${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.asset }}
path: target/${{ matrix.target }}/release/${{ matrix.binary }}
if-no-files-found: error
# Artifacts are only needed long enough for the `release` job to pull
# them and attach to the GitHub Release. The default 90-day retention
# wastes storage quota — 7 days is well past any retry window.
retention-days: 7
# GitHub Release + crates.io + npm in a single runner. Each individual
# publish step is well under a minute; running them in one job saves the
# ~30s × 2 runner spin-ups vs the old three-job split, and lets npm's
# postinstall safely find the freshly-attached GH release binaries
# (npm publish only happens after `gh release` attaches assets).
#
# Step order matters: cargo publish runs BEFORE artifact download so its
# dirty-tree check sees a clean working directory. Downloading artifacts
# first leaves `artifacts/` and `dist/` as untracked files and cargo
# refuses to publish without `--allow-dirty`.
publish:
name: Publish (GitHub Release + crates.io + npm)
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Publish to crates.io (skip if version exists)
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
# `--locked` forbids cargo from regenerating Cargo.lock during
# publish (which would dirty the working tree and trip the
# built-in cleanliness check). The lockfile must be committed
# in sync with Cargo.toml — bump both in every version commit.
run: |
set +e
OUTPUT=$(cargo publish --locked 2>&1)
STATUS=$?
echo "$OUTPUT"
if [ $STATUS -ne 0 ] && echo "$OUTPUT" | grep -q "already exists"; then
echo "Version already on crates.io — skipping."
exit 0
fi
exit $STATUS
- uses: actions/setup-node@v5
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Download all artifacts
uses: actions/download-artifact@v5
with:
path: artifacts/
- name: Rename binaries
run: |
mkdir -p dist
for dir in artifacts/*/; do
name=$(basename "$dir")
src="${dir}vision-squeezer-mcp.exe"
[ -f "$src" ] || src="${dir}vision-squeezer-mcp"
cp "$src" "dist/${name}"
done
- name: Extract changelog for this version
id: changelog
run: |
VERSION="${GITHUB_REF_NAME#v}"
BODY=$(awk "/^## \[${VERSION}\]/{found=1; next} found && /^## \[/{exit} found{print}" CHANGELOG.md)
echo "body<<EOF" >> "$GITHUB_OUTPUT"
echo "$BODY" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: dist/vision-squeezer-mcp-*
body: ${{ steps.changelog.outputs.body }}
- name: Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}