Skip to content

Release: add plugin npm publish workflow#47678

Merged
osolmaz merged 2 commits intoopenclaw:mainfrom
osolmaz:feat/plugin-npm-release-workflow
Mar 18, 2026
Merged

Release: add plugin npm publish workflow#47678
osolmaz merged 2 commits intoopenclaw:mainfrom
osolmaz:feat/plugin-npm-release-workflow

Conversation

@osolmaz
Copy link
Copy Markdown
Contributor

@osolmaz osolmaz commented Mar 15, 2026

Summary

  • add a shared GitHub Actions workflow for @openclaw/* plugin npm publishes
  • mark publishable plugin packages with openclaw.release.publishToNpm and stop enforcing lockstep plugin versions in release:check
  • add plugin release planning/validation helpers and tests, including changed-plugin selection on push to main

Testing

  • pnpm test -- test/plugin-npm-release.test.ts
  • pnpm release:plugins:npm:check
  • pnpm release:plugins:npm:check -- --plugins @openclaw/feishu,@openclaw/zalo
  • pnpm release:plugins:npm:plan -- --plugins @openclaw/feishu,@openclaw/zalo
  • pnpm release:plugins:npm:check -- --base-ref "$(git rev-parse HEAD~1)" --head-ref "$(git rev-parse HEAD)"
  • pnpm release:plugins:npm:plan -- --base-ref "$(git rev-parse HEAD~1)" --head-ref "$(git rev-parse HEAD)"
  • bash scripts/plugin-npm-publish.sh --dry-run /Users/onur/offline/openclaw/extensions/feishu

@osolmaz osolmaz requested a review from a team as a code owner March 15, 2026 23:03
@openclaw-barnacle openclaw-barnacle bot added channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: matrix Channel integration: matrix channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: voice-call Channel integration: voice-call channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser extensions: diagnostics-otel Extension: diagnostics-otel extensions: lobster Extension: lobster scripts Repository scripts channel: feishu Channel integration: feishu size: L maintainer Maintainer-authored PR labels Mar 15, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 15, 2026

Greptile Summary

This PR introduces a two-phase GitHub Actions workflow for publishing @openclaw/* plugin packages to npm, along with the scripts and metadata changes that drive it. On every push to main, the workflow auto-detects changed plugins, validates metadata, and previews what would be published; the actual publish step only fires on manual workflow_dispatch, which is a good safety gate. The PR also removes the old lockstep version enforcement from release-check.ts, allowing plugins to evolve independently on their own YYYY.M.D versioning cadence.

Key changes:

  • .github/workflows/plugin-npm-release.yml: New workflow with preview-only-on-push and publish-on-dispatch pattern. Two run: steps (Preview publish command and Publish) interpolate ${{ matrix.plugin.packageDir }} directly into the shell command string instead of using env vars, which is inconsistent with the Ensure version is not already published step in the same job that correctly uses env:.
  • scripts/lib/plugin-npm-release.ts: Core library with solid logic. The parseReleaseVersion re-check on lines 143–150 is unreachable dead code (the same validation already runs in collectPublishablePluginPackageErrors).
  • scripts/plugin-npm-release-check.ts / plugin-npm-release-plan.ts: Both scripts contain a byte-for-byte identical parseArgs function; extracting it to the shared lib would reduce duplication.
  • Extension package.json files: Correctly mark 12 plugins as publishToNpm: true.
  • test/plugin-npm-release.test.ts: Good coverage of all pure functions in the library.

Confidence Score: 4/5

  • Safe to merge; no logic errors or blocking issues — only style and minor security-hygiene suggestions.
  • The overall design is sound: preview-before-publish, explicit workflow_dispatch gate for actual publishing, null-ref protection for initial pushes, and an unauthenticated npm view check using an empty temporary .npmrc. The only issues are two run: steps that pass packageDir via direct expression interpolation instead of env vars (low-risk given controlled repo context but inconsistent with the same job's own pattern), one block of unreachable dead code, and a duplicated parseArgs function.
  • .github/workflows/plugin-npm-release.yml (lines 155–156 and 201–202) — env-var pattern for packageDir should be made consistent before merge.

Comments Outside Diff (1)

  1. .github/workflows/plugin-npm-release.yml, line 201-202 (link)

    Use env variable to avoid script injection

    ${{ matrix.plugin.packageDir }} is interpolated directly into the shell command. The Ensure version is not already published step above this one correctly passes values through env:, but this step does not. Consistent use of env vars is the recommended practice.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: .github/workflows/plugin-npm-release.yml
    Line: 201-202
    
    Comment:
    **Use env variable to avoid script injection**
    
    `${{ matrix.plugin.packageDir }}` is interpolated directly into the shell command. The `Ensure version is not already published` step above this one correctly passes values through `env:`, but this step does not. Consistent use of env vars is the recommended practice.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: .github/workflows/plugin-npm-release.yml
Line: 155-156

Comment:
**Use env variable to avoid script injection**

`${{ matrix.plugin.packageDir }}` is interpolated directly into the shell command string. While the value comes from controlled repository code today, GitHub's security guidance recommends always passing expression values through environment variables in `run:` steps to guard against future injection paths.

```suggestion
      - name: Preview publish command
        env:
          PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
        run: bash scripts/plugin-npm-publish.sh --dry-run "${PACKAGE_DIR}"
```

The same pattern applies at line 202 in `publish_plugins_npm`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: .github/workflows/plugin-npm-release.yml
Line: 201-202

Comment:
**Use env variable to avoid script injection**

`${{ matrix.plugin.packageDir }}` is interpolated directly into the shell command. The `Ensure version is not already published` step above this one correctly passes values through `env:`, but this step does not. Consistent use of env vars is the recommended practice.

```suggestion
      - name: Publish
        env:
          PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
        run: bash scripts/plugin-npm-publish.sh --publish "${PACKAGE_DIR}"
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: scripts/lib/plugin-npm-release.ts
Line: 143-150

Comment:
**Redundant version re-parse (dead code)**

`collectPublishablePluginPackageErrors` on line 137 already validates the version with `parseReleaseVersion` and, if invalid, adds an error that causes the `continue` on line 140 to fire. That means `parsedVersion` can never be `null` here — this block is unreachable and can be simplified:

```suggestion
    const version = packageJson.version!.trim();
    const parsedVersion = parseReleaseVersion(version)!;
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: scripts/plugin-npm-release-check.ts
Line: 12-52

Comment:
**Duplicated `parseArgs` logic**

The `parseArgs` function here is byte-for-byte identical to the one in `scripts/plugin-npm-release-plan.ts` (lines 6–46). Consider extracting it into `scripts/lib/plugin-npm-release.ts` (e.g. as `parsePluginReleaseArgs`) so both scripts can import and reuse it, reducing maintenance burden.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: ecb092c

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ecb092c567

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@osolmaz osolmaz force-pushed the feat/plugin-npm-release-workflow branch from ecb092c to 8b1779f Compare March 18, 2026 12:28
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 18, 2026

🔒 Aisle Security Analysis

We found 3 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Unpinned GitHub Actions references (tag-based) allow supply-chain compromise
2 🔵 Low Git option/argument injection via unvalidated --base-ref/--head-ref passed to git diff
3 🔵 Low GitHub Actions npm publish runs package lifecycle scripts with OIDC token permission

1. 🟠 Unpinned GitHub Actions references (tag-based) allow supply-chain compromise

Property Value
Severity High
CWE CWE-829
Location .github/workflows/plugin-npm-release.yml:54-58

Description

The workflow uses third-party GitHub Actions by mutable tags rather than immutable commit SHAs.

If an upstream action tag is moved or a maintainer account is compromised, the workflow could execute attacker-controlled code in CI, potentially leading to:

  • Unauthorized publishing to npm (via the publish_plugins_npm job)
  • Exfiltration of CI credentials/OIDC tokens
  • Modification of build artifacts

Vulnerable locations (examples):

  • actions/checkout@​v6 is used multiple times in this workflow.

Vulnerable code:

- name: Checkout
  uses: actions/checkout@​v6

Recommendation

Pin all third-party actions to an immutable commit SHA (and optionally keep the version comment for readability). Example:

- name: Checkout
  uses: actions/checkout@<COMMIT_SHA> # v4.2.2

Also review indirect action usage via local composite actions (e.g., .github/actions/setup-node-env) and pin those uses: references as well (e.g., actions/setup-node, actions/cache, etc.). Consider enabling Dependabot version updates for GitHub Actions to keep SHAs current.


2. 🔵 Git option/argument injection via unvalidated --base-ref/--head-ref passed to git diff

Property Value
Severity Low
CWE CWE-88
Location scripts/lib/plugin-npm-release.ts:309-317

Description

The plugin NPM release tooling accepts --base-ref and --head-ref from CLI args without validation, and passes them directly as positional arguments to git diff.

Because git diff parses options until it reaches a non-option argument, a ref value that starts with - (e.g., --no-index, --output=..., --patch) can be interpreted as a git diff option rather than a revision. This allows an attacker who can control these arguments to change the behavior of the git invocation (option injection), potentially causing:

  • incorrect change detection (release plan/check using the wrong set of changed extensions)
  • unexpected filesystem reads/writes depending on the injected git diff option (e.g., --no-index changes semantics to comparing paths; --output=<file> may write output to a file)
  • potential information exposure in logs if output mode is changed

Relevant locations:

  • argument source: parsePluginReleaseArgs allows arbitrary --base-ref/--head-ref strings with no validation [scripts/lib/plugin-npm-release.ts:94-150]
  • sink: execFileSync('git', ['diff', ... baseRef, headRef, '--', 'extensions']) [scripts/lib/plugin-npm-release.ts:298-323]

Vulnerable code:

execFileSync(
  "git",
  ["diff", "--name-only", "--diff-filter=ACMR", baseRef, headRef, "--", "extensions"],
  { cwd: rootDir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
);

Note: In the provided GitHub workflow, BASE_REF/HEAD_REF appear to be commit SHAs, which reduces exploitability in CI; however, the library/API itself remains vulnerable if invoked with attacker-controlled refs (now or in future automation).

Recommendation

Harden git ref handling to prevent git diff option injection.

Option A (recommended): resolve and validate refs to commit SHAs

  1. Reject values that begin with - or contain whitespace/control characters.
  2. Resolve to a commit SHA using git rev-parse --verify and pass the resolved SHA to git diff.

Example:

function resolveCommitSha(rootDir: string, ref: string): string {
  if (!ref || /^-/.test(ref) || /\s/.test(ref)) {
    throw new Error(`Invalid git ref: ${JSON.stringify(ref)}`);
  }

  return execFileSync(
    "git",
    ["rev-parse", "--verify", `${ref}^{commit}`],
    { cwd: rootDir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
  ).trim();
}

const baseSha = resolveCommitSha(rootDir, baseRef);
const headSha = resolveCommitSha(rootDir, headRef);
const changedPaths = execFileSync(
  "git",
  ["diff", "--name-only", "--diff-filter=ACMR", baseSha, headSha, "--", "extensions"],
  { cwd: rootDir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
).trim().split("\n").filter(Boolean);

Option B: add an explicit end-of-options marker before revisions (and keep a second -- for pathspec), if compatible with your git diff invocation:

["diff", "--name-only", "--diff-filter=ACMR", "--", baseRef, headRef, "--", "extensions"]

Either approach ensures refs cannot be interpreted as options.


3. 🔵 GitHub Actions npm publish runs package lifecycle scripts with OIDC token permission

Property Value
Severity Low
CWE CWE-250
Location scripts/plugin-npm-publish.sh:18-25

Description

The plugin NPM release workflow publishes packages using npm publish without disabling lifecycle scripts.

  • scripts/plugin-npm-publish.sh constructs and executes npm publish ... --provenance without --ignore-scripts ([scripts/plugin-npm-publish.sh:18-25,42-45]).
  • The publishing job grants id-token: write ([.github/workflows/plugin-npm-release.yml:174-182]) and then runs the publish script ([.github/workflows/plugin-npm-release.yml:213-214]).
  • If any published plugin package (e.g., under extensions/*) contains lifecycle scripts such as prepublishOnly, prepare, prepack, etc., npm publish may execute them during publish.
  • With id-token: write, those scripts can access GitHub Actions OIDC environment variables and attempt to mint/exfiltrate an OIDC token or use it to publish other packages configured for trusted publishing.

Vulnerable code:

publish_cmd=(npm publish --access public --provenance)
...
cd "${package_dir}"
"${publish_cmd[@]}"

Note: the current publishable plugin package.json files in this diff do not define a scripts section, but this workflow design remains brittle and becomes exploitable if scripts are added (intentionally or maliciously) to any publishable plugin package.

Recommendation

Disable lifecycle scripts during publishing, and run any required build steps explicitly.

Option A (recommended): add --ignore-scripts to publish:

publish_cmd=(npm publish --access public --provenance --ignore-scripts)
# and for beta:
publish_cmd=(npm publish --access public --tag beta --provenance --ignore-scripts)

Option B: enforce via env var at the workflow step:

- name: Publish
  env:
    npm_config_ignore_scripts: "true"
  run: bash scripts/plugin-npm-publish.sh --publish "${{ matrix.plugin.packageDir }}"

Additionally:

  • Add a CI check to ensure publishable plugin packages do not include lifecycle scripts (fail if package.json.scripts contains pre*publish*, prepare, prepack, etc.).
  • Keep the npm-release environment protected (required reviewers) and restrict who can run workflow_dispatch publishes.

Analyzed PR: #47678 at commit 2a46c1f

Last updated on: 2026-03-18T13:18:07Z

@osolmaz osolmaz merged commit d41c9ad into openclaw:main Mar 18, 2026
43 of 53 checks passed
@osolmaz osolmaz deleted the feat/plugin-npm-release-workflow branch March 18, 2026 12:44
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a46c1f700

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 338 to 340
async function main() {
checkPluginVersions();
checkAppcastSparkleVersions();
await checkPluginSdkExports();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore lockstep checks for installable npm plugins

If a plugin is published independently after this change, older OpenClaw releases will still install that newer package because channel setup passes bare @openclaw/... specs from src/commands/channel-setup/plugin-install.ts:185-191, and installPluginFromNpmSpec in src/plugins/install.ts:707-728 only validates the npm spec format before fetching it. There is no host/plugin compatibility gate anywhere in that path, so removing the checkPluginVersions() call here means a plugin can start using newer openclaw/plugin-sdk exports and then fail to load for stable/beta users on older core versions.

Useful? React with 👍 / 👎.

mrosmarin added a commit to mrosmarin/openclaw that referenced this pull request Mar 18, 2026
* main: (230 commits)
  fix llm-task invalid thinking timeout
  Build: narrow tsdown unresolved import guard
  Plugins: sync contract registry image providers
  Build: fail on unresolved tsdown imports
  Deps: align pi-agent-core for declaration builds
  Build: fail on plugin SDK declaration errors
  Release: add plugin npm publish workflow (openclaw#47678)
  fix(security): block build-tool and glibc env injection vectors in host exec sandbox (openclaw#49702)
  test simplify zero-state boundary guards
  ci enforce boundary guardrails
  test: enable vmForks for targeted channel test runs
  test(telegram): fix incomplete sticker-cache mocks in tests
  Config: align model compat thinking format types
  Tlon: pin api-beta to current known-good commit
  Plugin SDK: harden provider auth seams
  fix(config): add missing qwen-chat-template to thinking format schema
  Plugin SDK: register provider auth login entrypoint
  Plugin SDK: split provider auth login seam
  fix: serialize duplicate channel starts (openclaw#49583) (thanks @sudie-codes)
  Telegram: fix reply-runtime test typings
  ...
livingghost pushed a commit to livingghost/openclaw that referenced this pull request Mar 18, 2026
* Release: add plugin npm publish workflow

* Release: make plugin publish scope explicit
livingghost pushed a commit to livingghost/openclaw that referenced this pull request Mar 18, 2026
* Release: add plugin npm publish workflow

* Release: make plugin publish scope explicit
livingghost pushed a commit to livingghost/openclaw that referenced this pull request Mar 18, 2026
* Release: add plugin npm publish workflow

* Release: make plugin publish scope explicit
brandontyler pushed a commit to brandontyler/clawdbot that referenced this pull request Mar 19, 2026
* Release: add plugin npm publish workflow

* Release: make plugin publish scope explicit
fuller-stack-dev pushed a commit to fuller-stack-dev/openclaw that referenced this pull request Mar 20, 2026
* Release: add plugin npm publish workflow

* Release: make plugin publish scope explicit
fuller-stack-dev pushed a commit to fuller-stack-dev/openclaw that referenced this pull request Mar 20, 2026
* Release: add plugin npm publish workflow

* Release: make plugin publish scope explicit
pholpaphankorn pushed a commit to pholpaphankorn/openclaw that referenced this pull request Mar 22, 2026
* Release: add plugin npm publish workflow

* Release: make plugin publish scope explicit
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 25, 2026
* Release: add plugin npm publish workflow

* Release: make plugin publish scope explicit

(cherry picked from commit d41c9ad)
alexey-pelykh added a commit to remoteclaw/remoteclaw that referenced this pull request Mar 25, 2026
* fix(ci): stop serializing push workflow runs

(cherry picked from commit 0a20c5c)

* test: harden path resolution test helpers

(cherry picked from commit 1ad47b8)

* Fix launcher startup regressions (openclaw#48501)

* Fix launcher startup regressions

* Fix CI follow-up regressions

* Fix review follow-ups

* Fix workflow audit shell inputs

* Handle require resolve gaxios misses

(cherry picked from commit 313e5bb)

* refactor(scripts): move container setup entrypoints

(cherry picked from commit 46ccbac)

* perf(ci): gate install smoke on changed-smoke (openclaw#52458)

(cherry picked from commit 4bd90f2)

* Docs: prototype generated plugin SDK reference (openclaw#51877)

* Chore: unblock synced main checks

* Docs: add plugin SDK docs implementation plan

* Docs: scaffold plugin SDK reference phase 1

* Docs: mark plugin SDK reference surfaces unstable

* Docs: prototype generated plugin SDK reference

* docs(plugin-sdk): replace generated reference with api baseline

* docs(plugin-sdk): drop generated reference plan

* docs(plugin-sdk): align api baseline flow with config docs

---------

Co-authored-by: Onur <[email protected]>
Co-authored-by: Vincent Koc <[email protected]>
(cherry picked from commit 4f1e12a)

* fix(ci): harden docker builds and unblock config docs

(cherry picked from commit 9f08af1)

* Docs: add config drift baseline statefile (openclaw#45891)

* Docs: add config drift statefile generator

* Docs: generate config drift baseline

* CI: move config docs drift runner into workflow sanity

* Docs: emit config drift baseline json

* Docs: commit config drift baseline json

* Docs: wire config baseline into release checks

* Config: fix baseline drift walker coverage

* Docs: regenerate config drift baselines

(cherry picked from commit cbec476)

* Release: add plugin npm publish workflow (openclaw#47678)

* Release: add plugin npm publish workflow

* Release: make plugin publish scope explicit

(cherry picked from commit d41c9ad)

* build: default to Node 24 and keep Node 22 compat

(cherry picked from commit deada7e)

* ci(android): use explicit flavor debug tasks

(cherry picked from commit 0c2e6fe)

* ci: harden pnpm sticky cache on PRs

(cherry picked from commit 29b36f8)

* CI: add built plugin singleton smoke (openclaw#48710)

(cherry picked from commit 5a2a4ab)

* chore: add code owners for npm release paths

(cherry picked from commit 5c9fae5)

* test add extension plugin sdk boundary guards

(cherry picked from commit 77fb258)

* ci: tighten cache docs and node22 gate

(cherry picked from commit 797b6fe)

* ci: add npm release workflow and CalVer checks (openclaw#42414) (thanks @onutc)

(cherry picked from commit 8ba1b6e)

* CI: add CLI startup memory regression check

(cherry picked from commit c0e0115)

* Add bad-barnacle label to prevent barnacle closures. (openclaw#51945)

(cherry picked from commit c449a0a)

* ci: speed up scoped workflow lanes

(cherry picked from commit d17490f)

* ci: restore PR pnpm cache fallback

(cherry picked from commit e1d0545)

* CI: guard gateway watch against duplicate runtime regressions (openclaw#49048)

(cherry picked from commit f036ed2)

* fix: correct domain reference in docker setup script

* fix: adapt cherry-picks for fork TS strictness

* fix: adapt cherry-picked tests for fork structure

- Dockerfile test: OPENCLAW_ → REMOTECLAW_ ARG names
- ci-changed-scope test: add missing runChangedSmoke field
- doc-baseline test: rename to e2e (needs dist/ build artifacts)
- extension boundary test: update baselines and expectations for fork

* fix: adjust ci-changed-scope test for fork's narrower skills regex

---------

Co-authored-by: Vincent Koc <[email protected]>
Co-authored-by: Peter Steinberger <[email protected]>
Co-authored-by: Tak Hoffman <[email protected]>
Co-authored-by: Bob <[email protected]>
Co-authored-by: Onur <[email protected]>
Co-authored-by: Altay <[email protected]>
Co-authored-by: Ayaan Zaidi <[email protected]>
Co-authored-by: Onur Solmaz <[email protected]>
Co-authored-by: Harold Hunt <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: matrix Channel integration: matrix channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: voice-call Channel integration: voice-call channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser extensions: diagnostics-otel Extension: diagnostics-otel extensions: lobster Extension: lobster maintainer Maintainer-authored PR scripts Repository scripts size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant