From 65419a426e7a5f2fbcb9a3fd8b180de35883c5ab Mon Sep 17 00:00:00 2001 From: Calin Teodor Date: Mon, 13 Jul 2026 10:52:27 +0300 Subject: [PATCH] publish: skip rich submissions gracefully; fail loudly on malformed pointers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit publish-on-merge has been red since 2026-07-06: every submission merged since then uses the RICH shape (backend/methods spec, no pre-built .bundle), but publish-submission.sh only knows the legacy POINTER shape. jq -r on the missing fields printed the literal string "null", producing "missing bundle submissions/io.pilot.tldr/null". - publish-submission.sh: extract fields with `// empty` and validate each one with a clear error; detect rich submissions (backend object + methods) and skip them with a notice — they are built and signed with the publisher's own key via pilot-app submit / publish-api, which CI cannot do (no publisher key); error loudly when a pointer submission lacks .namespace/.bundle_sha256. - publish-on-merge.yml: add workflow_dispatch (manually (re)publish one submission dir); ignore DELETED submission.json files (--diff-filter=d); update the header comment (platform repo is pilot-protocol/pilotprotocol). - cmd/pilot-app/main.go:654 + internal/scaffold/templates/Makefile.tmpl:65: the printed publish instructions pointed users at TeoSlayer/pilotprotocol; the catalogue lives on pilot-protocol/pilotprotocol. Verified locally: rich submission (io.pilot.tldr) → skip exit 0; pointer submission (io.pilot.sixtyfour) resolves tag/sha/url correctly; missing id/namespace/sha each fail with a specific message; go build + tests pass. Co-Authored-By: Claude Fable 5 --- .github/workflows/publish-on-merge.yml | 23 +++++++++++--- cmd/pilot-app/main.go | 2 +- internal/scaffold/templates/Makefile.tmpl | 2 +- scripts/publish-submission.sh | 38 ++++++++++++++++++----- 4 files changed, 52 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish-on-merge.yml b/.github/workflows/publish-on-merge.yml index 8d302fb..b3a4c7e 100644 --- a/.github/workflows/publish-on-merge.yml +++ b/.github/workflows/publish-on-merge.yml @@ -1,9 +1,12 @@ -# On merge to main, publish any new/changed submission: release the bundle on -# pilot-protocol/catalog and open a catalogue.json PR on TeoSlayer/pilotprotocol. +# On merge to main, publish any new/changed POINTER submission (pre-built, +# publisher-signed .bundle): release the bundle on pilot-protocol/catalog and +# open a catalogue.json PR on pilot-protocol/pilotprotocol. RICH submissions +# (backend/methods spec, no .bundle) are built+signed by the publisher's own +# key via pilot-app submit / publish-api and are skipped here with a notice. # # Requires repo secret CATALOG_PUBLISH_TOKEN — a PAT or GitHub App token with: # - contents:write on pilot-protocol/catalog (create releases) -# - contents:write + pull_requests:write on TeoSlayer/pilotprotocol (catalogue PR) +# - contents:write + pull_requests:write on pilot-protocol/pilotprotocol (catalogue PR) # Without it, this job no-ops with a clear message (submissions still validate on PR). name: publish-on-merge @@ -12,6 +15,12 @@ on: branches: [main] paths: - "submissions/**" + workflow_dispatch: + inputs: + submission: + description: "submissions/ directory to (re)publish, e.g. submissions/io.pilot.tldr" + required: true + type: string jobs: publish: @@ -47,7 +56,13 @@ jobs: CATALOG_SIGN_KEY: ${{ secrets.CATALOG_SIGN_KEY }} run: | set -euo pipefail - changed="$(git diff --name-only HEAD~1 HEAD -- 'submissions/**/submission.json' || true)" + # Manual (re)publish of a single submission via workflow_dispatch. + if [ -n "${{ github.event.inputs.submission || '' }}" ]; then + scripts/publish-submission.sh "${{ github.event.inputs.submission }}" + exit 0 + fi + # --diff-filter=d: a DELETED submission.json has nothing to publish. + changed="$(git diff --diff-filter=d --name-only HEAD~1 HEAD -- 'submissions/**/submission.json' || true)" [ -n "$changed" ] || { echo "no submission.json changed"; exit 0; } for sub in $changed; do scripts/publish-submission.sh "$(dirname "$sub")" diff --git a/cmd/pilot-app/main.go b/cmd/pilot-app/main.go index d3e3586..479fee8 100644 --- a/cmd/pilot-app/main.go +++ b/cmd/pilot-app/main.go @@ -651,7 +651,7 @@ func cmdSubmit(args []string) { fmt.Printf("\nTo publish via the single central repo, run `pilot-app submit -C %s --prepare `.\n", *dir) fmt.Printf("\nDirect path (org maintainers only):\n── Step 1: release on pilot-protocol/catalog ──\n%s\n", relCmd) - fmt.Printf("\n── Step 2: add to catalogue/catalogue.json on TeoSlayer/pilotprotocol@main (PR) ──\n%s\n", catEntry) + fmt.Printf("\n── Step 2: add to catalogue/catalogue.json on pilot-protocol/pilotprotocol@main (PR) ──\n%s\n", catEntry) } // writeEnrichedMetadata reads the project's metadata.json (from init), fills the diff --git a/internal/scaffold/templates/Makefile.tmpl b/internal/scaffold/templates/Makefile.tmpl index f6f9811..c88213a 100644 --- a/internal/scaffold/templates/Makefile.tmpl +++ b/internal/scaffold/templates/Makefile.tmpl @@ -62,7 +62,7 @@ publish-help: package @echo "Publish $(APP_ID) v$(VERSION):" @echo " 1. gh release create {{.Namespace}}-v$(VERSION) -R pilot-protocol/catalog \\" @echo " $(TARBALL) -t \"$(APP_ID) v$(VERSION)\" -n \"...\"" - @echo " 2. add this entry to catalogue/catalogue.json on TeoSlayer/pilotprotocol (via PR):" + @echo " 2. add this entry to catalogue/catalogue.json on pilot-protocol/pilotprotocol (via PR):" @echo " {\"id\":\"$(APP_ID)\",\"version\":\"$(VERSION)\"," @echo " \"description\":\"{{.Description}}\"," @echo " \"bundle_url\":\"https://github.com/pilot-protocol/catalog/releases/download/{{.Namespace}}-v$(VERSION)/$(TARBALL)\"," diff --git a/scripts/publish-submission.sh b/scripts/publish-submission.sh index 11c7c65..82714fe 100755 --- a/scripts/publish-submission.sh +++ b/scripts/publish-submission.sh @@ -5,14 +5,40 @@ # publish-on-merge.yml with GH_TOKEN = CATALOG_PUBLISH_TOKEN. Idempotent on tag. set -euo pipefail -DIR="$1" +DIR="${1%/}" META="$DIR/submission.json" test -f "$META" || { echo "no submission.json in $DIR"; exit 1; } -ID="$(jq -r .id "$META")" -VERSION="$(jq -r .version "$META")" -NS="$(jq -r .namespace "$META")" -DESC="$(jq -r .description "$META")" +# jq -r prints the literal string "null" for a missing field, which used to +# flow straight into paths/tags ("missing bundle submissions//null"). +# Extract with `// empty` and validate every required field loudly instead. +ID="$(jq -r '.id // empty' "$META")" +VERSION="$(jq -r '.version // empty' "$META")" +DESC="$(jq -r '.description // empty' "$META")" +[ -n "$ID" ] || { echo "ERROR: $META has no .id"; exit 1; } +[ -n "$VERSION" ] || { echo "ERROR: $META ($ID) has no .version"; exit 1; } + +# Two submission shapes exist (docs/APP-PUBLISHING-SPEC.md): +# POINTER — carries a pre-built, publisher-signed .bundle (+ .bundle_sha256 + +# .namespace). That is what this script releases. +# RICH — carries only the backend/methods spec; its bundles are built and +# signed with the PUBLISHER's own key (`pilot-app submit` / +# publish-api). CI holds no publisher key, so this pointer +# publisher cannot build or sign them — skip gracefully. +PRIMARY_FILE="$(jq -r '.bundle // empty' "$META")" +if [ -z "$PRIMARY_FILE" ]; then + if jq -e '(.backend | type == "object") and ((.methods // []) | length > 0)' "$META" >/dev/null; then + echo "==> $ID v$VERSION is a RICH submission (no pre-built .bundle pointer) — it is built+signed by the publisher's own key via the pilot-app submit / publish-api path, not by this pointer publisher. Skipping." + exit 0 + fi + echo "ERROR: $META ($ID v$VERSION) has neither a .bundle pointer nor a rich backend/methods spec — nothing to publish" + exit 1 +fi + +NS="$(jq -r '.namespace // empty' "$META")" +SHA="$(jq -r '.bundle_sha256 // empty' "$META")" +[ -n "$NS" ] || { echo "ERROR: $META ($ID v$VERSION) declares .bundle but no .namespace (needed for the release tag)"; exit 1; } +[ -n "$SHA" ] || { echo "ERROR: $META ($ID v$VERSION) declares .bundle but no .bundle_sha256"; exit 1; } CATALOG_REPO="pilot-protocol/catalog" PLATFORM_REPO="pilot-protocol/pilotprotocol" @@ -22,8 +48,6 @@ REL_BASE="https://github.com/${CATALOG_REPO}/releases/download/${TAG}" # The linux/amd64 primary backs the top-level bundle_url (what pre-v3 clients # fetch). Newer submissions also carry a .bundles map of every platform's # {file,sha256}; older ones have only .bundle/.bundle_sha256 (single platform). -PRIMARY_FILE="$(jq -r .bundle "$META")" -SHA="$(jq -r .bundle_sha256 "$META")" BUNDLE_URL="${REL_BASE}/${PRIMARY_FILE}" # Every asset to release: the .bundles map's files, or just .bundle when absent.