Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions .github/workflows/npm-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
name: npm-release

# Publishes @pdpp/connector-protocol and @pdpp/collector-runtime to npm via
# semantic-release, using OIDC trusted publishing (id-token: write, no
# NPM_TOKEN) — the same mechanism PDP-Connect/pdpp already runs for
# @pdpp/cli, @pdpp/mcp-server, and @pdpp/read-core. See .releaserc.yaml for
# which commits actually trigger a release: this workflow's `quality` and
# `release` jobs only run at all when resolve-version's dry run found one.
#
# Scoped to just these two npm packages: this repo also ships a Tauri
# desktop app via .github/workflows/release.yml (a completely separate
# artifact class — GitHub Release installers, not npm packages), which this
# workflow does not touch.

on:
push:
branches: [main]
workflow_dispatch: {}

concurrency:
group: npm-release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
resolve-version:
name: resolve next version
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
id-token: write
outputs:
new-release-published: ${{ steps.version.outputs.new-release-published }}
new-release-version: ${{ steps.version.outputs.new-release-version }}
new-release-git-tag: ${{ steps.version.outputs.new-release-git-tag }}

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22.23.1"
cache: npm

- name: Install dependencies
run: npm ci

- name: Determine next version
id: version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -o pipefail
npm run release:npm:dry-run 2>&1 | tee "$RUNNER_TEMP/semantic-release-dry-run.log"
node --import tsx scripts/semantic-release-github-output.ts dry-run "$RUNNER_TEMP/semantic-release-dry-run.log"

quality:
name: release quality checks
needs: resolve-version
if: needs.resolve-version.outputs.new-release-published == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22.23.1"
cache: npm

- name: Install dependencies
run: npm ci

- name: Verify connector-protocol package
run: npm run --workspace packages/connector-protocol verify

- name: Verify collector-runtime package
run: npm run --workspace packages/collector-runtime verify

- name: Test commit-analyzer scope gate
run: npx vitest run scripts/npm-release-commit-analyzer.test.ts

release:
name: publish to npm and GitHub
needs: [resolve-version, quality]
if: needs.resolve-version.outputs.new-release-published == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: write
id-token: write # OIDC trusted publishing — no NPM_TOKEN

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22.23.1"
cache: npm

- name: Install dependencies
run: npm ci

- name: Run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run release:npm
122 changes: 122 additions & 0 deletions .releaserc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# @pdpp/connector-protocol and @pdpp/collector-runtime publish in lockstep
# from a single release channel: 0.x versions released from `main` to npm's
# default `latest` dist-tag, no prerelease branch. Mirrors PDP-Connect/pdpp's
# own semantic-release posture (see that repo's .releaserc.yaml) for the same
# stated reason — a second moving prerelease branch is drift risk with no
# offsetting benefit at 0.x.
#
# UNLIKE pdpp, this repository (PDP-Connect/data-connect) also ships a
# Tauri desktop app with its own commit history (build/release/ci-scoped
# commits, and plenty of unscoped feat/fix commits touching connectors,
# telemetry, and the desktop app itself — none of which should cut an npm
# release). pdpp's own commit-analyzer config has no scope gate: it
# republishes on ANY feat/fix/perf commit, relying on pdpp being
# npm-packages-plus-one-console with no unrelated commit surface. That
# assumption does not hold here, so releaseRules below explicitly requires
# a connector-protocol or collector-runtime scope (or a breaking-change
# footer on such a commit) before a commit can trigger a release — everything
# else, scoped or not, releases nothing. (@semantic-release/commit-analyzer
# has no built-in "require this scope" option; this releaseRules allowlist is
# the documented workaround — see
# ai/research/semantic-release-monorepo-scoping/ in the dotfiles repo.)
#
# IMPORTANT — two things that are NOT how this plugin's docs read at a
# glance, both confirmed by direct reproduction against the actual installed
# @semantic-release/commit-analyzer@13.0.1 (what semantic-release@25.0.9
# resolves), not by reading the README:
#
# 1. String `scope` rules match with micromatch (shell-glob syntax), NOT
# JavaScript RegExp. `SCOPE_GLOB` below uses micromatch's extglob
# alternation (`@(a|b)`) to match "connector-protocol or collector-runtime
# is this commit's whole scope, or one comma-separated entry anywhere in a
# multi-package scope" (this repo's existing convention for commits
# touching several workspaces at once, e.g.
# `fix(collector-runtime,connector-protocol,local-collector): ...`).
#
# 2. A naive catch-all `{ release: false }` as the last rule does NOT safely
# mean "everything else releases nothing" — it means "this rule's
# unconditional match beats every other commit in the same release batch",
# because analyze-commit.js's own rule-priority comparator treats `false`
# as higher-priority than any real release type (an off-by-one in how it
# ranks a type that isn't in its known list). Reproduced directly: a
# release batch containing one `fix(collector-runtime): ...` commit AND
# one unrelated `fix(local-collector): ...` commit produced NO release at
# all with a naive catch-all — the unrelated commit silently cancelled the
# real one. The two catch-alls below are written to be mutually exclusive
# with the five rules above (matching only scopes that are NOT
# collector-runtime/connector-protocol, or commits with no scope at all)
# so they never co-match the same commit as a real rule and can never
# cancel it. Full verified test matrix (18 single-commit cases + 4
# multi-commit batch cases, all passing) is not committed here; re-derive
# by testing analyzeCommits() directly against this file's exact rules
# before changing them.
branches:
- name: main
tagFormat: "v${version}"
preset: "conventionalcommits"
plugins:
- - "@semantic-release/commit-analyzer"
- preset: conventionalcommits
releaseRules:
- breaking: true
scope: &SCOPE_GLOB "@(collector-runtime|connector-protocol)*|*,@(collector-runtime|connector-protocol)|*,@(collector-runtime|connector-protocol),*"
release: major
- type: feat
scope: *SCOPE_GLOB
release: minor
- type: fix
scope: *SCOPE_GLOB
release: patch
- type: perf
scope: *SCOPE_GLOB
release: patch
- type: revert
scope: *SCOPE_GLOB
release: patch
# Matches any commit whose scope is a real string that is NOT one of
# the target packages (e.g. `fix(local-collector): ...`,
# `fix(polyfill-connectors): ...`). Mutually exclusive with the five
# rules above by construction.
- scope: "!(@(collector-runtime|connector-protocol)*|*,@(collector-runtime|connector-protocol)|*,@(collector-runtime|connector-protocol),*)"
release: false
# Matches genuinely unscoped commits (conventional-commits-parser
# sets `scope: null`, not `undefined`, when a commit has no
# `(scope)` at all — this must be `null`, not omitted, or this rule
# would match every commit unconditionally, including target-scoped
# ones, recreating the same cancellation bug this design avoids).
- scope: null
release: false
presetConfig:
types:
- type: feat
scope: connector-protocol
section: "Features (@pdpp/connector-protocol)"
- type: fix
scope: connector-protocol
section: "Bug Fixes (@pdpp/connector-protocol)"
- type: perf
scope: connector-protocol
section: "Performance Improvements (@pdpp/connector-protocol)"
- type: feat
scope: collector-runtime
section: "Features (@pdpp/collector-runtime)"
- type: fix
scope: collector-runtime
section: "Bug Fixes (@pdpp/collector-runtime)"
- type: perf
scope: collector-runtime
section: "Performance Improvements (@pdpp/collector-runtime)"
- type: revert
section: "Reverts"
- - "@semantic-release/npm"
- pkgRoot: "packages/connector-protocol"
- - "@semantic-release/npm"
- pkgRoot: "packages/collector-runtime"
- - "@semantic-release/github"
- successComment: false
failComment: false
failTitle: false
labels: false
releasedLabels: false
- - "@semantic-release/exec"
- publishCmd: "node --import tsx scripts/semantic-release-github-output.ts publish ${nextRelease.version} ${nextRelease.gitTag}"
Loading
Loading