Skip to content

Commit e1a47e4

Browse files
committed
Switch releases to npm trusted publishing; fix multi-word search
You had already made the repo public, so I verified the CLI end to end against the live repo rather than only against the mock: unauthenticated index fetch, a two-skill install with references and scripts, and the installed script executable and running. It works. That also means no NPM_TOKEN is needed. npm supports trusted publishing over OIDC, so release.yml now relies on `id-token: write` and there is no secret to store or rotate, with provenance generated automatically. The one constraint is ordering: trusted publishing cannot be configured for a package that does not exist yet, so the first publish is a local `npm publish` and every release after that is just a tag. The workflow header documents that sequence. Verified before touching the workflow: the name is still free, the tarball is four files and 20 kB, it contains no credential-shaped strings, and installing it into a throwaway prefix produces a working binary — which is what actually catches a bin or shebang problem, since `npm pack` will happily build a tarball whose executable does nothing. The live run also surfaced a real UX bug: `search "revenue at risk"` returned nothing despite cx-revenue-at-risk existing, because it substring-matched the raw query against a hyphenated slug. Search now normalises both sides and requires every token, so the obvious thing to type works — quoted or not. Also removes the README's private-repository caveat, which is no longer true. 149 skills, 213 tests, strict validation clean.
1 parent 15fc556 commit e1a47e4

4 files changed

Lines changed: 82 additions & 27 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,25 @@ name: release
55
# npm version patch # bumps package.json and creates the tag
66
# git push --follow-tags
77
#
8-
# Requires an NPM_TOKEN repository secret (npm → Access Tokens → Automation).
9-
# `id-token: write` lets npm attach a provenance attestation, which links the
10-
# published tarball to this workflow run and this commit.
8+
# This uses npm **trusted publishing** (OIDC), so there is no NPM_TOKEN secret to
9+
# create, store or rotate. `id-token: write` lets GitHub mint a short-lived token
10+
# that npm verifies against the trusted-publisher config on the package.
11+
#
12+
# One-time setup, in this order — trusted publishing cannot be configured for a
13+
# package that does not exist yet:
14+
#
15+
# 1. Publish the first version from your own machine:
16+
# npm login && npm publish --access public
17+
# 2. On npmjs.com → Packages → rulebase-skills → Settings → Trusted publishing,
18+
# add GitHub Actions with:
19+
# organization/user rulebase-co
20+
# repository rulebase-skills
21+
# workflow filename release.yml
22+
# 3. Every release after that is just a tag.
23+
#
24+
# Provenance attestations are generated automatically under trusted publishing —
25+
# no --provenance flag needed. Note that npm does not generate provenance for
26+
# private repositories, so this only produces one once the repo is public.
1127

1228
on:
1329
push:
@@ -25,6 +41,8 @@ jobs:
2541
runs-on: ubuntu-latest
2642
permissions:
2743
contents: read
44+
# Required for trusted publishing. Without it, npm falls back to looking
45+
# for a token and the publish fails.
2846
id-token: write
2947

3048
steps:
@@ -35,6 +53,14 @@ jobs:
3553
node-version: 22
3654
registry-url: 'https://registry.npmjs.org'
3755

56+
# Trusted publishing needs npm >= 11.5.1, which is newer than the npm
57+
# bundled with Node 22. Upgrading explicitly is cheaper than pinning a
58+
# Node version and hoping.
59+
- name: Use an npm that supports trusted publishing
60+
run: |
61+
npm install -g npm@latest
62+
npm --version
63+
3864
# The catalog has to be valid and the index current before anything ships:
3965
# the published CLI reads skills-index.json from the repo, so a stale index
4066
# is a broken release even though the tarball itself is fine.
@@ -61,23 +87,23 @@ jobs:
6187
fi
6288
echo "tag and package.json agree on $PKG"
6389
64-
- name: Pack
90+
# A package whose executable was excluded by `files` installs cleanly and
91+
# then does nothing, so assert the bin is actually in the tarball.
92+
- name: Pack and check the tarball
6593
run: |
66-
npm pack --dry-run
67-
echo "---"
68-
# The bin must be in the tarball or `npx rulebase-skills` resolves to nothing.
94+
set -euo pipefail
6995
npm pack --json | node -e "
7096
const p = JSON.parse(require('fs').readFileSync(0, 'utf8'))[0];
7197
const files = p.files.map((f) => f.path);
72-
if (!files.includes('bin/rulebase-skills.js')) {
73-
console.error('::error::bin/rulebase-skills.js is not in the tarball');
74-
process.exit(1);
98+
for (const required of ['bin/rulebase-skills.js', 'package.json', 'README.md', 'LICENSE']) {
99+
if (!files.includes(required)) {
100+
console.error('::error::' + required + ' is missing from the tarball');
101+
process.exit(1);
102+
}
75103
}
76-
console.error('tarball contains ' + files.length + ' file(s), bin present');
104+
console.error('tarball ok: ' + files.length + ' files, ' + (p.size / 1024).toFixed(1) + ' kB');
77105
"
78106
79107
- name: Publish
80108
if: startsWith(github.ref, 'refs/tags/v')
81-
run: npm publish --access public --provenance
82-
env:
83-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
109+
run: npm publish --access public

‎README.md‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@ Vendor-neutral by design, with no per-helpdesk exporters. Land your conversation
1919
[one canonical shape](skills/data-and-integration/cx-conversation-schema) and every
2020
analysis here runs against it.
2121

22-
> **While this repository is private**, the `npx rulebase-skills` commands below cannot
23-
> reach it — `raw.githubusercontent.com` returns 404 without a token. Use
24-
> `npx skills add rulebase-co/rulebase-skills`, which picks up your `gh auth token`.
25-
> Delete this note when the repository goes public.
26-
2722
## Install
2823

2924
```bash
@@ -61,8 +56,7 @@ so nothing here depends on us guessing right.
6156
<details>
6257
<summary>Other ways in</summary>
6358

64-
The [Vercel `skills` CLI](https://github.com/vercel-labs/skills) also works, and is the
65-
one to use while this repository is private because it can authenticate:
59+
The [Vercel `skills` CLI](https://github.com/vercel-labs/skills) also works:
6660

6761
```bash
6862
npx skills add rulebase-co/rulebase-skills

‎bin/rulebase-skills.js‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,25 @@ async function cmdList() {
169169
out(`${skills.length} of ${index.count} skills. \`npx rulebase-skills info <slug>\` for detail.`);
170170
}
171171

172+
/** Lowercase, and treat every non-alphanumeric run as a space, so a hyphenated
173+
* slug and a spaced query meet in the middle. */
174+
const normalise = (s) => (s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
175+
172176
async function cmdSearch() {
173-
const query = positionals()[0];
174-
if (!query) die('usage: npx rulebase-skills search <query>');
177+
// Everything after the command, so `search revenue at risk` works unquoted.
178+
const words = positionals();
179+
if (words.length === 0) die('usage: npx rulebase-skills search <query>');
180+
const query = words.join(' ');
175181
const index = await loadIndex();
176-
const q = query.toLowerCase();
177-
const hits = index.skills.filter(
178-
(s) => s.slug.toLowerCase().includes(q) || (s.description || '').toLowerCase().includes(q),
179-
);
182+
183+
// All tokens must appear somewhere. Substring-matching the raw query fails on
184+
// "revenue at risk" vs the slug cx-revenue-at-risk, which is the obvious thing
185+
// for someone to type.
186+
const tokens = normalise(query).split(' ').filter(Boolean);
187+
const hits = index.skills.filter((s) => {
188+
const haystack = `${normalise(s.slug)} ${normalise(s.category)} ${normalise(s.description)}`;
189+
return tokens.every((t) => haystack.includes(t));
190+
});
180191
if (hits.length === 0) {
181192
out(`no match for "${query}". \`npx rulebase-skills list\` shows all ${index.count}.`);
182193
return;

‎tests/cli.test.mjs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,30 @@ test('search matches slug and description', async () => {
120120
});
121121
});
122122

123+
test('a spaced query matches a hyphenated slug', async () => {
124+
// The obvious thing to type for cx-revenue-at-risk. Substring-matching the raw
125+
// query against the slug fails here, which is why search tokenises.
126+
await withMockApi(planner(), async ({ base }) => {
127+
const quoted = await runScript(CLI, ['search', 'revenue at risk'], env(base));
128+
assert.match(quoted.stdout, /cx-revenue-at-risk/);
129+
130+
// Unquoted, too — the words arrive as separate argv entries.
131+
const unquoted = await runScript(CLI, ['search', 'revenue', 'at', 'risk'], env(base));
132+
assert.match(unquoted.stdout, /cx-revenue-at-risk/);
133+
});
134+
});
135+
136+
test('search requires every token, not just one', async () => {
137+
await withMockApi(planner(), async ({ base }) => {
138+
const both = await runScript(CLI, ['search', 'complaint', 'deadline'], env(base));
139+
assert.match(both.stdout, /cx-complaints-sla/);
140+
// "complaint" alone matches several; adding "deadline" must narrow it.
141+
const one = await runScript(CLI, ['search', 'complaint'], env(base));
142+
const countOf = (out) => Number((out.match(/(\d+) match/) || [0, 0])[1]);
143+
assert.ok(countOf(one.stdout) > countOf(both.stdout), 'a second token should narrow the result');
144+
});
145+
});
146+
123147
test('info prints the frontmatter and the install line', async () => {
124148
await withMockApi(planner(), async ({ base }) => {
125149
const res = await runScript(CLI, ['info', 'cx-complaints-sla'], env(base));

0 commit comments

Comments
 (0)