@@ -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