shipit-skill — one-pass launch pipeline for developer tools
Take an existing repo from "it works" to published + listed + marketable. Four phases, each with a hard exit criterion. Skipping a phase is allowed only if its exit criterion is already met.
Philosophy: look before you build. Check what already exists and what the ecosystem requires first — the same way you'd advise any user not to reinvent a wheel.
Phase 0 — Recon (30s, always)
Run these checks before touching anything:
- Existing repo state —
git status,git remote -v, recentgit log. - Package registration — is the name taken? (
pip index versions <name>,npm view <name>, registry CDN). Names collide often; scoped names for npm (@org/pkg) dodge typosquatting rules. - Similar projects — search GitHub for direct competitors. If one exists, note the differentiation explicitly; don't build a carbon copy.
- The "exit criteria" gaps — CI? Dockerfile? GitHub Release? listed on Glama / awesome dirs? promo material current? Build a checklist of what's missing. That checklist drives the rest.
Output: a short gap report + plan. Get confirmation before Phase 1 if the gaps are large.
Phase 1 — Engineering baseline
Exit criterion: CI green on the default branch, package metadata correct, Dockerfile present (if it's an MCP/CLI server).
- CI: add GitHub Actions. Python: matrix over supported versions, but mind
extras that require a newer interpreter (e.g.
mcp>=2.0needs py3.10+ — run the base on 3.9, extras on 3.12). Always include lint + tests + a container MCP handshake job if a server ships. - Dockerfile: needed for Glama and container users. For stdio MCP servers:
base image + install the package +
CMD [the-server-command]. Build from source (COPY src) unless the published package is verified good — a stale PyPI/npm release will silently break introspection. .dockerignore: keep the image minimal (no.git, tests, promo, docs).- Version from metadata: MCP
serverInfo.versionand CLI--versionmust read the package version, not a hardcoded string. - Dependency floor: if your code targets a new SDK API, set the dependency
floor (
mcp>=2.0), don't leave>=1.0— pip/npm resolve to latest and break fresh installs.
Verify: clean pip install .[extras] / npm install in a temp venv, then run
the MCP handshake (initialize + tools/list) against the container.
Phase 2 — Publish
Exit criterion: package on the registry, GitHub Release with tag + notes, clean-env install verified.
- Version bump → build artifacts (
python -m build/npm pack). - Tag + GitHub Release: title
vX.Y.Z, notes with Changes / Install / What-it-fixes. Letgh release createattach from the tag. - Registry publish:
- PyPI: needs a token (granular, scoped to the package, Upload perm).
If the token is malformed PyPI returns
403 Invalid or non-existent authentication information— UUID-style strings are wrong; real PyPI tokens startpypi-.twine uploadwith--username __token__. - npm: scoped names need the org to exist; granular token with bypass-2FA
for automation;
--//registry.npmjs.org/:_authTokeninline avoids writing creds to disk.
- PyPI: needs a token (granular, scoped to the package, Upload perm).
If the token is malformed PyPI returns
- Fresh-install verification (non-negotiable): create a venv / clean dir, install from the registry, import the module / run the CLI / do the MCP handshake. This catches "works on my machine" packaging bugs.
- Gotchas:
- npm CDN serves cached 404s for minutes after first publish — don't panic, cache-bust with a query param.
File already existson retry usually means the first attempt succeeded.- Lockfile drift: rename packages → always
npm installto regen the lock ornpm cifails in CI.
Phase 3 — Directory listings
Exit criterion: listed on Glama with a score badge and awesome PR open (or merged).
- Glama (
glama.ai/mcp/servers): user submits via browser (GitHub OAuth — cannot be automated). Before submitting: Dockerfile must exist and pass introspection (see Phase 1). After submit: the page takes minutes to hours to appear — pollhttps://glama.ai/mcp/servers/<owner>/<repo>and/badges/score.svg. Add the badge to the repo README once live:[](https://glama.ai/mcp/servers/owner/repo) - awesome-mcp-servers (and similar curated lists): PRs are triaged by bots.
Format per the list's current convention (badge + lang + scope + OS + install
cmd). Maintainers now require the Glama badge in the entry — submit Glama
first. Add
🤖🤖🤖to the PR title to opt into the agent fast-track. Fork → branch off latest upstream main → add entry → PR. If a previous PR was closed for inactivity, force-push the branch or recreate it. - Other directories (Smithery, PulseMCP, mcp.so…): Smithery reads a
smithery.yamlat repo root (stdio start command + optional env schema).
Phase 4 — Promo material
Exit criterion: promo docs' versions/links/PR numbers match reality; per- platform posts are ready to paste (user posts manually — no fake automation).
- Maintain
promo/with per-platform files (V2EX, 知乎, Reddit, X, community). - Freshness check (run whenever versions/PRs change): grep promo files for old version numbers, dead PR links, stale release URLs. Update or mark stale.
- Fact discipline: verify every number/claim before posting — rerun the tool and compare output. Wrong claims get torn apart in comments (e.g. a "no LICENSE" claim that's actually a GitHub metadata quirk).
- Keep install commands unpinned (
pip install pkg) so promo doesn't rot.
CLI quick reference (shipit-skill)
Run these to move a repo through the phases. Everything is print-only by
default; the side-effecting variants are opt-in via --execute.
| Command | Does | Auto / print |
|---|---|---|
doctor [--json] | env self-check: gh authed, tokens set, clean tree on main | print; exit 1 if gaps |
preflight [--dir .] [--version X.Y.Z] [--json] | launch-readiness gap report | |
init <dir> --server <srv> --pkg <pkg> [--force] [--dry-run] | scaffold CI/Dockerfile/smithery.yaml/promo | writes files |
ci --lang python [--server srv] [--write path] [--release] | emit ci.yml; --release emits release.yml | print / write |
| `bump patch | minor | major |
release --lang python --pkg <pkg> [--repo o/r] [--execute] | doctor gate → bump→build→publish→tag→gh release→promo check. Tag/Release only created after publish succeeds; publish failure rolls back the commit; re-runs skip existing tags/releases; notes from CHANGELOG | print / execute |
publish --lang python --pkg <pkg> [--server srv] [--execute] | registry upload (token from PYPI_TOKEN/NPM_TOKEN env) + fresh-install verify | print / execute |
check-promo --dir promo --version X.Y.Z [--prs id=state,...] [--no-links] [--report] [--fix] | stale versions / unknown PRs / broken links; --report emits JSON; --fix rewrites stale versions + marks unknown PRs | print / JSON / fix |
check-glama --repo o/r [--poll N] [--add-badge] [--json] | wait for Glama listing; --add-badge writes README badge once live; --json structured | print / write |
awesome-pr --upstream o/r --repo o/r --fork o/r --branch b [--execute --category C --description D --install I] | PR recipe, or actually fork→edit→push→gh pr create | print / execute |
Tokens: never stored on disk. publish/release --execute reads PYPI_TOKEN
or NPM_TOKEN from the environment; prompt the user for it and pass it inline.
Automation boundary
| Step | Who runs it |
|---|---|
| CI files, Dockerfile, metadata, code fixes | Agent |
| Build artifacts, fresh-install verification | Agent |
| Registry publish (PyPI/npm) | Agent, with a user-supplied token |
| Glama browser submit | User (OAuth) |
| awesome/other PR create | Agent |
| Posting to platforms | User |
Never fabricate credentials; ask for the token explicitly and use it inline, never write it to a file.
Real-world scars (from shipping wheel-hub + zspace-cli)
mcp>=1.0in extras + new SDK → fresh installs crash. Set floors.- Python 3.9 can't install
mcp>=2.0; CI matrix must split base vs extras. - YAML block scalar + inline python heredoc = broken workflow. Use a script file.
- MCP 2.x
Serverobject has nolist_tools— use@server.tool()decorators. - Glama introspection needs stdin held open for a beat (
sleep 1) in smoke tests. - PyPI token formats:
pypi-...real, UUID = wrong. - npm typosquatting blocks
wheel-hubnext towheelhub; scoped names work. .dockerignoremissing → giant images / secrets in build context.