Merge pull request #9 from walter-base/feat/npm-registry-publishing

This commit is contained in:
Walter Cheng
2026-07-26 15:33:44 -04:00
committed by GitHub
2 changed files with 162 additions and 0 deletions
@@ -98,6 +98,29 @@ on:
required: false
type: string
default: ""
publish_npm:
description: >-
Publish the release contents as an npm package so agents that support npm
marketplace catalogs can install and auto-update from the registry.
required: false
type: boolean
default: false
npm_package_name:
description: >-
Scoped npm package name, for example `@acme/agent-skills`. A scope is
required because it is what routes installs to the registry, but it does
not have to match the registry owner. Required when publish_npm is enabled.
required: false
type: string
default: ""
npm_registry:
description: >-
npm registry base URL, for example
`https://gitea.example.com/api/packages/acme/npm/`. Defaults to the Gitea
npm registry for gitea_user when gitea_url is set.
required: false
type: string
default: ""
secrets:
GITHUB_PAT:
description: Optional GitHub token for release-please pull requests.
@@ -105,6 +128,11 @@ on:
GITEA_TOKEN:
description: Gitea token with permission to publish generic packages.
required: false
NPM_TOKEN:
description: >-
Token for the npm registry. Falls back to GITEA_TOKEN when unset, which
covers publishing to a Gitea npm registry.
required: false
permissions:
contents: write
@@ -166,6 +194,93 @@ jobs:
done
tar -czf "dist/${ARCHIVE_NAME}-${VERSION}.tar.gz" -C dist "${dir}"
- name: Set up Node
if: inputs.publish_npm
uses: actions/setup-node@v4
with:
node-version: 20
- name: Publish npm package
if: inputs.publish_npm
id: npm
env:
ARCHIVE_NAME: ${{ inputs.archive_name }}
GITEA_URL: ${{ inputs.gitea_url }}
GITEA_USER: ${{ inputs.gitea_user }}
NPM_PACKAGE_NAME: ${{ inputs.npm_package_name }}
NPM_REGISTRY: ${{ inputs.npm_registry }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN || secrets.GITEA_TOKEN }}
PACKAGE_PATHS: ${{ inputs.package_paths }}
VERSION: ${{ needs.release-please.outputs.version }}
shell: bash
run: |
set -euo pipefail
: "${NPM_PACKAGE_NAME:?Set npm_package_name when publish_npm is enabled}"
: "${NPM_TOKEN:?Pass NPM_TOKEN or GITEA_TOKEN when publish_npm is enabled}"
registry="${NPM_REGISTRY}"
if [[ -z "$registry" ]]; then
: "${GITEA_URL:?Set npm_registry, or gitea_url and gitea_user, when publish_npm is enabled}"
: "${GITEA_USER:?Set npm_registry, or gitea_url and gitea_user, when publish_npm is enabled}"
registry="${GITEA_URL%/}/api/packages/${GITEA_USER}/npm/"
fi
[[ "$registry" == */ ]] || registry="${registry}/"
scope="${NPM_PACKAGE_NAME%%/*}"
if [[ "$scope" != @* ]]; then
echo "npm_package_name must be scoped (for example @acme/agent-skills) so the registry mapping is unambiguous" >&2
exit 1
fi
# The staged directory already holds exactly the released content, so the npm
# package and the generic archive can never drift apart.
pkg_dir="dist/${ARCHIVE_NAME}-${VERSION}"
files_json="$(printf '%s\n' ${PACKAGE_PATHS} | jq -R . | jq -s .)"
jq -n \
--arg name "$NPM_PACKAGE_NAME" \
--arg version "$VERSION" \
--argjson files "$files_json" \
'{name: $name, version: $version, files: $files, private: false}' \
> "${pkg_dir}/package.json"
# `npm pack <path>` without a leading `./` is parsed as a GitHub owner/repo
# spec, so npm tries to clone it instead of packing the directory.
tarball_name="$(npm pack --pack-destination dist "./${pkg_dir}")"
: "${tarball_name:?npm pack did not produce a tarball}"
tarball="dist/${tarball_name}"
# npm's default ignore rules have historically surprised people with
# dot-directories, and the agent catalogs live in `.agents/` and
# `.claude-plugin/`. Fail here rather than shipping an unusable package.
contents="$(tar -tzf "$tarball")"
missing=0
for path in ${PACKAGE_PATHS}; do
[[ -e "$path" ]] || continue
if ! grep -q "^package/${path}" <<<"$contents"; then
echo "npm package is missing released path: ${path}" >&2
missing=1
fi
done
if [[ "$missing" -ne 0 ]]; then
echo "Refusing to publish an npm package that does not match the release archive." >&2
exit 1
fi
registry_auth="${registry#https://}"
registry_auth="${registry_auth#http://}"
{
printf '%s:registry=%s\n' "$scope" "$registry"
printf '//%s:_authToken=%s\n' "$registry_auth" "$NPM_TOKEN"
} > "$HOME/.npmrc"
trap 'rm -f "$HOME/.npmrc"' EXIT
npm publish "./${tarball}" --registry "$registry"
# Keep the packed tarball out of the GitHub Release upload glob.
rm -f "$tarball"
echo "npm_package=${NPM_PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
echo "npm_registry=${registry}" >> "$GITHUB_OUTPUT"
- name: Build Gitea installers
if: inputs.publish_gitea
env:
@@ -363,11 +478,24 @@ jobs:
CODEX_MARKETPLACE_NAME: ${{ inputs.codex_marketplace_name }}
CURSOR_PLUGIN_NAME: ${{ inputs.cursor_plugin_name }}
GITEA_INSTALLER_URL: ${{ steps.gitea.outputs.installer_url }}
NPM_PACKAGE: ${{ steps.npm.outputs.npm_package }}
NPM_REGISTRY_URL: ${{ steps.npm.outputs.npm_registry }}
run: |
{
printf '# Agent plugin %s\n\n' "$VERSION"
printf 'Release tag: `%s`\n\n' "$TAG"
if [[ -n "$NPM_PACKAGE" ]]; then
printf '## npm registry\n\n'
printf 'Published `%s@%s` to `%s`.\n\n' "$NPM_PACKAGE" "$VERSION" "$NPM_REGISTRY_URL"
printf 'Agents that support npm marketplace catalogs can install this package '
printf 'directly and pick up later releases without re-running an installer. '
printf 'Authenticate first if the registry is private:\n\n'
printf '%s\n' '```bash'
printf 'npm config set %s:registry %s\n' "${NPM_PACKAGE%%/*}" "$NPM_REGISTRY_URL"
printf '%s\n\n' '```'
fi
if [[ -n "$GITEA_INSTALLER_URL" ]]; then
printf '## Claude Code\n\n'
printf '%s\n' '```bash'
+34
View File
@@ -8,6 +8,7 @@ This repository intentionally contains only generic marketplace automation:
- skill and plugin manifest validation
- pull-request preview archives
- release archives and installation summaries
- optional npm registry publishing for agents that support npm catalogs
Container, infrastructure, deployment, and environment-specific workflows are
kept in the private `Walter0697/common-workflow` repository.
@@ -28,3 +29,36 @@ The validator is also available as:
```yaml
uses: walter-base/common-workflow/actions/validate-agent-marketplace@master
```
## Publishing to an npm registry
The generic archive and its installer are opaque to coding agents: something has
to download and unpack them, which is what the generated installer script does.
Agents can, however, resolve npm packages on their own, so publishing the same
release contents to an npm registry lets them install and update without an
installer.
Enable it on the release workflow:
```yaml
with:
publish_npm: true
npm_package_name: "@acme/agent-skills"
# npm_registry defaults to the Gitea npm registry for gitea_user
secrets:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
```
Notes:
- the package is built from the same staged directory as the generic archive, so
the two can never drift apart
- `package.json` is generated at publish time with the release version, which
keeps it in step with the plugin manifest versions that release-please bumps
- `npm_package_name` must be scoped, because the scope is what routes installs to
the registry in `.npmrc`. The scope does not have to match the registry owner:
Gitea's own example maps an `@test` scope to a `testuser` registry URL
- `NPM_TOKEN` is used when set; otherwise `GITEA_TOKEN` is reused
- publishing fails if any released path is absent from the packed tarball, which
guards against npm's ignore rules silently dropping `.agents/` or
`.claude-plugin/`