Skip to content

Commit 9029b12

Browse files
committed
fix: make skill publish idempotent by name/version
Signed-off-by: Michael Kantor <6068672+kantorcodes@users.noreply.github.com>
1 parent 861bd6e commit 9029b12

File tree

2 files changed

+104
-10
lines changed

2 files changed

+104
-10
lines changed

README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ Canonical docs and API surfaces:
1010
- Registry landing page: https://hol.org/registry
1111
- Skill index: https://hol.org/registry/skills
1212
- Skill manifest schema: https://raw.githubusercontent.com/hashgraph-online/skill-publish/main/schemas/skill.schema.json
13-
- API docs: https://hol.org/registry/docs
13+
- Product docs: https://hol.org/docs/registry-broker/
14+
- Interactive API docs: https://hol.org/registry/docs
1415
- OpenAPI schema: https://hol.org/registry/api/v1/openapi.json
16+
- Live stats endpoint: https://hol.org/registry/api/v1/dashboard/stats
1517
- APIs.json metadata: https://hol.org/registry/apis.json
18+
- Repository APIs.json: https://raw.githubusercontent.com/hashgraph-online/skill-publish/main/apis.json
19+
- Repository LLM index: https://raw.githubusercontent.com/hashgraph-online/skill-publish/main/llms.txt
1620

1721
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.18748325.svg?style=for-the-badge)](https://doi.org/10.5281/zenodo.18748325)
1822
[![HOL Registry](https://img.shields.io/badge/HOL-Registry-5599FE?style=for-the-badge)](https://hol.org/registry)
@@ -54,6 +58,8 @@ jobs:
5458

5559
- Zenodo metadata: [`.zenodo.json`](./.zenodo.json)
5660
- Citation metadata: [`CITATION.cff`](./CITATION.cff)
61+
- Schema validation workflow: [`.github/workflows/schema-validate.yml`](./.github/workflows/schema-validate.yml)
62+
- Release workflow: [`.github/workflows/release.yml`](./.github/workflows/release.yml)
5763

5864
## Required secret
5965

@@ -88,11 +94,12 @@ jobs:
8894
The action:
8995

9096
1. Validates package files and `/skills/config` constraints.
91-
2. Calls `POST /skills/quote`.
92-
3. Calls `POST /skills/publish`.
93-
4. Polls `GET /skills/jobs/{jobId}` until completion.
94-
5. Stamps `repo` and `commit` metadata in `skill.json` payload by default.
95-
6. Appends publish result details to release notes (release events) or merged PR comments (push to `main`) when annotation is enabled.
97+
2. Checks whether the exact `name@version` already exists and skips publish when it does.
98+
3. Calls `POST /skills/quote` when publish is needed.
99+
4. Calls `POST /skills/publish`.
100+
5. Polls `GET /skills/jobs/{jobId}` until completion.
101+
6. Stamps `repo` and `commit` metadata in `skill.json` payload by default.
102+
7. Appends publish result details to release notes (release events) or merged PR comments (push to `main`) when annotation is enabled.
96103

97104
## Cite this repository
98105

entrypoint.mjs

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,33 @@ const requestJson = async (params) => {
176176
return response.json();
177177
};
178178

179+
const findExistingSkillVersion = async (params) => {
180+
const { apiBaseUrl, apiKey, name, version } = params;
181+
const response = await requestJson({
182+
method: 'GET',
183+
url: buildApiUrl(apiBaseUrl, '/skills/list', {
184+
name,
185+
version,
186+
limit: 20,
187+
}),
188+
apiKey,
189+
});
190+
191+
const items = Array.isArray(response?.items) ? response.items : [];
192+
for (const item of items) {
193+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
194+
continue;
195+
}
196+
const itemName = typeof item.name === 'string' ? item.name.trim() : '';
197+
const itemVersion = typeof item.version === 'string' ? item.version.trim() : '';
198+
if (itemName === name && itemVersion === version) {
199+
return item;
200+
}
201+
}
202+
203+
return null;
204+
};
205+
179206
const parseEventPayload = async () => {
180207
const eventPath = getEnv('GITHUB_EVENT_PATH');
181208
if (!eventPath) {
@@ -237,15 +264,19 @@ const buildPublishMarkdown = (result) => {
237264
const lines = [];
238265
lines.push('### HCS-26 skill publish result');
239266
lines.push('');
267+
lines.push(`- Status: \`${result.published === false ? 'skipped' : 'published'}\``);
240268
lines.push(`- Name: \`${result.skillName}\``);
241269
lines.push(`- Version: \`${result.skillVersion}\``);
242-
lines.push(`- Quote ID: \`${result.quoteId}\``);
243-
lines.push(`- Job ID: \`${result.jobId}\``);
270+
lines.push(`- Quote ID: \`${result.quoteId || 'n/a'}\``);
271+
lines.push(`- Job ID: \`${result.jobId || 'n/a'}\``);
244272
lines.push(`- Directory Topic: \`${result.directoryTopicId ?? 'n/a'}\``);
245273
lines.push(`- Package Topic: \`${result.packageTopicId ?? 'n/a'}\``);
246274
lines.push(`- skill.json HRL: \`${result.skillJsonHrl ?? 'n/a'}\``);
247-
lines.push(`- Credits: \`${result.credits}\``);
248-
lines.push(`- Estimated Cost: \`${result.estimatedCostHbar} HBAR\``);
275+
lines.push(`- Credits: \`${result.credits ?? 0}\``);
276+
lines.push(`- Estimated Cost: \`${result.estimatedCostHbar ?? '0'} HBAR\``);
277+
if (result.published === false && result.skippedReason) {
278+
lines.push(`- Skip reason: \`${result.skippedReason}\``);
279+
}
249280
lines.push('');
250281
lines.push(`- Repo: \`${result.repoUrl ?? 'n/a'}\``);
251282
lines.push(`- Commit: \`${result.commitSha ?? 'n/a'}\``);
@@ -417,6 +448,62 @@ const run = async () => {
417448
throw new ActionError('skill.json must include description.');
418449
}
419450

451+
const existingVersion = await findExistingSkillVersion({
452+
apiBaseUrl,
453+
apiKey,
454+
name: skillName,
455+
version: skillVersion,
456+
});
457+
458+
if (existingVersion) {
459+
const result = {
460+
skillName,
461+
skillVersion,
462+
quoteId: '',
463+
jobId: '',
464+
directoryTopicId:
465+
typeof existingVersion.directoryTopicId === 'string'
466+
? existingVersion.directoryTopicId
467+
: null,
468+
packageTopicId:
469+
typeof existingVersion.packageTopicId === 'string'
470+
? existingVersion.packageTopicId
471+
: typeof existingVersion.versionRegistryTopicId === 'string'
472+
? existingVersion.versionRegistryTopicId
473+
: null,
474+
skillJsonHrl:
475+
typeof existingVersion.skillJsonHrl === 'string'
476+
? existingVersion.skillJsonHrl
477+
: typeof existingVersion.manifestHrl === 'string'
478+
? existingVersion.manifestHrl
479+
: null,
480+
credits: 0,
481+
estimatedCostHbar: '0',
482+
repoUrl: repoUrl || null,
483+
commitSha: commitSha || null,
484+
published: false,
485+
skippedReason: 'version-exists',
486+
};
487+
488+
stdout(`Skill version ${skillName}@${skillVersion} already exists. Skipping publish.`);
489+
490+
const markdown = buildPublishMarkdown(result);
491+
await appendStepSummary(markdown);
492+
493+
await setActionOutput('skill-name', result.skillName);
494+
await setActionOutput('skill-version', result.skillVersion);
495+
await setActionOutput('quote-id', '');
496+
await setActionOutput('job-id', '');
497+
await setActionOutput('directory-topic-id', result.directoryTopicId ?? '');
498+
await setActionOutput('package-topic-id', result.packageTopicId ?? '');
499+
await setActionOutput('skill-json-hrl', result.skillJsonHrl ?? '');
500+
await setActionOutput('annotation-target', 'none');
501+
await setActionOutput('result-json', JSON.stringify(result, null, 2));
502+
503+
stdout(markdown);
504+
return;
505+
}
506+
420507
const config = await requestJson({
421508
method: 'GET',
422509
url: buildApiUrl(apiBaseUrl, '/skills/config'),

0 commit comments

Comments
 (0)