Skip to content

Commit 58bcdde

Browse files
author
Teodor Calin
committed
Fan out release to PyPI, npm, and Homebrew
1 parent f137d88 commit 58bcdde

1 file changed

Lines changed: 192 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,195 @@ jobs:
189189
generate_release_notes: true
190190
draft: false
191191
prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') }}
192+
193+
# ---------------------------------------------------------------------------
194+
# Downstream fan-out. After the GitHub Release (binaries + checksums) is
195+
# published, propagate THIS tag's version to every distribution channel:
196+
# PyPI, npm, Homebrew. Each leg is independent: one failing channel does not
197+
# block the others, but every failure is surfaced (the final "gate" step
198+
# fails the job if any leg failed). Skipped for -rc/-beta prereleases.
199+
#
200+
# Version-locking: every channel publishes ${GITHUB_REF_NAME} (the daemon
201+
# tag), NOT the stale version sitting in each downstream repo's source.
202+
#
203+
# PyPI/npm are triggered via `gh workflow run publish.yml -f version=...`
204+
# against the SDK repos. This MUST use a cross-repo PAT — a release created
205+
# with GITHUB_TOKEN cannot trigger another repo's workflow, and GITHUB_TOKEN
206+
# has no write scope on sibling repos. See RELEASE_DISPATCH_TOKEN below.
207+
publish-downstream:
208+
name: Publish to PyPI / npm / Homebrew
209+
needs: release
210+
if: ${{ !(contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta')) }}
211+
runs-on: ubuntu-latest
212+
permissions:
213+
contents: read
214+
steps:
215+
- name: Compute version
216+
id: ver
217+
run: |
218+
TAG="${GITHUB_REF_NAME}"
219+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
220+
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
221+
222+
# --- PyPI -------------------------------------------------------------
223+
- name: Dispatch sdk-python publish
224+
id: pypi
225+
continue-on-error: true
226+
env:
227+
GH_TOKEN: ${{ secrets.RELEASE_DISPATCH_TOKEN }}
228+
run: |
229+
if [ -z "$GH_TOKEN" ]; then
230+
echo "::error::RELEASE_DISPATCH_TOKEN is not set — cannot dispatch sdk-python"
231+
exit 1
232+
fi
233+
gh workflow run publish.yml \
234+
--repo pilot-protocol/sdk-python \
235+
--ref main \
236+
-f version="${{ steps.ver.outputs.version }}"
237+
echo "Dispatched sdk-python publish @ ${{ steps.ver.outputs.version }}"
238+
239+
# --- npm --------------------------------------------------------------
240+
- name: Dispatch sdk-node publish
241+
id: npm
242+
continue-on-error: true
243+
env:
244+
GH_TOKEN: ${{ secrets.RELEASE_DISPATCH_TOKEN }}
245+
run: |
246+
if [ -z "$GH_TOKEN" ]; then
247+
echo "::error::RELEASE_DISPATCH_TOKEN is not set — cannot dispatch sdk-node"
248+
exit 1
249+
fi
250+
gh workflow run publish.yml \
251+
--repo pilot-protocol/sdk-node \
252+
--ref main \
253+
-f version="${{ steps.ver.outputs.version }}"
254+
echo "Dispatched sdk-node publish @ ${{ steps.ver.outputs.version }}"
255+
256+
# --- Homebrew ---------------------------------------------------------
257+
# Regenerate the formula from this release's checksums + tag and push it
258+
# to the tap. HOMEBREW_TAP_TOKEN must be a PAT with write access to the
259+
# tap repo (the tap is in a different owner, so GITHUB_TOKEN can't push).
260+
- name: Update Homebrew formula
261+
id: brew
262+
continue-on-error: true
263+
env:
264+
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
265+
TAP_REPO: TeoSlayer/homebrew-pilot
266+
FORMULA_PATH: Formula/pilotprotocol.rb
267+
run: |
268+
set -euo pipefail
269+
if [ -z "$GH_TOKEN" ]; then
270+
echo "::error::HOMEBREW_TAP_TOKEN is not set — cannot update the tap"
271+
exit 1
272+
fi
273+
TAG="${{ steps.ver.outputs.tag }}"
274+
VERSION="${{ steps.ver.outputs.version }}"
275+
276+
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern checksums.txt --clobber
277+
cat checksums.txt
278+
279+
sum() { grep "pilot-$1.tar.gz" checksums.txt | awk '{print $1}'; }
280+
DA=$(sum darwin-arm64); DX=$(sum darwin-amd64)
281+
LA=$(sum linux-arm64); LX=$(sum linux-amd64)
282+
for v in "$DA" "$DX" "$LA" "$LX"; do
283+
[ -n "$v" ] || { echo "::error::missing checksum in checksums.txt"; exit 1; }
284+
done
285+
286+
base="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}"
287+
cat > pilotprotocol.rb <<RUBY
288+
class Pilotprotocol < Formula
289+
desc "The network stack for AI agents - addresses, ports, tunnels, encryption, trust"
290+
homepage "https://pilotprotocol.network"
291+
version "${VERSION}"
292+
license "AGPL-3.0"
293+
294+
on_macos do
295+
if Hardware::CPU.arm?
296+
url "${base}/pilot-darwin-arm64.tar.gz"
297+
sha256 "${DA}"
298+
else
299+
url "${base}/pilot-darwin-amd64.tar.gz"
300+
sha256 "${DX}"
301+
end
302+
end
303+
304+
on_linux do
305+
if Hardware::CPU.arm?
306+
url "${base}/pilot-linux-arm64.tar.gz"
307+
sha256 "${LA}"
308+
else
309+
url "${base}/pilot-linux-amd64.tar.gz"
310+
sha256 "${LX}"
311+
end
312+
end
313+
314+
def install
315+
bin.install "daemon" => "pilot-daemon"
316+
bin.install "pilotctl" => "pilotctl"
317+
bin.install "updater" => "pilot-updater"
318+
end
319+
320+
def post_install
321+
(var/"pilot").mkpath
322+
config_dir = Pathname.new(Dir.home)/".pilot"
323+
config_dir.mkpath
324+
(config_dir/"bin").mkpath
325+
(config_dir/"bin/.pilot-version").write "v#{version}\n"
326+
end
327+
328+
def caveats
329+
<<~EOS
330+
Get started:
331+
pilotctl daemon start --hostname my-agent --email you@example.com
332+
pilotctl info
333+
Docs: https://pilotprotocol.network/docs
334+
EOS
335+
end
336+
337+
service do
338+
run [opt_bin/"pilot-daemon", "-socket", "/tmp/pilot.sock", "-encrypt"]
339+
keep_alive crashed: true
340+
log_path var/"log/pilot-daemon.log"
341+
error_log_path var/"log/pilot-daemon.log"
342+
end
343+
344+
test do
345+
assert_match "pilotctl", shell_output("#{bin}/pilotctl --help 2>&1", 0)
346+
end
347+
end
348+
RUBY
349+
sed -i 's/^ //' pilotprotocol.rb
350+
cat pilotprotocol.rb
351+
352+
CONTENT=$(base64 -w 0 pilotprotocol.rb)
353+
SHA=$(gh api "repos/${TAP_REPO}/contents/${FORMULA_PATH}" --jq '.sha' 2>/dev/null || echo "")
354+
if [ -n "$SHA" ]; then
355+
gh api "repos/${TAP_REPO}/contents/${FORMULA_PATH}" -X PUT \
356+
-f message="pilotprotocol ${TAG}" -f content="$CONTENT" -f sha="$SHA"
357+
else
358+
gh api "repos/${TAP_REPO}/contents/${FORMULA_PATH}" -X PUT \
359+
-f message="pilotprotocol ${TAG}" -f content="$CONTENT"
360+
fi
361+
echo "Updated ${TAP_REPO}/${FORMULA_PATH} -> ${TAG}"
362+
363+
# --- Surface failures -------------------------------------------------
364+
- name: Fan-out gate
365+
if: always()
366+
run: |
367+
echo "PyPI dispatch : ${{ steps.pypi.outcome }}"
368+
echo "npm dispatch : ${{ steps.npm.outcome }}"
369+
echo "Homebrew : ${{ steps.brew.outcome }}"
370+
{
371+
echo "## Downstream fan-out (${{ steps.ver.outputs.tag }})"
372+
echo "| Channel | Result |"
373+
echo "|---------|--------|"
374+
echo "| PyPI (sdk-python dispatch) | ${{ steps.pypi.outcome }} |"
375+
echo "| npm (sdk-node dispatch) | ${{ steps.npm.outcome }} |"
376+
echo "| Homebrew (tap formula) | ${{ steps.brew.outcome }} |"
377+
} >> "$GITHUB_STEP_SUMMARY"
378+
if [ "${{ steps.pypi.outcome }}" != "success" ] || \
379+
[ "${{ steps.npm.outcome }}" != "success" ] || \
380+
[ "${{ steps.brew.outcome }}" != "success" ]; then
381+
echo "::error::one or more downstream channels failed to dispatch/update"
382+
exit 1
383+
fi

0 commit comments

Comments
 (0)