@@ -191,193 +191,29 @@ jobs:
191191 prerelease : ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') }}
192192
193193 # ---------------------------------------------------------------------------
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.
194+ # Downstream fan-out is NO LONGER driven from here.
199195 #
200- # Version-locking: every channel publishes ${GITHUB_REF_NAME} (the daemon
201- # tag), NOT the stale version sitting in each downstream repo's source.
196+ # Each distribution channel now SELF-UPDATES by polling this repo's public
197+ # latest release on a schedule, using only secrets already present in its
198+ # own repo. This removes the need for any cross-repo PAT
199+ # (RELEASE_DISPATCH_TOKEN, HOMEBREW_TAP_TOKEN) — a workflow's built-in
200+ # GITHUB_TOKEN can write to its own repo but not sibling repos, which is why
201+ # the old push-based fan-out needed PATs. The pull-based watchers below act
202+ # with each repo's own token instead:
202203 #
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
204+ # - Homebrew tap TeoSlayer/homebrew-pilot
205+ # .github/workflows/update-formula.yml (schedule + workflow_dispatch)
206+ # regenerates Formula/pilotprotocol.rb from this release's public
207+ # checksums.txt and commits with its own GITHUB_TOKEN.
208+ #
209+ # - npm SDK pilot-protocol/sdk-node
210+ # .github/workflows/release-watch.yml (schedule + workflow_dispatch)
211+ # dispatches its own publish.yml (NPM_TOKEN) at the new version.
212+ #
213+ # - PyPI SDK pilot-protocol/sdk-python
214+ # .github/workflows/release-watch.yml (schedule + workflow_dispatch)
215+ # dispatches its own publish.yml (PYPI_API_TOKEN) at the new version.
216+ #
217+ # All three poll every ~30 min (workflow_dispatch is the instant manual
218+ # path) and are idempotent. This release workflow's only job is to build
219+ # the binaries and publish the GitHub Release — the watchers do the rest.
0 commit comments