Skip to content

Commit 1680671

Browse files
feat: add reusable node publish action
1 parent fce5086 commit 1680671

3 files changed

Lines changed: 231 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,7 @@ jobs:
9292
brew install act
9393
act -l
9494
```
95+
96+
## Additional Action Docs
97+
98+
- [Publish Node Package](docs/publish-node-package.md): Install dependencies, build a package, derive version from the Git tag, and publish to a custom npm registry.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
name: 'Publish Node Package'
2+
description: 'Install, build, version from tag, and publish an npm package to a custom registry.'
3+
4+
inputs:
5+
working_directory:
6+
description: 'Package directory relative to the repository root.'
7+
required: false
8+
default: '.'
9+
node_version:
10+
description: 'Node.js version passed to actions/setup-node.'
11+
required: false
12+
default: '20.x'
13+
cache_dependency_path:
14+
description: 'Lockfile path used by actions/setup-node cache.'
15+
required: false
16+
default: 'package-lock.json'
17+
build_command:
18+
description: 'Command used to build the package. Leave empty to skip.'
19+
required: false
20+
default: 'npm run build'
21+
publish_command:
22+
description: 'Command used to publish the package. Leave empty to skip.'
23+
required: false
24+
default: 'npm publish'
25+
tag_prefix:
26+
description: 'Prefix removed from github.ref_name before writing package.json version.'
27+
required: false
28+
default: 'v'
29+
registry_url:
30+
description: 'Registry URL. Pass from a repository secret.'
31+
required: true
32+
npm_token:
33+
description: 'Registry auth token. Pass from a repository secret.'
34+
required: true
35+
36+
runs:
37+
using: 'composite'
38+
steps:
39+
- name: Setup Node.js
40+
uses: actions/setup-node@v4
41+
with:
42+
node-version: ${{ inputs.node_version }}
43+
cache: npm
44+
cache-dependency-path: ${{ inputs.cache_dependency_path }}
45+
46+
- name: Validate publish configuration
47+
shell: bash
48+
env:
49+
WORKING_DIRECTORY: ${{ inputs.working_directory }}
50+
TAG_PREFIX: ${{ inputs.tag_prefix }}
51+
REGISTRY_URL: ${{ inputs.registry_url }}
52+
NPM_TOKEN: ${{ inputs.npm_token }}
53+
run: |
54+
set -euo pipefail
55+
56+
if [ -z "$WORKING_DIRECTORY" ]; then
57+
echo "Missing required input: working_directory" >&2
58+
exit 1
59+
fi
60+
61+
if [ ! -d "$WORKING_DIRECTORY" ]; then
62+
echo "working_directory does not exist: $WORKING_DIRECTORY" >&2
63+
exit 1
64+
fi
65+
66+
if [ -z "$REGISTRY_URL" ]; then
67+
echo "Missing required input: registry_url" >&2
68+
exit 1
69+
fi
70+
71+
if [ -z "$NPM_TOKEN" ]; then
72+
echo "Missing required input: npm_token" >&2
73+
exit 1
74+
fi
75+
76+
RAW_TAG="${GITHUB_REF_NAME:-}"
77+
if [ -z "$RAW_TAG" ]; then
78+
echo "GITHUB_REF_NAME is empty; this action expects a tag-triggered workflow." >&2
79+
exit 1
80+
fi
81+
82+
if [ -n "$TAG_PREFIX" ] && [[ "$RAW_TAG" != "$TAG_PREFIX"* ]]; then
83+
echo "Tag '$RAW_TAG' does not start with expected prefix '$TAG_PREFIX'." >&2
84+
exit 1
85+
fi
86+
87+
- name: Install dependencies
88+
shell: bash
89+
working-directory: ${{ inputs.working_directory }}
90+
run: |
91+
set -euo pipefail
92+
npm ci
93+
94+
- name: Build package
95+
if: ${{ inputs.build_command != '' }}
96+
shell: bash
97+
working-directory: ${{ inputs.working_directory }}
98+
env:
99+
BUILD_COMMAND: ${{ inputs.build_command }}
100+
run: |
101+
set -euo pipefail
102+
eval "$BUILD_COMMAND"
103+
104+
- name: Set package version from tag
105+
shell: bash
106+
working-directory: ${{ inputs.working_directory }}
107+
env:
108+
TAG_PREFIX: ${{ inputs.tag_prefix }}
109+
run: |
110+
set -euo pipefail
111+
112+
RAW_TAG="${GITHUB_REF_NAME}"
113+
VERSION="$RAW_TAG"
114+
115+
if [ -n "$TAG_PREFIX" ]; then
116+
case "$RAW_TAG" in
117+
"$TAG_PREFIX"*)
118+
VERSION="${RAW_TAG#$TAG_PREFIX}"
119+
;;
120+
*)
121+
echo "Unable to derive package version from tag: $RAW_TAG" >&2
122+
exit 1
123+
;;
124+
esac
125+
fi
126+
127+
if [ -z "$VERSION" ]; then
128+
echo "Derived package version is empty for tag: $RAW_TAG" >&2
129+
exit 1
130+
fi
131+
132+
npm pkg set version="$VERSION"
133+
134+
- name: Configure npm registry
135+
shell: bash
136+
env:
137+
REGISTRY_URL: ${{ inputs.registry_url }}
138+
NPM_TOKEN: ${{ inputs.npm_token }}
139+
run: |
140+
set -euo pipefail
141+
142+
REGISTRY_HOST="${REGISTRY_URL#https://}"
143+
REGISTRY_HOST="${REGISTRY_HOST#http://}"
144+
REGISTRY_HOST="${REGISTRY_HOST%/}"
145+
146+
cat <<EOF > ~/.npmrc
147+
registry=${REGISTRY_URL}
148+
//${REGISTRY_HOST}/:_authToken=${NPM_TOKEN}
149+
always-auth=true
150+
EOF
151+
152+
- name: Publish package
153+
if: ${{ inputs.publish_command != '' }}
154+
shell: bash
155+
working-directory: ${{ inputs.working_directory }}
156+
env:
157+
PUBLISH_COMMAND: ${{ inputs.publish_command }}
158+
NODE_AUTH_TOKEN: ${{ inputs.npm_token }}
159+
run: |
160+
set -euo pipefail
161+
eval "$PUBLISH_COMMAND"

docs/publish-node-package.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Publish Node Package Action
2+
3+
This composite action installs dependencies, builds a Node.js package, derives the package version from the Git tag, configures a custom npm registry, and publishes the package.
4+
5+
## Required Secrets
6+
7+
Configure these repository secrets in the caller repository:
8+
9+
| Name | Description |
10+
|---|---|
11+
| `NPM_REGISTRY_URL` | Registry URL used for publishing |
12+
| `NPM_TOKEN` | Registry auth token |
13+
14+
## Inputs
15+
16+
| Input | Required | Default | Description |
17+
|---|---|---|---|
18+
| `working_directory` | No | `.` | Package directory relative to the repository root |
19+
| `node_version` | No | `20.x` | Node.js version for `actions/setup-node` |
20+
| `cache_dependency_path` | No | `package-lock.json` | Lockfile path used for npm cache |
21+
| `build_command` | No | `npm run build` | Build command. Leave empty to skip |
22+
| `publish_command` | No | `npm publish` | Publish command. Leave empty to skip |
23+
| `tag_prefix` | No | `v` | Prefix removed from `github.ref_name` before writing package version |
24+
| `registry_url` | Yes | None | Registry URL, usually passed from `secrets.NPM_REGISTRY_URL` |
25+
| `npm_token` | Yes | None | Registry token, usually passed from `secrets.NPM_TOKEN` |
26+
27+
## Usage Example
28+
29+
Create or update `.github/workflows/publish.yml` in the caller repository:
30+
31+
```yaml
32+
name: Publish TMLSPEC CLI
33+
34+
on:
35+
push:
36+
tags:
37+
- 'v*'
38+
39+
permissions:
40+
contents: read
41+
42+
jobs:
43+
publish:
44+
runs-on: ubuntu-latest
45+
steps:
46+
- name: Checkout code
47+
uses: actions/checkout@v4
48+
49+
- name: Publish package
50+
uses: Time-Machine-Lab/TML-Github_Actions/actions/publish-node-package@main
51+
with:
52+
working_directory: TMLSPEC-cli
53+
node_version: '20.x'
54+
cache_dependency_path: TMLSPEC-cli/package-lock.json
55+
build_command: npm run build
56+
publish_command: npm publish
57+
tag_prefix: v
58+
registry_url: ${{ secrets.NPM_REGISTRY_URL }}
59+
npm_token: ${{ secrets.NPM_TOKEN }}
60+
```
61+
62+
## Notes
63+
64+
- The caller workflow must run on a tag context if you want the package version to be derived from `github.ref_name`.
65+
- The caller workflow should checkout the repository before invoking this action.
66+
- The action writes `~/.npmrc` on the runner to configure the target registry for the publish step.

0 commit comments

Comments
 (0)