Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions .claude/skills/bump-coana/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,37 @@ fi
1. Read `package.json` in the repository root.
2. Find the current `@coana-tech/cli` version in `devDependencies` and note it as `CURRENT_VERSION`.
3. Update `@coana-tech/cli` to the new version.
4. Bump the patch version of the package (e.g., `1.1.59` → `1.1.60`).
5. Write the updated `package.json`.
4. Write the updated `package.json`.

🚨 **Do NOT touch the top-level `version` field.** The release workflow owns it:
between releases the manifest holds the last released version, and
`scripts/release/bump.mts` derives and writes the next one in-run. Hand-writing
it invents a version that never releases and breaks the next release — a manifest
already sitting on the version being released has no line for the bump to
advance.

**Values to extract**:
- `CURRENT_VERSION`: The old @coana-tech/cli version (for PR body)
- `NEW_PKG_VERSION`: The bumped package.json version (for changelog)

### Step 3: Update CHANGELOG.md

1. Read `CHANGELOG.md` in the repository root.
2. Add a new version entry after the header section (which ends with "The format is based on...").
3. Use today's date in `YYYY-MM-DD` format.

**Entry format**:
2. Find the `## [Unreleased]` heading. If it is absent — the previous release
consumes it — recreate it directly after the header section (which ends with
"The format is based on...").
3. Add the entry under `## [Unreleased]`, in its `### Changed` subsection,
creating that subsection if it is missing. If a Coana line is already there
from an earlier unreleased bump, update it in place rather than adding a
second one.

🚨 **Never write a `## [<version>]` heading.** Release headings belong to the
release workflow, which promotes the whole `## [Unreleased]` block under the
version it derives. Writing one here both names a version that may never exist
and consumes the block, leaving the real release with empty notes.

**Resulting shape**:
```markdown
## [NEW_PKG_VERSION](https://github.com/SocketDev/socket-cli/releases/tag/vNEW_PKG_VERSION) - YYYY-MM-DD
## [Unreleased]

### Changed
- Updated the Coana CLI to v `COANA_VERSION`.
Expand Down Expand Up @@ -105,7 +120,8 @@ Replace `CURRENT_VERSION` and `COANA_VERSION` with actual values.

- Branch: `coana-<VERSION>` pushed to origin
- PR: Created targeting `v1.x` branch
- Files modified: `package.json`, `CHANGELOG.md`, `pnpm-lock.yaml`
- Files modified: `package.json` (the `@coana-tech/cli` devDependency only, never
the top-level `version`), `CHANGELOG.md` (under `## [Unreleased]`), `pnpm-lock.yaml`

Report the PR URL to the user when complete.

Expand All @@ -120,3 +136,6 @@ Report the PR URL to the user when complete.

- Do NOT add any AI/Claude co-authorship or attribution to the commit message or PR.
- Do NOT include "Generated with Claude Code" or similar text anywhere.
- Do NOT bump `package.json`'s `version` or write a `## [<version>]` changelog
heading. Both belong to the release workflow. Hand-writing them has produced
versions that never released and cost a real release its notes entirely.
16 changes: 10 additions & 6 deletions scripts/release/bump.mts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ const rootPath = path.join(

const REGISTRY_URL = 'https://registry.npmjs.org'

const VERSION_FIELD_PATTERN = /("version":\s*")[^"]+(")/

interface PackageJsonShape {
name?: string | undefined
repository?: { url?: string | undefined } | string | undefined
Expand Down Expand Up @@ -158,19 +160,21 @@ function readPackageJson(): { parsed: PackageJsonShape; raw: string } {
* is the one line a reviewer expects.
*/
export function writeManifestVersion(raw: string, version: string): string {
const replaced = raw.replace(
/("version":\s*")[^"]+(")/,
(_m, pre: string, post: string) => `${pre}${version}${post}`,
)
if (replaced === raw) {
// Test for the field before replacing: a manifest already sitting on
// `version` rewrites to itself, and comparing output to input cannot tell
// that no-op apart from a missing field.
if (!VERSION_FIELD_PATTERN.test(raw)) {
throw new Error(
'[bump] could not rewrite the package.json version field.\n' +
' Where: the root package.json, at bump time.\n' +
' Saw: no top-level `"version": "…"` line; wanted exactly one to replace.\n' +
' Fix: restore the version field, then re-dispatch.',
)
}
return replaced
return raw.replace(
VERSION_FIELD_PATTERN,
(_m, pre: string, post: string) => `${pre}${version}${post}`,
)
}

function emitOutputs(outputs: Record<string, string>): void {
Expand Down
17 changes: 17 additions & 0 deletions test/release-bump.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ describe('writeManifestVersion', () => {
)
})

// A manifest already sitting on the target rewrites to itself. The prior
// `replaced === raw` guard read that no-op as a missing field and failed the
// release with "no top-level version line" for a manifest whose version was
// present and well-formed — the exact failure that broke the 1.1.160 run,
// where a hand-written bump had already put 1.1.160 in the manifest.
it('is a no-op when the manifest already holds the target version', () => {
const raw = [
'{',
' "name": "socket",',
' "version": "1.1.160",',
' "description": "CLI for Socket.dev"',
'}',
'',
].join('\n')
expect(writeManifestVersion(raw, '1.1.160')).toBe(raw)
})

it('throws when the manifest has no version field', () => {
const raw = '{\n "name": "socket"\n}\n'
expect(() => writeManifestVersion(raw, '1.1.160')).toThrow(
Expand Down