Skip to content
Closed
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
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
lint-and-typecheck:
name: Lint and Type Check
Expand Down Expand Up @@ -80,3 +83,61 @@ jobs:

- name: Build packages
run: pnpm build

dsh-compatibility:
name: DSH Compatibility (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [22.19.0, 24, 26]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Run deterministic plugin checks
run: pnpm --filter @parallel-web/dsh-web-search-parallel check

- name: Verify packed DSH profile lifecycle
shell: bash
run: |
set -euo pipefail
pack_dir="$(mktemp -d)"
dsh_home="$(mktemp -d)"
trap 'rm -rf "$pack_dir" "$dsh_home"' EXIT

pnpm --dir packages/dsh-web-search-parallel pack --pack-destination "$pack_dir"
artifact="$(find "$pack_dir" -name '*.tgz' -print -quit)"
version="$(node -p "require('./packages/dsh-web-search-parallel/package.json').version")"

node packages/dsh-web-search-parallel/scripts/verify-packed-artifact.mjs \
--tarball "$artifact"
DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search-parallel exec dsh \
--profile web --dump-config > "$dsh_home/before.yml"
DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search-parallel exec dsh \
plugin --profile web add "$artifact"
DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search-parallel exec dsh \
--profile web --dump-config > "$dsh_home/after.yml"
node packages/dsh-web-search-parallel/scripts/verify-packed-profile.mjs \
--dsh-home "$dsh_home" \
--expected-version "$version"
DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search-parallel exec dsh \
plugin --profile web remove @parallel-web/dsh-web-search-parallel
DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search-parallel exec dsh \
--profile web --dump-config > "$dsh_home/removed.yml"
node packages/dsh-web-search-parallel/scripts/verify-profile-overlay.mjs \
--before "$dsh_home/before.yml" \
--after "$dsh_home/after.yml" \
--removed "$dsh_home/removed.yml"
80 changes: 12 additions & 68 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ name: Release
# When a version-bump commit lands on main (from a merged release PR opened by
# scripts/release.sh), publish any package whose version changed and that does
# not yet have a tag. Each package is versioned and tagged independently — git
# tags are namespaced per package (<package>-vX.Y.Z) so the three packages
# never collide.
# tags are namespaced per package (<package>-vX.Y.Z) so packages never collide.
on:
push:
branches: [main]
Expand Down Expand Up @@ -43,75 +42,14 @@ jobs:
EVENT_NAME: ${{ github.event_name }}
INPUT_PACKAGE: ${{ github.event.inputs.package }}
BEFORE: ${{ github.event.before }}
REF: ${{ github.ref }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail

# Build the candidate list of package.json paths to consider.
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
CANDIDATES="packages/${INPUT_PACKAGE}/package.json"
if [ ! -f "$CANDIDATES" ]; then
echo "::error::no package found at $CANDIDATES"
exit 1
fi
else
BASE="$BEFORE"
if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then
BASE="$(git rev-parse "$SHA^" 2>/dev/null || echo '')"
fi
if [ -n "$BASE" ]; then
CANDIDATES="$(git diff --name-only "$BASE" "$SHA" -- 'packages/*/package.json')"
else
CANDIDATES="$(git ls-files 'packages/*/package.json')"
fi
fi

echo "Candidate package.json files:"
echo "${CANDIDATES:-<none>}"

INCLUDES="[]"
for pkg_json in $CANDIDATES; do
[ -f "$pkg_json" ] || continue
name="$(basename "$(dirname "$pkg_json")")"
private="$(node -p "require('./$pkg_json').private === true")"
if [ "$private" = "true" ]; then
echo "skip $name: private package"
continue
fi
version="$(node -p "require('./$pkg_json').version")"
tag="${name}-v${version}"
if git show-ref --quiet --verify "refs/tags/$tag"; then
echo "skip $name: tag $tag already exists (already released)"
continue
fi
case "$version" in
*-rc.*) npm_tag="rc"; prerelease="true" ;;
*) npm_tag="latest"; prerelease="false" ;;
esac
echo "release $name@$version -> tag $tag (dist-tag $npm_tag)"
INCLUDES="$(node -e "
const arr = JSON.parse(process.argv[1]);
arr.push({
name: process.argv[2],
dir: 'packages/' + process.argv[2],
version: process.argv[3],
tag: process.argv[4],
npm_tag: process.argv[5],
prerelease: process.argv[6] === 'true',
});
console.log(JSON.stringify(arr));
" "$INCLUDES" "$name" "$version" "$tag" "$npm_tag" "$prerelease")"
done

COUNT="$(node -p "JSON.parse(process.argv[1]).length" "$INCLUDES")"
if [ "$COUNT" = "0" ]; then
echo "any=false" >> "$GITHUB_OUTPUT"
echo "matrix={\"include\":[]}" >> "$GITHUB_OUTPUT"
echo "Nothing to release."
else
echo "any=true" >> "$GITHUB_OUTPUT"
echo "matrix={\"include\":$INCLUDES}" >> "$GITHUB_OUTPUT"
fi
RESULT="$(node scripts/detect-releases.mjs)"
echo "Release detection: $RESULT"
echo "any=$(node -p 'JSON.parse(process.argv[1]).any' "$RESULT")" >> "$GITHUB_OUTPUT"
echo "matrix=$(node -p 'JSON.stringify(JSON.parse(process.argv[1]).matrix)' "$RESULT")" >> "$GITHUB_OUTPUT"

release:
name: Publish ${{ matrix.name }}@${{ matrix.version }}
Expand Down Expand Up @@ -171,6 +109,12 @@ jobs:
working-directory: ${{ matrix.dir }}
run: pnpm build

- name: Validate package metadata and contents
working-directory: ${{ matrix.dir }}
run: |
pnpm run --if-present check:manifest
pnpm run --if-present check:package

- name: Publish to npm
working-directory: ${{ matrix.dir }}
run: npm publish --access public --tag ${{ matrix.npm_tag }}
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ node_modules/

# Build output
dist/
packages/dsh-web-search-parallel/lib/
*.tsbuildinfo

# Testing
Expand Down Expand Up @@ -36,9 +37,9 @@ yarn-error.log*

# Misc
.cache/
.agent/
temp/
tmp/

# Local opencode plugin scaffolding (npm lockfile in a pnpm repo)
.opencode/

52 changes: 42 additions & 10 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
# Publishing Guide

This monorepo publishes three npm packages, each versioned, tagged, and released **independently**:
This monorepo publishes four npm packages, each versioned, tagged, and released **independently**:

- `@parallel-web/ai-sdk-tools` — `packages/ai-sdk-tools`
- `@parallel-web/dsh-web-search-parallel` — `packages/dsh-web-search-parallel`
- `@parallel-web/opencode-plugin` — `packages/opencode-plugin`
- `@parallel-web/pi-extension` — `packages/pi-extension`

(`@parallel-web/oauth` in `packages/parallel-oauth` is `private` — it is bundled into the
packages above at build time and is never published.)
OpenCode plugin and Pi extension at build time and is never published.)

## How releases work

Releases are **PR-driven** and fully automated on merge — there is no manual "Run workflow"
button to click.
Routine releases are **PR-driven** and automated on merge. The manual exceptions are the
one-time bootstrap for a new npm package and recovery from an interrupted publish.

1. You run `./scripts/release.sh <package> <rc|stable|X.Y.Z>` locally. It bumps the version
in that package's `package.json`, creates a `release/<package>-vX.Y.Z` branch, commits with
message `chore(<package>): bump version to X.Y.Z`, pushes, and opens a PR.
2. You review and merge the PR.
3. `.github/workflows/release.yml` runs on the push to `main`. It detects which
`packages/*/package.json` changed, and for each whose `<package>-vX.Y.Z` git tag does not
yet exist, it builds + lints + type-checks + tests that package, publishes it to npm, creates
the git tag `<package>-vX.Y.Z`, and cuts a GitHub Release.
3. `.github/workflows/release.yml` runs on the push to `main`. It detects existing public
packages whose version changed, and for each whose `<package>-vX.Y.Z` git tag does not yet
exist, it builds + lints + type-checks + tests that package, publishes it to npm, creates the
git tag `<package>-vX.Y.Z`, and cuts a GitHub Release. Adding a package or editing metadata
without changing its version does not publish it.

### Per-package git tags

Expand All @@ -47,6 +49,36 @@ OIDC — **no npm token is stored in the repo**. The workflow upgrades npm to th
first, because trusted publishing requires npm ≥ 11.5.1 (newer than what Node 20 bundles).
Skipping that upgrade causes a misleading `404 Not Found` on the publish `PUT`.

### First release of a new package

npm requires a package to exist before its trusted publisher can be configured. Adding a package
to this repository intentionally does not publish it. An npm organization owner must first publish
the reviewed bootstrap release manually from a clean, updated `main` checkout:

```bash
pnpm install --frozen-lockfile
pnpm --filter @parallel-web/dsh-web-search-parallel check
BOOTSTRAP_DIR="$(mktemp -d)"
pnpm --dir packages/dsh-web-search-parallel pack --pack-destination "$BOOTSTRAP_DIR"
BOOTSTRAP_TARBALL="$(find "$BOOTSTRAP_DIR" -name '*.tgz' -print -quit)"
npm publish "$BOOTSTRAP_TARBALL" --access public --tag rc
npm view @parallel-web/dsh-web-search-parallel dist-tags --json
```

The npm owner should inspect the tarball listing before the publish and complete npm's 2FA prompt.
The explicit `--tag rc` is required: npm otherwise assigns even a prerelease version to `latest`.
The bootstrap intentionally has no git tag or GitHub Release. After it succeeds, configure the
package's trusted publisher for:

- organization: `parallel-web`
- repository: `parallel-npm-packages`
- workflow: `release.yml`
- allowed action: `npm publish`

After that one-time bootstrap, use `scripts/release.sh` for every later RC and stable release; those
releases create the package git tag and GitHub Release normally. Never add a long-lived npm publish
token to this repository.

## Cutting a release

From a clean `main`:
Expand Down Expand Up @@ -83,8 +115,8 @@ git fetch --tags && git tag -l 'ai-sdk-tools-v*'
## Re-publishing / manual trigger

If a publish step failed after the version was already merged (so the tag was never created), use
the workflow's `workflow_dispatch` input to re-run it for a single package at its current
`package.json` version:
the workflow's `workflow_dispatch` input on the `main` branch to re-run it for a single package at
its current `package.json` version. The release detector rejects every other branch or tag:

- Actions → **Release** → **Run workflow** → enter the package directory name (e.g.
`opencode-plugin`).
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Monorepo for @parallel-web npm packages.
## Packages

- [`@parallel-web/ai-sdk-tools`](./packages/ai-sdk-tools) - AI SDK tools for Parallel Web
- [`@parallel-web/dsh-web-search-parallel`](./packages/dsh-web-search-parallel) - Parallel Search provider for DeepSeek Harness
- [`@parallel-web/opencode-plugin`](./packages/opencode-plugin) - Opencode plugin for Parallel Web
- [`@parallel-web/pi-extension`](./packages/pi-extension) - pi agent extension for Parallel Web
- `@parallel-web/oauth` - Internal, unpublished shared PKCE OAuth helper. Bundled into the opencode plugin and pi extension at build time (`noExternal`), so it is never installed by consumers and is intentionally marked `private`.
Expand Down Expand Up @@ -84,7 +85,7 @@ pnpm --filter @parallel-web/ai-sdk-tools test
1. Create a new directory in `packages/`
2. Add a `package.json` with the package name `@parallel-web/package-name`
3. Set up TypeScript config extending from root
4. Add build configuration (tsup)
4. Add build configuration appropriate for the package
5. Implement your package

## Publishing
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"scripts": {
"build": "pnpm -r build",
"test": "pnpm -r test",
"test:ci": "pnpm -r test",
"test:ci": "pnpm -r test && pnpm run test:release-detection",
"test:release-detection": "node --test scripts/detect-releases.test.mjs",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
Expand Down
Loading
Loading