diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b7225300c..20fa4d2c5 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -89,11 +89,55 @@ jobs: platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + publish-npm: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # mint an OIDC token for npm trusted publishing (no NPM_TOKEN needed) + needs: + - build + - configure + if: ${{ needs.configure.outputs.should_release == 'true' }} + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Setup PNPM + uses: pnpm/action-setup@v3 + - name: Setup Node + uses: actions/setup-node@v4 + with: + cache: pnpm + node-version-file: '.nvmrc' + registry-url: 'https://registry.npmjs.org' + - name: Install Dependencies + run: pnpm install --frozen-lockfile + - name: Build Publishable Packages + run: | + # Build each publishable package and its dependency closure (turbo skips + # packages that have no build script, e.g. those that publish source directly). + filters=() + while IFS=$'\t' read -r name version pkgPath; do + filters+=("--filter=${name}...") + done < <(scripts/list-publishable.sh) + pnpm exec turbo run build "${filters[@]}" + - name: Publish Packages + run: | + # Publish each marked package whose version is not already on npm. The version + # check keeps this idempotent across workflow re-runs. + while IFS=$'\t' read -r name version pkgPath; do + if [ -n "$(npm view "${name}@${version}" version 2>/dev/null)" ]; then + echo "Skipping ${name}@${version} (already published)" + continue + fi + echo "Publishing ${name}@${version}" + pnpm --filter "${name}" publish --no-git-checks + done < <(scripts/list-publishable.sh) release: runs-on: ubuntu-latest needs: - build - configure + - publish-npm steps: - name: Checkout Repository uses: actions/checkout@v4 diff --git a/package.json b/package.json index 6b4173e30..eb827f86b 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "opendatacapture", "type": "module", - "version": "1.16.3", + "version": "1.16.4", "private": true, - "packageManager": "pnpm@10.7.0", + "packageManager": "pnpm@10.34.1", "license": "Apache-2.0", "engines": { "node": ">=v22.11.0" diff --git a/packages/instrument-bundler/package.json b/packages/instrument-bundler/package.json index d9a3cc128..03dd54970 100644 --- a/packages/instrument-bundler/package.json +++ b/packages/instrument-bundler/package.json @@ -1,8 +1,11 @@ { "name": "@opendatacapture/instrument-bundler", "type": "module", - "version": "0.0.0", + "version": "1.16.4", "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, "exports": { ".": "./src/index.ts" }, diff --git a/packages/instrument-guidelines/package.json b/packages/instrument-guidelines/package.json index 8c0b5aad2..d85dae767 100644 --- a/packages/instrument-guidelines/package.json +++ b/packages/instrument-guidelines/package.json @@ -1,9 +1,12 @@ { "name": "@opendatacapture/instrument-guidelines", "type": "module", - "version": "0.0.1", + "version": "1.16.4", "description": "Guidelines for authoring Open Data Capture instruments, intended to be read by an AI agent (e.g. Claude Code).", "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, "keywords": [ "opendatacapture", "instrument", diff --git a/packages/serve-instrument/package.json b/packages/serve-instrument/package.json index 44e6ac685..e23f333f4 100644 --- a/packages/serve-instrument/package.json +++ b/packages/serve-instrument/package.json @@ -1,8 +1,11 @@ { "name": "@opendatacapture/serve-instrument", "type": "module", - "version": "0.0.9", + "version": "1.16.4", "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, "bin": { "serve-instrument": "./dist/cli.js" }, diff --git a/runtime/v1/package.json b/runtime/v1/package.json index c093bc20e..8c1c5e485 100644 --- a/runtime/v1/package.json +++ b/runtime/v1/package.json @@ -1,12 +1,15 @@ { "name": "@opendatacapture/runtime-v1", "type": "module", - "version": "1.8.10", + "version": "1.16.4", "author": { "name": "Douglas Neuroinformatics", "email": "support@douglasneuroinformatics.ca" }, "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, "homepage": "https://github.com/DouglasNeuroInformatics/OpenDataCapture/#readme", "repository": { "type": "git", diff --git a/scripts/increment-version.sh b/scripts/increment-version.sh index 07dc0a0d7..65c51cb07 100755 --- a/scripts/increment-version.sh +++ b/scripts/increment-version.sh @@ -7,14 +7,13 @@ IFS=$'\n\t' projectRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")" && cd .. && pwd)" -# package.json files (relative to projectRoot) kept in sync with the root version -packages=( - "package.json" - "runtime/v1/package.json" - "packages/instrument-bundler/package.json" - "packages/serve-instrument/package.json" - "packages/instrument-guidelines/package.json" -) +# package.json files kept in sync with the root version: the (private) root package, +# plus every publishable package discovered via its `publishConfig` marker. See +# scripts/list-publishable.sh — adding a new publishable package needs no edit here. +packages=("$projectRoot/package.json") +while IFS=$'\t' read -r _name _version pkgPath; do + packages+=("$pkgPath") +done < <("$projectRoot/scripts/list-publishable.sh") currentVersion=$(node -e 'process.stdout.write(require(process.argv[1]).version)' "$projectRoot/package.json") IFS='.' read -r major minor patch <<< "$currentVersion" @@ -38,8 +37,7 @@ case "$confirm" in *) echo "Aborted."; exit 0 ;; esac -for pkg in "${packages[@]}"; do - file="$projectRoot/$pkg" +for file in "${packages[@]}"; do node -e ' const fs = require("fs"); const [file, version] = process.argv.slice(1); @@ -51,7 +49,7 @@ for pkg in "${packages[@]}"; do } fs.writeFileSync(file, updated); ' "$file" "$newVersion" - echo "Updated $pkg -> $newVersion" + echo "Updated ${file#"$projectRoot"/} -> $newVersion" done echo "Done! All packages set to $newVersion" diff --git a/scripts/list-publishable.sh b/scripts/list-publishable.sh new file mode 100755 index 000000000..a6fd82489 --- /dev/null +++ b/scripts/list-publishable.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# +# Lists the workspace packages that should be published to npm on release. +# +# A package is "publishable" when it is not private and declares a `publishConfig` +# field in its package.json. This is the single source of truth consumed by both +# scripts/increment-version.sh (to keep versions in sync) and the release workflow +# (to decide what to publish). Adding a future package only requires the +# `publishConfig` field — no list needs editing here. +# +# Output (one package per line, tab-separated): \t\t + +set -euo pipefail +IFS=$'\n\t' + +[ "${BASH_VERSINFO:-0}" -ge 5 ] || (echo "Error: Bash >= 5.0 is required for this script" >&2 && exit 1) + +projectRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")" && cd .. && pwd)" + +# Enumerate every workspace package (depth -1 = workspace packages only, no deps), +# then keep the non-private ones that declare a publishConfig. +pnpm -C "$projectRoot" ls -r --depth -1 --json \ + | jq -r '.[].path' \ + | while read -r dir; do + pkg="$dir/package.json" + [ -f "$pkg" ] || continue + jq -r --arg path "$pkg" \ + 'select(.private != true and .publishConfig != null) | [.name, .version, $path] | @tsv' "$pkg" + done \ + | sort