ci: npm release archives from source + two zips (Refs #35) #38

Merged
jmiller merged 3 commits from fix/npm-release-from-source into dev 2026-07-18 18:23:33 +00:00
2 changed files with 121 additions and 20 deletions
+79 -19
View File
@@ -3,8 +3,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# MCP-specific release pipeline that builds TypeScript, runs validation,
# attaches the compiled dist/ as a release artifact, and creates a GitHub
# Release with tool inventory in the release notes.
# attaches a full-repository zip and a source-directory zip as release
# artifacts, and creates a GitHub Release with tool inventory in the notes.
#
# This replaces the generic auto-release.yml for MCP server repos.
@@ -15,6 +15,9 @@ on:
branches:
- main
paths:
# Transitional: match both the new 'source/' layout and legacy 'src/'.
# This static YAML filter cannot use the resolve-source-dir output.
- 'source/**'
- 'src/**'
- 'package.json'
- 'tsconfig.json'
@@ -51,6 +54,18 @@ jobs:
- name: Install dependencies
run: npm ci
# ── Resolve source directory ─────────────────────────────────────
# Resolves the canonical source dir (e.g. 'source/' or 'src/') via the
# shared composite action. Its 'source_dir' output has a trailing slash.
# Note: the action lives on feature/ci-resolve-source-dir; this path
# resolves once that branch merges.
- name: Resolve source directory
id: srcdir
uses: ./.mokogit/actions/resolve-source-dir
with:
mokogit_token: ${{ secrets.MOKOGIT_TOKEN }}
strict: 'true'
- name: TypeScript compile check
run: npx tsc --noEmit
@@ -68,17 +83,30 @@ jobs:
- name: Generate tool inventory
id: tools
run: |
TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || echo "0")
# Resolve the entrypoint from the source dir output, fall back to
# legacy src/index.ts if the resolved file is missing.
SRC="${{ steps.srcdir.outputs.source_dir }}"
INDEX="${SRC}index.ts"
if [ ! -f "$INDEX" ]; then
echo "::warning::resolved entrypoint $INDEX missing; falling back to src/index.ts"
INDEX="src/index.ts"
fi
# Fail loud on an unreadable entry file so it isn't silently
# counted as 0 tools.
[ -r "$INDEX" ] || { echo "::error::entry file unreadable"; exit 1; }
TOOL_COUNT=$(grep -c "server\.tool(" "$INDEX" || echo "0")
echo "count=${TOOL_COUNT}" >> "$GITHUB_OUTPUT"
# Extract tool names
TOOL_LIST=$(grep -oE "'[a-z_]+'" src/index.ts | head -100 | tr -d "'" | sort -u)
TOOL_LIST=$(grep -oE "'[a-z_]+'" "$INDEX" | head -100 | tr -d "'" | sort -u)
echo "Tools registered: ${TOOL_COUNT}"
# Generate inventory for release notes
echo "## Tool Inventory (${TOOL_COUNT} tools)" > /tmp/tool-inventory.md
echo "" >> /tmp/tool-inventory.md
grep -B0 -A1 "server\.tool(" src/index.ts | grep -oE "'[^']+'" | while IFS= read -r name; do
grep -B0 -A1 "server\.tool(" "$INDEX" | grep -oE "'[^']+'" | while IFS= read -r name; do
read -r desc 2>/dev/null || true
CLEAN_NAME=$(echo "$name" | tr -d "'")
CLEAN_DESC=$(echo "$desc" | tr -d "'" | sed 's/,$//')
@@ -139,15 +167,6 @@ jobs:
git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
# ── Release Artifact ─────────────────────────────────────────────
- name: Package dist
if: steps.version.outputs.skip != 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
REPO_NAME="${{ github.event.repository.name }}"
tar -czf "/tmp/${REPO_NAME}-${VERSION}.tar.gz" -C dist .
echo "artifact=/tmp/${REPO_NAME}-${VERSION}.tar.gz" >> "$GITHUB_OUTPUT"
# ── Version Updates ──────────────────────────────────────────────
- name: Set platform version
if: >-
@@ -208,6 +227,44 @@ jobs:
git push origin "$TAG"
fi
# ── Release Artifacts ────────────────────────────────────────────
# Built AFTER the release commit/tag so both zips reflect the final
# released tree. Mirrors the Joomla two-artifact model:
# 1. full-repo zip — tracked files only (git archive excludes
# .git/node_modules/dist automatically).
# 2. source zip — just the resolved source subtree. The git
# tree-ish path must NOT have a trailing slash, hence ${SRC%/}.
- name: Package release artifacts
id: package
if: steps.version.outputs.skip != 'true'
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
REPO_NAME="${{ github.event.repository.name }}"
REPO_ZIP="/tmp/${REPO_NAME}-${VERSION}.zip"
SOURCE_ZIP="/tmp/${REPO_NAME}-${VERSION}-source.zip"
# Full-repository "standard" zip (tracked files only).
git archive --format=zip -o "$REPO_ZIP" HEAD
# Source-directory subtree zip.
SRC="${{ steps.srcdir.outputs.source_dir }}"
SRC="${SRC%/}"
if [ -z "$SRC" ] || [ "$SRC" = "." ]; then
echo "::error::source_dir did not resolve to a real subdirectory (got '${SRC}'); refusing to build a misleading source zip"
exit 1
fi
git archive --format=zip -o "$SOURCE_ZIP" "HEAD:${SRC}"
# Fail closed if either archive is missing or empty.
for z in "$REPO_ZIP" "$SOURCE_ZIP"; do
[ -s "$z" ] || { echo "::error::archive $z missing or empty"; exit 1; }
done
echo "repo_zip=${REPO_ZIP}" >> "$GITHUB_OUTPUT"
echo "source_zip=${SOURCE_ZIP}" >> "$GITHUB_OUTPUT"
- name: GitHub Release
if: >-
steps.version.outputs.skip != 'true' &&
@@ -215,6 +272,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
MAJOR="${{ steps.version.outputs.major }}"
@@ -223,8 +281,8 @@ jobs:
REPO_NAME="${{ github.event.repository.name }}"
# Build release notes
NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
if [ -z "$NOTES" ]; then NOTES="Release ${VERSION}"; fi
{
echo "$NOTES"
@@ -241,20 +299,22 @@ jobs:
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
ARTIFACT="/tmp/${REPO_NAME}-${VERSION}.tar.gz"
# Two artifacts: full-repo zip and source zip (from Package step).
REPO_ZIP="${{ steps.package.outputs.repo_zip }}"
SOURCE_ZIP="${{ steps.package.outputs.source_zip }}"
if [ -z "$EXISTING" ]; then
gh release create "$RELEASE_TAG" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md \
--target "$BRANCH" \
"$ARTIFACT"
"$REPO_ZIP" "$SOURCE_ZIP"
echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY
else
gh release edit "$RELEASE_TAG" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md
gh release upload "$RELEASE_TAG" "$ARTIFACT" --clobber 2>/dev/null || true
gh release upload "$RELEASE_TAG" "$REPO_ZIP" "$SOURCE_ZIP" --clobber
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY
fi
+42 -1
View File
@@ -45,6 +45,17 @@ env:
GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }}
jobs:
# ─────────────────────────────────────────────────────────────────────
# NOTE (npm/MCP repos): This is the PHP/Joomla pre-release pipeline. It is
# NOT a Node-aware release path — it has no setup-node / npm ci / npm build.
# npm and MCP repos MUST NOT rely on this workflow for releasing; they use
# 'npm-auto-release.yml' instead. The "Check platform eligibility" step
# already sets proceed=false for non-Joomla platforms, so the PHP build
# steps are skipped for npm repos. In addition, the PHP toolchain install
# below is guarded on the ABSENCE of package.json so npm repos don't pull
# in php-cli/composer. TODO(#35): once resolve-source-dir lands everywhere,
# split this into platform-specific reusable workflows.
# ─────────────────────────────────────────────────────────────────────
build:
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
runs-on: release
@@ -76,7 +87,11 @@ jobs:
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
else
echo Falling back to fresh clone
if ! command -v composer > /dev/null 2>&1; then
# Skip the PHP/composer toolchain install for npm/MCP repos
# (detected via package.json). Those repos release through
# npm-auto-release.yml and the Joomla-only steps below are gated
# off via the eligibility check, so the toolchain is unneeded.
if [ ! -f package.json ] && ! command -v composer > /dev/null 2>&1; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/mokocli
@@ -86,8 +101,23 @@ jobs:
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
fi
# Early clean-exit for npm/MCP repos: this is the Joomla pre-release
# pipeline and does not apply. Detecting here (before any php call)
# avoids invoking platform_detect.php on runners where php may be
# absent (npm repos take the fresh-clone path that skips the PHP
# toolchain install).
- name: Detect npm/MCP repo
id: npmcheck
run: |
if [ -f package.json ]; then
echo "is_npm=true" >> "$GITHUB_OUTPUT"
else
echo "is_npm=false" >> "$GITHUB_OUTPUT"
fi
- name: Detect platform
id: platform
if: steps.npmcheck.outputs.is_npm != 'true'
run: |
# Auto-detect and update platform if not set in manifest
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
@@ -96,7 +126,18 @@ jobs:
- name: Check platform eligibility (Joomla only)
id: eligibility
run: |
# npm/MCP repos: Joomla pre-release does not apply — clean exit.
if [ "${{ steps.npmcheck.outputs.is_npm }}" = "true" ]; then
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "::notice::npm/MCP repo — pre-release (Joomla) not applicable; use npm-auto-release.yml"
exit 0
fi
PLATFORM="${{ steps.platform.outputs.platform }}"
# Do not silently no-op an eligible Joomla repo if detection failed.
if [ -z "$PLATFORM" ]; then
echo "::warning::platform detection returned empty"
fi
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
else