Skip to content

Commit c079171

Browse files
committed
feat(release): publish .tgz for ts/node plugins (alongside rust/go binaries)
Until now the publish workflow only built native binaries (rust/go) into 9-target GitHub releases. TS/node plugins had no release path: the GitLab compile-job ran `npm pack` but the resulting .tgz never left the runner, and the consumer-side SKILL.md inject (.gitlab-ci/scripts/ inject-preflight.py) was forced to fall back to `npm install -g git+https://github.com/<src_repo>#<sha>` — which does NOT work for monorepo-inlined ts plugins (okx/plugin-store has no root package.json) and performs no SHA256 verification. This commit closes that gap end-to-end on the producer side: detect / build_info step - Lang gate widened: typescript/node now flow through validation. - Plugins routed into two buckets via a `case "$LANG" in` switch: build_plugins_json (rust/go, 9-target binary matrix) vs build_node_plugins_json (ts/node, single npm pack). - Two `has_*_builds` flags + two `build_*_plugins_json` outputs. build-release-node (new job) - Matrix: plugin × 1, runs on ubuntu-latest. - `npm install` + (optional) `npm run build` + `npm pack`. - Rename the file `npm pack` produces (`<package>-<ver>.tgz`) to `<binary_name>.tgz` so the consumer-side URL is deterministic (mirrors rust/go's `<bin>-<target>` naming). - Upload as artifact `build-<name>-node` so the existing merge-multiple download in create-release picks it up. create-release - `needs` adds build-release-node; `if` accepts either has_builds or has_node_builds. - Combined plugin list = native + npm; one publish loop iterates both with a per-lang staging switch: * rust/go → `${BIN}-*` glob + `sha256sum ${BIN}-*` * typescript/node → `${BIN}.tgz` + `sha256sum ${BIN}.tgz` - PLATFORM_LABEL in release notes: count for rust/go, "any (npm)" for ts/node. notify-failure (housekeeping) - `needs` adds build-release-node so failures in the new path also page through. The consumer-side inject change (.gitlab-ci/scripts/inject-preflight.py) landed earlier on ci_plan_a (11eb915). That now finds the .tgz this workflow publishes, verifies SHA256, and `npm install -g`s it.
1 parent d524222 commit c079171

1 file changed

Lines changed: 155 additions & 19 deletions

File tree

.github/workflows/plugin-publish.yml

Lines changed: 155 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ jobs:
102102
outputs:
103103
has_builds: ${{ steps.build_info.outputs.has_builds }}
104104
build_plugins_json: ${{ steps.build_info.outputs.build_plugins_json }}
105+
has_node_builds: ${{ steps.build_info.outputs.has_node_builds }}
106+
build_node_plugins_json: ${{ steps.build_info.outputs.build_node_plugins_json }}
105107
steps:
106108
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
107109
with:
@@ -159,6 +161,7 @@ jobs:
159161
run: |
160162
set -euo pipefail
161163
BUILD_PLUGINS_JSON='[]'
164+
BUILD_NODE_PLUGINS_JSON='[]'
162165
163166
# Pre-compute (plugin_name, author.github, author.name) for every
164167
# plugin in skills/. Used below to mark contributors as "new" only
@@ -198,8 +201,8 @@ jobs:
198201
echo "$BIN" | grep -qE '^[a-zA-Z0-9._-]+$' \
199202
|| { echo "::warning::$p binary_name '$BIN' invalid, skip"; continue; }
200203
case "$LANG" in
201-
rust|go) : ;;
202-
*) echo "::warning::$p lang '$LANG' not in {rust,go}, skip"; continue ;;
204+
rust|go|typescript|node) : ;;
205+
*) echo "::warning::$p lang '$LANG' not in {rust,go,typescript,node}, skip"; continue ;;
203206
esac
204207
if [ -n "$REPO" ] && [ "$REPO" != "null" ]; then
205208
echo "$REPO" | grep -qE '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$' \
@@ -239,22 +242,41 @@ jobs:
239242
'$1 != plugin && ($2 == c || $3 == c) { n++ } END { print (n+0) }' "$ALL_AUTHORS")
240243
if [ "$HITS" -eq 0 ]; then IS_NEW="true"; else IS_NEW="false"; fi
241244
242-
BUILD_PLUGINS_JSON=$(echo "$BUILD_PLUGINS_JSON" | jq \
245+
# Build a single JSON entry then route it to the right bucket:
246+
# - rust / go → BUILD_PLUGINS_JSON (9-target native binary matrix)
247+
# - typescript / node → BUILD_NODE_PLUGINS_JSON (single npm package)
248+
ENTRY=$(jq -nc \
243249
--arg n "$p" --arg l "$LANG" --arg b "$BIN" --arg v "$VER" \
244250
--arg r "$REPO" --arg c "$COMMIT" --arg s "$SUBDIR" \
245251
--arg desc "$DESC" --arg an "$AUTH_NAME" --arg ag "$AUTH_GH" \
246252
--arg ct "$CONTRIBUTOR" --arg new "$IS_NEW" \
247-
'. += [{name:$n, lang:$l, binary_name:$b, version:$v, source_repo:$r, source_commit:$c, source_dir:$s, description:$desc, author_name:$an, author_github:$ag, contributor:$ct, is_new_contributor:$new}]')
248-
echo " + $p ($LANG) $BIN@$VER contributor=$CONTRIBUTOR new=$IS_NEW"
253+
'{name:$n, lang:$l, binary_name:$b, version:$v, source_repo:$r, source_commit:$c, source_dir:$s, description:$desc, author_name:$an, author_github:$ag, contributor:$ct, is_new_contributor:$new}')
254+
case "$LANG" in
255+
rust|go)
256+
BUILD_PLUGINS_JSON=$(echo "$BUILD_PLUGINS_JSON" | jq --argjson e "$ENTRY" '. += [$e]')
257+
echo " + $p ($LANG, native) $BIN@$VER contributor=$CONTRIBUTOR new=$IS_NEW"
258+
;;
259+
typescript|node)
260+
BUILD_NODE_PLUGINS_JSON=$(echo "$BUILD_NODE_PLUGINS_JSON" | jq --argjson e "$ENTRY" '. += [$e]')
261+
echo " + $p ($LANG, npm) $BIN@$VER contributor=$CONTRIBUTOR new=$IS_NEW"
262+
;;
263+
esac
249264
done
250265
251-
# GitHub Actions output: serialise JSON onto one line
266+
# GitHub Actions outputs: one JSON per bucket + has_* flags.
252267
ONE_LINE=$(echo "$BUILD_PLUGINS_JSON" | jq -c .)
253268
echo "build_plugins_json=${ONE_LINE}" >> "$GITHUB_OUTPUT"
254269
COUNT=$(echo "$BUILD_PLUGINS_JSON" | jq 'length')
255270
[ "$COUNT" -gt 0 ] && echo "has_builds=true" >> "$GITHUB_OUTPUT" \
256271
|| echo "has_builds=false" >> "$GITHUB_OUTPUT"
257-
echo "buildable plugins: $COUNT"
272+
273+
NODE_ONE_LINE=$(echo "$BUILD_NODE_PLUGINS_JSON" | jq -c .)
274+
echo "build_node_plugins_json=${NODE_ONE_LINE}" >> "$GITHUB_OUTPUT"
275+
NODE_COUNT=$(echo "$BUILD_NODE_PLUGINS_JSON" | jq 'length')
276+
[ "$NODE_COUNT" -gt 0 ] && echo "has_node_builds=true" >> "$GITHUB_OUTPUT" \
277+
|| echo "has_node_builds=false" >> "$GITHUB_OUTPUT"
278+
279+
echo "buildable plugins: $COUNT (native) + $NODE_COUNT (npm)"
258280
259281
# ═══════════════════════════════════════════════════════════════════════
260282
# JOB 3 build-release
@@ -486,6 +508,88 @@ jobs:
486508
if-no-files-found: error
487509
retention-days: 7
488510

511+
# ═══════════════════════════════════════════════════════════════════════
512+
# JOB 3.5 build-release-node
513+
#
514+
# TS / node plugins package via `npm pack` into a single
515+
# `<binary_name>.tgz` (vs rust/go's 9 native binaries). One ubuntu
516+
# runner per plugin — no target matrix dimension, because the .tgz is
517+
# platform-agnostic JavaScript and the consumer-side install (in
518+
# SKILL.md, injected by .gitlab-ci/scripts/inject-preflight.py) just
519+
# does `npm install -g <tgz>` after verifying SHA256.
520+
#
521+
# We rename the file npm pack produces (`<package-name>-<ver>.tgz`)
522+
# to `<binary_name>.tgz` so the consumer-side URL is deterministic
523+
# (mirrors rust/go's `<bin>-<target>` asset naming convention).
524+
# ═══════════════════════════════════════════════════════════════════════
525+
build-release-node:
526+
name: Build ${{ matrix.plugin.name }} (npm package)
527+
needs: [verify-source, detect]
528+
if: needs.detect.outputs.has_node_builds == 'true'
529+
runs-on: ubuntu-latest
530+
permissions:
531+
contents: read
532+
strategy:
533+
fail-fast: false
534+
max-parallel: 10
535+
matrix:
536+
plugin: ${{ fromJSON(needs.detect.outputs.build_node_plugins_json) }}
537+
steps:
538+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
539+
with:
540+
fetch-depth: 1
541+
persist-credentials: false
542+
543+
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
544+
with:
545+
node-version: '22'
546+
547+
- name: npm install + build + pack
548+
env:
549+
NAME: ${{ matrix.plugin.name }}
550+
BIN: ${{ matrix.plugin.binary_name }}
551+
SUBDIR: ${{ matrix.plugin.source_dir }}
552+
run: |
553+
set -euo pipefail
554+
SRC_REL="skills/${NAME}/${SUBDIR:-.}"
555+
SRC="${GITHUB_WORKSPACE}/${SRC_REL}"
556+
[ -f "${SRC}/package.json" ] || { echo "::error::no package.json in ${SRC_REL}"; exit 1; }
557+
mkdir -p "${GITHUB_WORKSPACE}/artifacts"
558+
559+
echo "::group::npm install"
560+
( cd "${SRC}" && npm install --no-audit --no-fund )
561+
echo "::endgroup::"
562+
563+
# Run `npm run build` if package.json defines a build script,
564+
# otherwise skip (some plugins ship pre-built dist/ already).
565+
if grep -q '"build"[[:space:]]*:' "${SRC}/package.json"; then
566+
echo "::group::npm run build"
567+
( cd "${SRC}" && npm run build )
568+
echo "::endgroup::"
569+
else
570+
echo "::notice::no 'build' script in package.json; skipping build step"
571+
fi
572+
573+
echo "::group::npm pack"
574+
( cd "${SRC}" && npm pack --quiet )
575+
TGZ=$(ls -t "${SRC}"/*.tgz 2>/dev/null | head -1)
576+
[ -n "${TGZ}" ] && [ -f "${TGZ}" ] || { echo "::error::npm pack produced no .tgz"; exit 1; }
577+
# Rename to <binary_name>.tgz for predictable consumer-side URL
578+
cp "${TGZ}" "${GITHUB_WORKSPACE}/artifacts/${BIN}.tgz"
579+
ls -la "${GITHUB_WORKSPACE}/artifacts/${BIN}.tgz"
580+
if command -v sha256sum >/dev/null; then sha256sum "${GITHUB_WORKSPACE}/artifacts/${BIN}.tgz"; \
581+
else shasum -a 256 "${GITHUB_WORKSPACE}/artifacts/${BIN}.tgz"; fi
582+
echo "::endgroup::"
583+
584+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
585+
with:
586+
# Suffix `-node` so the artifact name doesn't collide with rust/go
587+
# `build-<name>-<target>` artifacts in the merge-multiple download.
588+
name: build-${{ matrix.plugin.name }}-node
589+
path: artifacts/*
590+
if-no-files-found: error
591+
retention-days: 7
592+
489593
# ═══════════════════════════════════════════════════════════════════════
490594
# JOB 4 create-release
491595
#
@@ -511,8 +615,12 @@ jobs:
511615
# ═══════════════════════════════════════════════════════════════════════
512616
create-release:
513617
name: Create release
514-
needs: [detect, build-release]
515-
if: needs.detect.outputs.has_builds == 'true'
618+
needs: [detect, build-release, build-release-node]
619+
if: |
620+
always() &&
621+
(needs.detect.outputs.has_builds == 'true' || needs.detect.outputs.has_node_builds == 'true') &&
622+
(needs.build-release.result == 'success' || needs.build-release.result == 'skipped') &&
623+
(needs.build-release-node.result == 'success' || needs.build-release-node.result == 'skipped')
516624
runs-on: ubuntu-latest
517625
permissions:
518626
# contents: write is required to create the git tag and the
@@ -541,12 +649,20 @@ jobs:
541649
id: publish
542650
env:
543651
BUILD_PLUGINS: ${{ needs.detect.outputs.build_plugins_json }}
652+
BUILD_NODE_PLUGINS: ${{ needs.detect.outputs.build_node_plugins_json }}
544653
GH_TOKEN: ${{ github.token }}
545654
REPO: ${{ github.repository }}
546655
run: |
547656
set -euo pipefail
548657
PUBLISHED='[]'
549-
echo "$BUILD_PLUGINS" | jq -c '.[]' | while read -r P; do
658+
# Combine native + npm plugin lists into one stream; each entry
659+
# carries its own `lang` field, so the publish loop branches on
660+
# that to pick the right staging strategy.
661+
ALL_PLUGINS=$(jq -nc \
662+
--argjson native "${BUILD_PLUGINS:-[]}" \
663+
--argjson npm "${BUILD_NODE_PLUGINS:-[]}" \
664+
'$native + $npm')
665+
echo "$ALL_PLUGINS" | jq -c '.[]' | while read -r P; do
550666
NAME=$(echo "$P" | jq -r .name)
551667
BIN=$( echo "$P" | jq -r .binary_name)
552668
VER=$( echo "$P" | jq -r .version)
@@ -569,18 +685,38 @@ jobs:
569685
# on `gh release create FILE#LABEL`, which sets only the
570686
# display LABEL — the asset filename would otherwise have
571687
# remained `${BIN}-checksums.tmp`.
572-
if ! ls artifacts/${BIN}-* >/dev/null 2>&1; then
573-
echo "::error::no artifacts found for ${NAME} (pattern: ${BIN}-*)"
574-
continue
575-
fi
688+
#
689+
# Two staging shapes by lang:
690+
# - rust / go : 9 binaries `${BIN}-<target>[.exe]`
691+
# - typescript / node : 1 npm package `${BIN}.tgz`
692+
# checksums.txt is generated over whichever set is staged, so
693+
# the consumer-side SHA256 check in inject-preflight.py finds
694+
# exactly the asset it just downloaded.
576695
STAGE="artifacts/_${BIN}"
577696
mkdir -p "$STAGE"
578-
cp artifacts/${BIN}-* "$STAGE"/
579-
( cd "$STAGE" && sha256sum ${BIN}-* > checksums.txt )
697+
case "$LANG" in
698+
rust|go)
699+
if ! ls artifacts/${BIN}-* >/dev/null 2>&1; then
700+
echo "::error::no artifacts found for ${NAME} (pattern: ${BIN}-*)"
701+
continue
702+
fi
703+
cp artifacts/${BIN}-* "$STAGE"/
704+
( cd "$STAGE" && sha256sum ${BIN}-* > checksums.txt )
705+
PLATFORM_LABEL=$(ls "$STAGE"/${BIN}-* | wc -l | tr -d ' ')
706+
;;
707+
typescript|node)
708+
if [ ! -f "artifacts/${BIN}.tgz" ]; then
709+
echo "::error::no artifact found for ${NAME} (expected: ${BIN}.tgz)"
710+
continue
711+
fi
712+
cp "artifacts/${BIN}.tgz" "$STAGE"/
713+
( cd "$STAGE" && sha256sum "${BIN}.tgz" > checksums.txt )
714+
PLATFORM_LABEL="any (npm)"
715+
;;
716+
esac
580717
581718
ASSETS=$(ls "$STAGE"/* | tr '\n' ' ')
582719
ASSET_COUNT=$(echo "$ASSETS" | wc -w | tr -d ' ')
583-
PLATFORM_COUNT=$(ls "$STAGE"/${BIN}-* 2>/dev/null | wc -l | tr -d ' ')
584720
585721
# Build the release-notes body. Format mirrors the old
586722
# cross-platform-release workflow so the GitHub release page
@@ -593,7 +729,7 @@ jobs:
593729
{
594730
printf "## What's Changed\n\n"
595731
printf '%s\n' "- **${NAME}** v${VER} -- ${DESC}"
596-
printf '%s\n' "- Language: ${LANG} | Platforms: ${PLATFORM_COUNT}"
732+
printf '%s\n' "- Language: ${LANG} | Platforms: ${PLATFORM_LABEL}"
597733
printf '%s\n' "- Install: npx skills add ${REPO} --skill ${NAME}"
598734
printf '%s\n' "- Source: skills/${NAME}/ (included in submission)"
599735
if [ "$IS_NEW" = "true" ]; then
@@ -693,7 +829,7 @@ jobs:
693829
# ═══════════════════════════════════════════════════════════════════════
694830
notify-failure:
695831
name: Notify failure
696-
needs: [verify-source, detect, build-release, create-release]
832+
needs: [verify-source, detect, build-release, build-release-node, create-release]
697833
if: failure()
698834
runs-on: ubuntu-latest
699835
permissions: read-all

0 commit comments

Comments
 (0)