Skip to content

Commit b8ba01e

Browse files
garethxclaude
andcommitted
ci: adopt the sibling plugins' CI and release setup
Modelled on hookdeck/n8n-nodes-hookdeck and hookdeck/hermes-hookdeck, so someone moving between the three finds the same shape. CI gains what it was missing: concurrency with cancel-in-progress, least-privilege permissions, a formatting gate, and — borrowed from n8n, with their reasoning — actionlint over the workflows themselves. An invalid workflow file does not fail loudly; GitHub refuses to validate it and simply never runs it, so a broken publish.yml looks exactly like a release that published nothing. The publish workflow cannot check itself. It also gains a `package` job running the packed-tarball check that already existed but was never wired in. Both siblings have an equivalent, for the same reason: `npm pack` honours `files`, so a runtime file nobody listed is invisible to every test that reads the source tree. integration.yml runs the live suites, skipping rather than failing when the API key secret is absent so fork pull requests stay green, and serialised on a single concurrency group because several assertions read project-wide state. The tunnel suites are manual-dispatch only: they need the CLI and a Gateway, and a broken tunnel is not something a pull request introduces. publish.yml follows n8n's release-driven model. A published GitHub Release is the trigger and the source of truth; the tag is the version, so there is no bump commit and no window where the two disagree. Publishing uses npm trusted publishing with provenance, which is why `registry-url` is deliberately not set on setup-node — it writes an empty credential that npm prefers over OIDC, and trusted publishing then silently fails to engage. A prerelease tag publishes under `beta`. The tarball is attached to the release from a separate job, so the publishing one keeps `contents: read`. Two things this codebase does differently from the siblings, both deliberate: ClawHub is the primary channel and is NOT automated, because the OpenClaw build in use ships no publishing command. Said plainly in the workflow header and CONTRIBUTING rather than left to be discovered. The tunnel job runs `hookdeck ci`, which the plugin itself refuses to run. The objection is about a developer's machine, where it rewrites the shared CLI config and switches the active project; a throwaway runner has no other Hookdeck use. The exception is commented where it is made. The e2e scripts no longer hardcode /usr/local/bin/hookdeck — HOOKDECK_CLI_BIN overrides it, since a runner installs the CLI elsewhere. The absolute default stays, because on a developer machine an npm shim commonly shadows a newer Homebrew build on PATH. The new formatting gate found three unformatted test files on its first run, which is the argument for having it. Workflows validated against the Actions schema locally. 675 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c7b0b6b commit b8ba01e

10 files changed

Lines changed: 488 additions & 21 deletions

File tree

.github/workflows/ci.yml

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,60 @@ on:
44
push:
55
branches: [main]
66
pull_request:
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
# One run per branch or PR. A superseded run proves nothing about the commit
13+
# that replaced it.
14+
concurrency:
15+
group: ci-${{ github.ref }}
16+
cancel-in-progress: true
717

818
jobs:
9-
test:
19+
check:
1020
runs-on: ubuntu-latest
1121
steps:
1222
- uses: actions/checkout@v5
1323
- uses: actions/setup-node@v5
1424
with:
15-
node-version: 22
25+
node-version: 'lts/*'
1626
cache: npm
1727
- run: npm ci
18-
- run: npm run typecheck
28+
29+
# An invalid workflow file does not fail loudly — GitHub refuses to
30+
# validate it and simply never runs it, so a broken publish.yml looks
31+
# exactly like a release that published nothing. Checked here because the
32+
# publish workflow, by definition, cannot check itself.
33+
- name: Lint the workflows
34+
uses: docker://rhysd/actionlint:latest
35+
with:
36+
args: -color
37+
38+
- name: Formatting
39+
run: npx prettier --check "src/**/*.ts" "test/**/*.ts" index.ts
40+
41+
- name: Types
42+
run: npm run typecheck
43+
1944
# Excludes test/live, which needs a Hookdeck project and an API key.
20-
- run: npm test
45+
- name: Tests
46+
run: npm test
47+
48+
package:
49+
# A plugin that installs without its manifest or an entry file registers
50+
# nothing, and the failure is silent — the host simply never loads it.
51+
# `npm pack` honours the `files` field, so a runtime file nobody listed is
52+
# invisible to every other test here: they all read the source tree, where
53+
# it is present. This boots a real Gateway from the packed tarball instead.
54+
runs-on: ubuntu-latest
55+
steps:
56+
- uses: actions/checkout@v5
57+
- uses: actions/setup-node@v5
58+
with:
59+
node-version: 'lts/*'
60+
cache: npm
61+
- run: npm ci
62+
- name: The packed plugin must load and verify a delivery
63+
run: npm run test:package

.github/workflows/integration.yml

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Live tests against the Hookdeck API.
2+
#
3+
# The offline suite drives the plugin against fakes, so it can prove what the
4+
# plugin sends but never what Hookdeck does with it. That gap is where the
5+
# defects were: a bulk replay whose body Hookdeck rejects outright, a catch-up
6+
# filter matching neither disconnect regime, and a crash leaving no record an
7+
# outage happened. All three passed a green offline suite for weeks.
8+
#
9+
# Requires a HOOKDECK_TEST_API_KEY repository secret holding a project API key.
10+
# Without it the job is skipped rather than failed, so pull requests from forks
11+
# — which cannot read secrets — stay green.
12+
#
13+
# The suite creates and deletes real sources, destinations and connections,
14+
# every one prefixed `openclaw-`. Point the secret at a project used for
15+
# nothing else.
16+
name: Integration
17+
18+
on:
19+
push:
20+
branches: [main]
21+
pull_request:
22+
workflow_dispatch:
23+
24+
permissions:
25+
contents: read
26+
27+
# Never two at once against the same project: several assertions read
28+
# project-wide state, and a sibling run creating a source moves the number
29+
# between two reads.
30+
concurrency:
31+
group: integration-hookdeck-project
32+
cancel-in-progress: false
33+
34+
jobs:
35+
api:
36+
runs-on: ubuntu-latest
37+
38+
# Fork pull requests get no secrets, so the job would fail for a reason the
39+
# contributor cannot fix. Skipping keeps the signal honest.
40+
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
41+
42+
steps:
43+
- uses: actions/checkout@v5
44+
- uses: actions/setup-node@v5
45+
with:
46+
node-version: 'lts/*'
47+
cache: npm
48+
- run: npm ci
49+
50+
- name: Skip when no API key is configured
51+
id: guard
52+
env:
53+
KEY: ${{ secrets.HOOKDECK_TEST_API_KEY }}
54+
run: |
55+
if [ -z "$KEY" ]; then
56+
echo "No HOOKDECK_TEST_API_KEY secret; skipping the live suite."
57+
echo "run=false" >> "$GITHUB_OUTPUT"
58+
else
59+
echo "run=true" >> "$GITHUB_OUTPUT"
60+
fi
61+
62+
# The suites read the key from .env.local, the same file a developer
63+
# uses, so there is one code path rather than a CI-only branch.
64+
- name: Write .env.local
65+
if: steps.guard.outputs.run == 'true'
66+
env:
67+
KEY: ${{ secrets.HOOKDECK_TEST_API_KEY }}
68+
run: printf 'HOOKDECK_TEST_API_KEY=%s\n' "$KEY" > .env.local
69+
70+
- name: Live API suite
71+
if: steps.guard.outputs.run == 'true'
72+
run: npm run test:live
73+
74+
# Provider verification needs no tunnel: the source is provisioned by the
75+
# plugin, and a Stripe signature is generated from a secret we choose.
76+
- name: Provider verification, end to end
77+
if: steps.guard.outputs.run == 'true'
78+
run: npm run test:e2e:verification
79+
80+
- name: Remove the key
81+
if: always()
82+
run: rm -f .env.local
83+
84+
# The tunnel suites need the Hookdeck CLI and boot a real Gateway, so they
85+
# are slower and heavier than the API ones. Run on demand rather than on
86+
# every push — a broken tunnel is not something a pull request introduces.
87+
tunnel:
88+
if: github.event_name == 'workflow_dispatch'
89+
runs-on: ubuntu-latest
90+
91+
steps:
92+
- uses: actions/checkout@v5
93+
- uses: actions/setup-node@v5
94+
with:
95+
node-version: 'lts/*'
96+
cache: npm
97+
- run: npm ci
98+
99+
- name: Install the Hookdeck CLI
100+
run: |
101+
npm i -g hookdeck-cli
102+
hookdeck version
103+
104+
- name: Write .env.local
105+
env:
106+
KEY: ${{ secrets.HOOKDECK_TEST_API_KEY }}
107+
run: printf 'HOOKDECK_TEST_API_KEY=%s\n' "$KEY" > .env.local
108+
109+
# `hookdeck listen` needs a CLI session, which is a different credential
110+
# from the API key — the project key alone does not authenticate it.
111+
#
112+
# `hookdeck ci` is the only command that mints one, and the plugin itself
113+
# refuses to run it: against a developer's machine it rewrites the shared
114+
# CLI config and switches the active project for everything else there.
115+
# A throwaway runner has no other Hookdeck use and is discarded after
116+
# the job, so the objection does not apply here. It writes the CLI's
117+
# default config, which is where `hookdeck listen` looks — scoping it
118+
# elsewhere would authenticate a session the tunnel never finds.
119+
- name: Authenticate the CLI
120+
env:
121+
KEY: ${{ secrets.HOOKDECK_TEST_API_KEY }}
122+
run: hookdeck ci --api-key "$KEY"
123+
124+
- name: Dispatch modes, filters and transports
125+
run: npm run test:e2e:dispatch
126+
env:
127+
HOOKDECK_CLI_BIN: hookdeck
128+
129+
- name: The full end-to-end suite
130+
run: npm run test:e2e
131+
env:
132+
HOOKDECK_CLI_BIN: hookdeck
133+
134+
- name: Remove the credentials
135+
if: always()
136+
run: rm -f .env.local ~/.config/hookdeck/config.toml

.github/workflows/publish.yml

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Publishes to npm when a GitHub Release is published.
2+
#
3+
# The release is the source of truth: creating it makes the tag, carries the
4+
# notes people read, and starts this workflow. Nothing publishes from a bare
5+
# tag push, and nothing publishes from a laptop.
6+
#
7+
# ─── WHY A RELEASE AND NOT A TAG ─────────────────────────────────────────────
8+
#
9+
# Driving it from a published Release rather than a tag push means the version,
10+
# the notes and the artifact are decided in one place, and a draft release can
11+
# be prepared and reviewed before anything reaches npm.
12+
#
13+
# ─── PROVENANCE IS NOT TRUSTED PUBLISHING ────────────────────────────────────
14+
#
15+
# Two separate things, easily conflated:
16+
#
17+
# Provenance the signed attestation linking the package to this repo,
18+
# workflow and commit. Needs `id-token: write`, and works
19+
# with either auth method below.
20+
#
21+
# Trusted publishing publishing with a short-lived OIDC token instead of a
22+
# long-lived npm token.
23+
#
24+
# ─── SETUP ───────────────────────────────────────────────────────────────────
25+
#
26+
# Trusted publishers are configured on an *existing* package's settings page,
27+
# so a package that has never been published cannot use OIDC for its first
28+
# release. Claim the name with one version published by hand, then configure
29+
# the publisher; every version after that comes from here, with provenance.
30+
#
31+
# There should be no NPM_TOKEN secret on this repository. npm >= 11.5.1 finds
32+
# the trusted publisher itself and exchanges the Actions OIDC token during
33+
# publish, so there is nothing to inject — and a credential in .npmrc takes
34+
# precedence over OIDC, so a half-configured or empty secret silently becomes
35+
# the publishing identity, or fails.
36+
#
37+
# ─── CLAWHUB ─────────────────────────────────────────────────────────────────
38+
#
39+
# ClawHub is the plugin's primary distribution channel, and it is NOT published
40+
# here: the OpenClaw build in use ships no publishing command (`openclaw
41+
# plugins` has no publish path, and there is no `clawhub` subcommand), so there
42+
# is nothing to automate yet. Publishing there is manual — see CONTRIBUTING.md.
43+
# npm is the second channel, and `openclaw plugins install <npm spec>` works
44+
# from it today.
45+
name: Publish
46+
47+
on:
48+
release:
49+
types: [published]
50+
51+
jobs:
52+
publish:
53+
name: Publish to npm
54+
runs-on: ubuntu-latest
55+
56+
permissions:
57+
# Required to mint an OIDC token for the npm provenance attestation.
58+
id-token: write
59+
# Scoped down from the default for least-privilege publishing.
60+
contents: read
61+
62+
steps:
63+
- uses: actions/checkout@v5
64+
with:
65+
# The release tag, not the default branch: publish exactly what was
66+
# released, even if main has moved on since.
67+
ref: ${{ github.event.release.tag_name }}
68+
69+
# `registry-url` is deliberately not set. It makes setup-node write
70+
# `_authToken=${NODE_AUTH_TOKEN}` into .npmrc, and with no token that is
71+
# an empty credential — which npm prefers over OIDC, so trusted
72+
# publishing silently fails to engage. The default registry is npmjs.
73+
- uses: actions/setup-node@v5
74+
with:
75+
node-version: 'lts/*'
76+
cache: npm
77+
78+
- run: npm ci
79+
80+
# The tag is the version. package.json is not bumped in a commit, so
81+
# there is no release commit to land and no window where the two
82+
# disagree. `--allow-same-version` keeps a first release working when
83+
# package.json already carries the number.
84+
- name: Set the version from the release tag
85+
run: |
86+
VERSION="${GITHUB_REF_NAME#v}"
87+
echo "Publishing $VERSION"
88+
npm version --no-git-tag-version --allow-same-version "$VERSION"
89+
90+
# The same gates CI runs, repeated because this is the last point at
91+
# which a bad package can be stopped. The packed-artifact check matters
92+
# most here: it is the only one that sees what `files` actually ships.
93+
- name: Verify before publishing
94+
run: |
95+
npx prettier --check "src/**/*.ts" "test/**/*.ts" index.ts
96+
npm run typecheck
97+
npm test
98+
npm run test:package
99+
100+
# A pre-release tag (0.2.0-beta.1) publishes under `beta`, so
101+
# `npm install @hookdeck/openclaw` keeps resolving to the last stable
102+
# version.
103+
- name: Choose the npm dist-tag
104+
id: disttag
105+
run: |
106+
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
107+
echo "tag=beta" >> "$GITHUB_OUTPUT"
108+
else
109+
echo "tag=latest" >> "$GITHUB_OUTPUT"
110+
fi
111+
112+
- name: Check npm supports trusted publishing
113+
run: |
114+
npx --yes semver -r '>=11.5.1' "$(npm -v)" >/dev/null \
115+
|| { echo "npm $(npm -v) is too old for OIDC publishing (needs >=11.5.1)"; exit 1; }
116+
117+
- name: Publish
118+
run: npm publish --provenance --access public --tag "${{ steps.disttag.outputs.tag }}"
119+
120+
- name: Keep the published tarball
121+
run: npm pack --pack-destination dist
122+
123+
- uses: actions/upload-artifact@v4
124+
with:
125+
name: tarball
126+
path: dist/
127+
128+
attach:
129+
# A separate job so the publishing one keeps `contents: read`. Writing to
130+
# the release is the only thing here that needs more, and it needs no
131+
# token for npm.
132+
needs: publish
133+
runs-on: ubuntu-latest
134+
permissions:
135+
contents: write
136+
steps:
137+
- uses: actions/download-artifact@v4
138+
with:
139+
name: tarball
140+
path: dist/
141+
142+
# The human created the release, so it has notes but no files. Attach the
143+
# exact tarball that went to npm, so the GitHub release is not a
144+
# description of a build nobody can see.
145+
- env:
146+
GH_TOKEN: ${{ github.token }}
147+
TAG: ${{ github.event.release.tag_name }}
148+
run: gh release upload "$TAG" dist/* --repo "$GITHUB_REPOSITORY" --clobber

0 commit comments

Comments
 (0)