chore: remove gstack-publish — no real user need

User feedback: "i don't think i would use gstack-publish, i think we
should remove it." Agreed. The CLI + marketplace wiring was an
ambitious but speculative primitive. Zero users, zero validated demand,
and the existing manual `clawhub publish` workflow already covers the
real case (OpenClaw methodology skill publishing).

Deleted:
- bin/gstack-publish (the CLI)
- skills.json (the marketplace manifest)
- test/publish-dry-run.test.ts (13 tests)
- ship/SKILL.md.tmpl Step 19.5 — the methodology-skill publish-on-ship
  check. No target to dispatch to anymore.
- README.md Power tools row for gstack-publish

Updated:
- bin/gstack-model-benchmark doc comment: dropped "matches gstack-publish
  --dry-run semantics" reference (self-describing flag now)
- CHANGELOG 1.3.0.0 entry:
  * Release summary: "three new binaries" → "two new binaries".
    Dropped the /ship publish-check narrative.
  * Numbers table: "1 of 3 → 3 of 3 wired" → "1 of 2 → 2 of 2 wired".
    Deterministic test count: 45 → 32 (removed publish-dry-run's 13).
  * Added section: removed gstack-publish CLI bullet + /ship Step 19.5
    bullet.
  * "What this means for users" closer: replaced the /ship publish
    paragraph with the design-taste-engine learning loop, which IS
    real, wired, and something users hit every week via /design-shotgun.
  * Contributors section: "Four new test files" → "Three new test files"

Retained:
- openclaw/skills/gstack-openclaw-* skill dirs (pre-existed this PR,
  still publishable manually via `clawhub publish`, useful standalone
  for ClawHub installs)
- CLAUDE.md publishing-native-skills section (same rationale)

Regenerated SKILL.md across all hosts. Ship golden fixtures refreshed
for claude/codex/factory. 455 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-19 09:02:13 +08:00
parent 08486bbf8b
commit 8af68207f5
11 changed files with 12 additions and 805 deletions
+1 -32
View File
@@ -2930,38 +2930,7 @@ EOF
**If neither CLI is available:**
Print the branch name, remote URL, and instruct the user to create the PR/MR manually via the web UI. Do not stop — the code is pushed and ready.
**Output the PR/MR URL** — then proceed to Step 19.5.
---
## Step 19.5: Offer methodology skill publishing (conditional)
If this PR touched any standalone methodology skill (`openclaw/skills/gstack-*/SKILL.md`) or the marketplace manifest (`skills.json`), offer to publish the updated skills to the configured marketplaces after merge.
```bash
git diff origin/<base>...HEAD --name-only | grep -E '^(openclaw/skills/gstack-[^/]+/SKILL\.md|skills\.json)$' || true
```
If the output is empty: skip this step silently. Continue to Step 20.
If any matches: run a dry-run preview so the user sees exactly what would publish and what auth is missing.
```bash
~/.claude/skills/gstack/bin/gstack-publish --dry-run
```
Then use AskUserQuestion with the structure in the preamble:
- **Re-ground:** project, branch, PR just opened.
- **Simplify:** "This PR updated methodology skills. gstack-publish can push them to ClawHub, SkillsMP, and Vercel Skills.sh so other developers on other agents can install them. Dry-run above shows what would ship and whether each marketplace is authed."
- **RECOMMENDATION:** A because the dry-run just verified the manifest and one-command distribution is the whole point of having a marketplace publisher.
- **Options:**
- A) Publish now — run `gstack-publish` (human: ~2min / CC: ~30s). Completeness: 9/10.
- B) Publish after the PR merges — `/document-release` won't push this automatically; you'll run `gstack-publish` manually once the branch is on main. Completeness: 7/10.
- C) Skip — don't publish this release. Completeness: 4/10.
If A: run `~/.claude/skills/gstack/bin/gstack-publish` (no --dry-run). Report success/failure per marketplace. If any marketplace auth is missing, the dry-run above surfaced it; the real publish will skip that marketplace with an isolated error rather than aborting the batch.
If B or C: continue to Step 20.
**Output the PR/MR URL** — then proceed to Step 20.
---
+1 -32
View File
@@ -2545,38 +2545,7 @@ EOF
**If neither CLI is available:**
Print the branch name, remote URL, and instruct the user to create the PR/MR manually via the web UI. Do not stop — the code is pushed and ready.
**Output the PR/MR URL** — then proceed to Step 19.5.
---
## Step 19.5: Offer methodology skill publishing (conditional)
If this PR touched any standalone methodology skill (`openclaw/skills/gstack-*/SKILL.md`) or the marketplace manifest (`skills.json`), offer to publish the updated skills to the configured marketplaces after merge.
```bash
git diff origin/<base>...HEAD --name-only | grep -E '^(openclaw/skills/gstack-[^/]+/SKILL\.md|skills\.json)$' || true
```
If the output is empty: skip this step silently. Continue to Step 20.
If any matches: run a dry-run preview so the user sees exactly what would publish and what auth is missing.
```bash
$GSTACK_ROOT/bin/gstack-publish --dry-run
```
Then use AskUserQuestion with the structure in the preamble:
- **Re-ground:** project, branch, PR just opened.
- **Simplify:** "This PR updated methodology skills. gstack-publish can push them to ClawHub, SkillsMP, and Vercel Skills.sh so other developers on other agents can install them. Dry-run above shows what would ship and whether each marketplace is authed."
- **RECOMMENDATION:** A because the dry-run just verified the manifest and one-command distribution is the whole point of having a marketplace publisher.
- **Options:**
- A) Publish now — run `gstack-publish` (human: ~2min / CC: ~30s). Completeness: 9/10.
- B) Publish after the PR merges — `/document-release` won't push this automatically; you'll run `gstack-publish` manually once the branch is on main. Completeness: 7/10.
- C) Skip — don't publish this release. Completeness: 4/10.
If A: run `$GSTACK_ROOT/bin/gstack-publish` (no --dry-run). Report success/failure per marketplace. If any marketplace auth is missing, the dry-run above surfaced it; the real publish will skip that marketplace with an isolated error rather than aborting the batch.
If B or C: continue to Step 20.
**Output the PR/MR URL** — then proceed to Step 20.
---
+1 -32
View File
@@ -2921,38 +2921,7 @@ EOF
**If neither CLI is available:**
Print the branch name, remote URL, and instruct the user to create the PR/MR manually via the web UI. Do not stop — the code is pushed and ready.
**Output the PR/MR URL** — then proceed to Step 19.5.
---
## Step 19.5: Offer methodology skill publishing (conditional)
If this PR touched any standalone methodology skill (`openclaw/skills/gstack-*/SKILL.md`) or the marketplace manifest (`skills.json`), offer to publish the updated skills to the configured marketplaces after merge.
```bash
git diff origin/<base>...HEAD --name-only | grep -E '^(openclaw/skills/gstack-[^/]+/SKILL\.md|skills\.json)$' || true
```
If the output is empty: skip this step silently. Continue to Step 20.
If any matches: run a dry-run preview so the user sees exactly what would publish and what auth is missing.
```bash
$GSTACK_ROOT/bin/gstack-publish --dry-run
```
Then use AskUserQuestion with the structure in the preamble:
- **Re-ground:** project, branch, PR just opened.
- **Simplify:** "This PR updated methodology skills. gstack-publish can push them to ClawHub, SkillsMP, and Vercel Skills.sh so other developers on other agents can install them. Dry-run above shows what would ship and whether each marketplace is authed."
- **RECOMMENDATION:** A because the dry-run just verified the manifest and one-command distribution is the whole point of having a marketplace publisher.
- **Options:**
- A) Publish now — run `gstack-publish` (human: ~2min / CC: ~30s). Completeness: 9/10.
- B) Publish after the PR merges — `/document-release` won't push this automatically; you'll run `gstack-publish` manually once the branch is on main. Completeness: 7/10.
- C) Skip — don't publish this release. Completeness: 4/10.
If A: run `$GSTACK_ROOT/bin/gstack-publish` (no --dry-run). Report success/failure per marketplace. If any marketplace auth is missing, the dry-run above surfaced it; the real publish will skip that marketplace with an isolated error rather than aborting the batch.
If B or C: continue to Step 20.
**Output the PR/MR URL** — then proceed to Step 20.
---
-280
View File
@@ -1,280 +0,0 @@
/**
* gstack-publish end-to-end tests via --dry-run.
*
* Verifies manifest parsing, schema validation, marketplace auth checks, per-skill
* error isolation, and command building — all without touching real marketplaces.
*
* --dry-run does NOT run execSync on publish commands. Auth checks still run
* against real binaries; we use fake marketplaces whose `auth_check` commands
* are always-succeed (`true`) or always-fail (`false`) so the test is hermetic.
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
const ROOT = path.resolve(import.meta.dir, '..');
const BIN = path.join(ROOT, 'bin', 'gstack-publish');
let sandbox: string;
let binCopy: string;
beforeEach(() => {
// gstack-publish reads skills.json relative to the binary's dir (import.meta.dir/..).
// To isolate each test's manifest, we create a sandbox repo that mirrors the real
// structure: copy the bin into sandbox/bin/, write a controlled skills.json at the root.
sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'publish-sandbox-'));
fs.mkdirSync(path.join(sandbox, 'bin'));
binCopy = path.join(sandbox, 'bin', 'gstack-publish');
fs.copyFileSync(BIN, binCopy);
fs.chmodSync(binCopy, 0o755);
});
afterEach(() => {
fs.rmSync(sandbox, { recursive: true, force: true });
});
function writeManifest(manifest: object): void {
fs.writeFileSync(path.join(sandbox, 'skills.json'), JSON.stringify(manifest, null, 2));
}
function writeSkillFile(relPath: string, content = '# Test Skill\n'): void {
const full = path.join(sandbox, relPath);
fs.mkdirSync(path.dirname(full), { recursive: true });
fs.writeFileSync(full, content);
}
function run(args: string[]): { status: number | null; stdout: string; stderr: string } {
const result = spawnSync('bun', ['run', binCopy, ...args], {
cwd: sandbox,
encoding: 'utf-8',
timeout: 15000,
});
return {
status: result.status,
stdout: result.stdout?.toString() ?? '',
stderr: result.stderr?.toString() ?? '',
};
}
const VALID_MARKETPLACES = {
fakestore_ok: {
cli: 'true', // binary that always succeeds
login_cmd: 'fakestore_ok login',
publish_cmd_template: 'echo publish {slug} {version}',
docs: 'https://fakestore.example',
auth_check: 'true', // always-authenticated
},
fakestore_noauth: {
cli: 'true',
login_cmd: 'fakestore_noauth login',
publish_cmd_template: 'echo publish {slug} {version}',
docs: 'https://fakestore.example',
auth_check: 'false', // always-fails auth
},
fakestore_missing: {
cli: 'nonexistent-binary-xyz',
login_cmd: 'fakestore_missing login',
publish_cmd_template: 'echo publish {slug} {version}',
docs: 'https://fakestore.example',
auth_check: 'nonexistent-binary-xyz whoami',
},
};
function validSkill(slug: string, sourceRel: string, marketplaces: string[] = ['fakestore_ok']) {
const m: Record<string, { slug: string; publish: boolean }> = {};
for (const name of marketplaces) m[name] = { slug, publish: true };
return {
slug,
source: sourceRel,
name: `Skill ${slug}`,
version: '1.0.0',
category: 'test',
description: 'A test skill',
marketplaces: m,
standalone: true,
compatible_hosts: ['claude-code'],
};
}
describe('gstack-publish: manifest loading', () => {
test('--list prints every skill and marketplace', () => {
writeSkillFile('skills/alpha/SKILL.md');
writeSkillFile('skills/beta/SKILL.md');
writeManifest({
version: '1.0.0',
description: 't',
skills: [validSkill('alpha', 'skills/alpha/SKILL.md'), validSkill('beta', 'skills/beta/SKILL.md')],
marketplaces: VALID_MARKETPLACES,
});
const r = run(['--list']);
expect(r.status).toBe(0);
expect(r.stdout).toContain('alpha');
expect(r.stdout).toContain('beta');
expect(r.stdout).toContain('fakestore_ok');
});
test('missing manifest exits non-zero', () => {
// Delete any manifest
const r = run(['--dry-run']);
expect(r.status).not.toBe(0);
expect(r.stderr).toContain('skills.json');
});
test('malformed JSON exits non-zero', () => {
fs.writeFileSync(path.join(sandbox, 'skills.json'), '{ not json');
const r = run(['--dry-run']);
expect(r.status).not.toBe(0);
expect(r.stderr).toContain('parse');
});
});
describe('gstack-publish: validation', () => {
test('missing source file reports validation error and exits 1', () => {
writeManifest({
version: '1.0.0',
description: 't',
skills: [validSkill('ghost', 'skills/ghost/DOES_NOT_EXIST.md')],
marketplaces: VALID_MARKETPLACES,
});
const r = run(['--dry-run']);
expect(r.status).not.toBe(0);
expect(r.stderr).toContain('source file missing');
expect(r.stderr).toContain('ghost');
});
test('missing slug reports validation error', () => {
writeSkillFile('skills/x/SKILL.md');
const s = validSkill('temp', 'skills/x/SKILL.md');
delete (s as Partial<typeof s>).slug;
writeManifest({
version: '1.0.0',
description: 't',
skills: [s],
marketplaces: VALID_MARKETPLACES,
});
const r = run(['--dry-run']);
expect(r.status).not.toBe(0);
expect(r.stderr).toContain('missing slug');
});
test('missing version reports validation error', () => {
writeSkillFile('skills/x/SKILL.md');
const s = validSkill('x', 'skills/x/SKILL.md');
delete (s as Partial<typeof s>).version;
writeManifest({
version: '1.0.0',
description: 't',
skills: [s],
marketplaces: VALID_MARKETPLACES,
});
const r = run(['--dry-run']);
expect(r.status).not.toBe(0);
expect(r.stderr).toContain('missing version');
});
test('no marketplaces configured reports validation error', () => {
writeSkillFile('skills/x/SKILL.md');
const s = { ...validSkill('x', 'skills/x/SKILL.md'), marketplaces: {} };
writeManifest({ version: '1.0.0', description: 't', skills: [s], marketplaces: VALID_MARKETPLACES });
const r = run(['--dry-run']);
expect(r.status).not.toBe(0);
expect(r.stderr).toContain('no marketplaces configured');
});
});
describe('gstack-publish: dry-run execution', () => {
test('happy path reports DRY-RUN tag and templated command', () => {
writeSkillFile('skills/alpha/SKILL.md');
writeManifest({
version: '1.0.0',
description: 't',
skills: [validSkill('alpha', 'skills/alpha/SKILL.md')],
marketplaces: VALID_MARKETPLACES,
});
const r = run(['--dry-run']);
expect(r.status).toBe(0);
expect(r.stdout).toContain('DRY-RUN');
expect(r.stdout).toContain('alpha');
expect(r.stdout).toContain('Published: 1');
expect(r.stdout).toContain('Failed: 0');
});
test('per-skill filter publishes only the requested slug', () => {
writeSkillFile('skills/alpha/SKILL.md');
writeSkillFile('skills/beta/SKILL.md');
writeManifest({
version: '1.0.0',
description: 't',
skills: [validSkill('alpha', 'skills/alpha/SKILL.md'), validSkill('beta', 'skills/beta/SKILL.md')],
marketplaces: VALID_MARKETPLACES,
});
const r = run(['alpha', '--dry-run']);
expect(r.status).toBe(0);
expect(r.stdout).toContain('Publishing alpha');
expect(r.stdout).not.toContain('Publishing beta');
expect(r.stdout).toContain('Published: 1');
});
test('unknown skill filter exits non-zero', () => {
writeSkillFile('skills/alpha/SKILL.md');
writeManifest({
version: '1.0.0',
description: 't',
skills: [validSkill('alpha', 'skills/alpha/SKILL.md')],
marketplaces: VALID_MARKETPLACES,
});
const r = run(['nonexistent', '--dry-run']);
expect(r.status).not.toBe(0);
expect(r.stderr).toContain('skill not found');
});
});
describe('gstack-publish: auth check isolation', () => {
test('failing auth for one marketplace does NOT abort the batch in dry-run', () => {
writeSkillFile('skills/alpha/SKILL.md');
writeManifest({
version: '1.0.0',
description: 't',
skills: [validSkill('alpha', 'skills/alpha/SKILL.md', ['fakestore_ok', 'fakestore_noauth'])],
marketplaces: VALID_MARKETPLACES,
});
const r = run(['--dry-run']);
// In dry-run, auth failures are reported but don't block dispatch
expect(r.status).toBe(0);
expect(r.stdout).toContain('fakestore_ok: OK');
expect(r.stdout).toContain('fakestore_noauth: NOT READY');
});
test('missing binary reported as not-ready with docs link', () => {
writeSkillFile('skills/alpha/SKILL.md');
writeManifest({
version: '1.0.0',
description: 't',
skills: [validSkill('alpha', 'skills/alpha/SKILL.md', ['fakestore_missing'])],
marketplaces: VALID_MARKETPLACES,
});
const r = run(['--dry-run']);
expect(r.stdout).toContain('fakestore_missing: NOT READY');
expect(r.stdout).toContain('not on PATH');
});
});
describe('gstack-publish: real manifest sanity', () => {
test('the real repo skills.json passes --dry-run validation', () => {
// This uses the actual bin against the actual manifest (ROOT/skills.json).
// If auth to any real marketplace isn't set up it just reports NOT READY;
// --dry-run still exits 0 because it doesn't require auth to pass.
const real = spawnSync('bun', ['run', path.join(ROOT, 'bin', 'gstack-publish'), '--dry-run'], {
cwd: ROOT,
encoding: 'utf-8',
timeout: 20000,
});
expect(real.status).toBe(0);
expect(real.stdout).toContain('Validating manifest');
// Every skill in the real manifest should pass validation
expect(real.stderr).not.toContain('Manifest validation failed');
});
});