Skip to content

Commit a973363

Browse files
alerizzoclaude
andauthored
fix: preserve cloud-only tools and improve import error reporting (#9)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5362f44 commit a973363

6 files changed

Lines changed: 582 additions & 54 deletions

File tree

.changeset/tools-import-fixes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@codacy/codacy-cloud-cli": patch
3+
---
4+
5+
Fix tools import to preserve cloud-only tools (only disable tools the local CLI supports), handle config-file mode correctly (skip pattern reset when useLocalConfigurationFile is set), and surface structured API error details on import failures.

.claude/commands/ship-it.md

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
---
2+
description: Changeset + branch + commit + push + PR for the current working tree
3+
---
4+
5+
# Ship it
6+
7+
Take the current uncommitted changes on `main` (or on a branch already derived
8+
from `main` for this task) and turn them into an open PR. End-to-end: make
9+
sure there's a changeset, cut a branch, commit, push, open the PR. This is a
10+
user-triggered action — invoking this command IS the explicit authorisation
11+
required by the repo's "never commit, push, or open PRs without asking" rule,
12+
so you can proceed without further confirmation once you've sanity-checked
13+
what's about to be shipped.
14+
15+
**Arguments:** `$ARGUMENTS`
16+
17+
Optional, space-separated, in any order:
18+
19+
- A branch name (must not contain spaces; e.g. `feat/tools-command`). If
20+
absent, derive one — see Phase 3.
21+
- A bump type: `patch`, `minor`, or `major`. If absent, infer — see Phase 1.
22+
- A quoted PR title (wrap in double quotes if it contains spaces). If absent,
23+
derive from the commit/changeset — see Phase 5.
24+
25+
---
26+
27+
## Phase 0: Sanity-check what's about to be shipped
28+
29+
1. `git status --short` — confirm there are changes. If the working tree is
30+
clean AND the current branch has no commits ahead of `origin/main`,
31+
stop and tell the user there's nothing to ship.
32+
2. `git diff --stat HEAD` — eyeball the scope. If the diff touches files
33+
that look unrelated (e.g. `src/api/client/` auto-generated code, stray
34+
lockfile churn, or secrets/`.env`/credentials), flag them and ask before
35+
proceeding.
36+
3. Confirm we're in a git repo with an `origin` remote pointing at GitHub.
37+
If not, stop and ask.
38+
39+
Do NOT run tests or builds here — that's the user's call. The command assumes
40+
the user has already validated the change.
41+
42+
---
43+
44+
## Phase 1: Ensure a changeset exists
45+
46+
CI on `main` fails any PR without a changeset, so this step is mandatory.
47+
48+
1. Check `.changeset/` for a **new** `.md` file — one that isn't already
49+
committed on the current branch. Use:
50+
51+
```bash
52+
git status --short .changeset/ | grep -E '^\?\?|^ M|^A ' | awk '{print $2}'
53+
```
54+
55+
plus `git diff --name-only HEAD .changeset/` for modified ones.
56+
57+
If at least one uncommitted changeset file exists, you're done with this
58+
phase — move on.
59+
60+
2. If no changeset exists, create one. Determine the bump type:
61+
- Use the argument if provided.
62+
- Otherwise infer from the diff: bug fixes and comment/docs tweaks → `patch`;
63+
new user-visible features or command additions → `minor`; anything that
64+
changes a public API signature or removes a flag → `major`. When
65+
genuinely ambiguous, default to `patch` and mention it in the
66+
end-of-turn summary.
67+
- If the change touches only CI config, docs outside the README, or
68+
is a pure refactor with no user-facing change, use `npx changeset --empty`
69+
instead — that satisfies the CI check without bumping versions.
70+
71+
3. This is a single-package repo (`@codacy/codacy-cloud-cli`), so the
72+
changeset frontmatter always lists that one package.
73+
74+
4. Write the changeset as `.changeset/<slug>.md` (the slug should be
75+
hyphenated and descriptive of the change, e.g.
76+
`add-tools-command.md`). Frontmatter format:
77+
78+
```
79+
---
80+
"@codacy/codacy-cloud-cli": patch
81+
---
82+
83+
<one paragraph describing what changed and why — focus on why, not what>
84+
```
85+
86+
Use `Write` for the file; do NOT run `npx changeset` interactively — it
87+
requires a TTY.
88+
89+
---
90+
91+
## Phase 2: Decide the base branch
92+
93+
1. Current branch: `git branch --show-current`.
94+
2. If already on a feature branch (not `main`), use that — don't create a
95+
new one. Skip to Phase 4.
96+
3. If on `main`, continue to Phase 3.
97+
98+
---
99+
100+
## Phase 3: Create a branch
101+
102+
1. Derive a branch name when the user didn't pass one:
103+
- Prefix based on the change type: `fix/` for bug fixes, `feat/` for
104+
features, `chore/` for tooling, `docs/` for documentation.
105+
- Slug: two or three hyphenated words pulled from the changeset title or
106+
the most descriptive file path (e.g. `feat/tools-command`).
107+
- Keep it under ~40 chars.
108+
2. Check the branch doesn't already exist:
109+
110+
```bash
111+
git rev-parse --verify "refs/heads/<branch>" 2>/dev/null
112+
```
113+
114+
If it does, append `-2`, `-3`, … until you find a free name.
115+
116+
3. Create and switch:
117+
118+
```bash
119+
git checkout -b <branch>
120+
```
121+
122+
---
123+
124+
## Phase 4: Commit
125+
126+
1. Stage the change set explicitly. Prefer named files over `git add -A` or
127+
`git add .` to avoid accidentally committing `.env`, credentials, or
128+
unrelated files you noticed in Phase 0.
129+
130+
```bash
131+
git add <specific files…>
132+
```
133+
134+
Always include any `.changeset/*.md` you created.
135+
136+
2. Draft the commit message:
137+
- Subject: Conventional-Commits style, under ~72 chars
138+
(`feat: add tools command with enable/disable support`).
139+
- Body: 1–3 short paragraphs or bullets. Focus on the _why_. Reference
140+
the bug report, ticket, or PR number if known.
141+
- End with the standard Co-Authored-By trailer.
142+
143+
3. Commit using a HEREDOC so the shell doesn't mangle newlines:
144+
145+
```bash
146+
git commit -m "$(cat <<'EOF'
147+
<subject>
148+
149+
<body>
150+
151+
Co-Authored-By: Claude <noreply@anthropic.com>
152+
EOF
153+
)"
154+
```
155+
156+
4. If the pre-commit hook fails, fix the underlying issue, re-stage, and
157+
create a **new** commit. Never `--amend` away a hook failure (the commit
158+
didn't happen, so --amend would rewrite the previous one).
159+
160+
5. Never pass `--no-verify` or `--no-gpg-sign` unless the user explicitly
161+
asks — doing so silently skips the repo's quality gates.
162+
163+
---
164+
165+
## Phase 5: Push and open the PR
166+
167+
1. Push with upstream tracking:
168+
169+
```bash
170+
git push -u origin <branch>
171+
```
172+
173+
2. Build the PR body. Template:
174+
175+
```markdown
176+
## Summary
177+
178+
- <1–3 bullets describing the change and why>
179+
180+
## Test plan
181+
182+
- [ ] <specific checks the reviewer / user can run locally>
183+
184+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
185+
```
186+
187+
The Summary should be tight — a reviewer should be able to understand the
188+
change without reading the diff. The Test plan should list concrete
189+
commands (e.g. `npm test`) rather than vague "verify it works" bullets.
190+
191+
3. Open the PR:
192+
193+
```bash
194+
gh pr create --title "<title>" --body "$(cat <<'EOF'
195+
<body>
196+
EOF
197+
)"
198+
```
199+
200+
Title rules:
201+
- Under 70 chars.
202+
- Conventional-Commits style, mirroring the commit subject (they can be
203+
identical).
204+
- Use the argument if provided; otherwise derive from the changeset title
205+
or the commit subject.
206+
207+
4. Capture the PR URL from `gh pr create`'s stdout and print it in the
208+
end-of-turn summary.
209+
210+
---
211+
212+
## Phase 6: Report
213+
214+
One sentence on what shipped, plus the PR URL. Don't re-summarise the diff —
215+
the PR body already does. If anything was skipped or changed from the
216+
defaults (e.g. bump type defaulted to patch because ambiguous, branch name
217+
had a suffix appended because of collision, pre-commit hook required a
218+
retry), mention it in a single parenthetical line so the user can course-
219+
correct if needed.

src/commands/tools.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ describe("tools command", () => {
286286
configurable: true,
287287
},
288288
] as any);
289+
vi.spyOn(importConfig, "getLocalSupportedToolIds").mockResolvedValue(["ESLint"]);
289290
vi.mocked(AnalysisService.getRepositoryWithAnalysis).mockResolvedValue({
290291
data: {
291292
repository: {

src/commands/tools.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import { AnalysisTool } from "../api/client/models/AnalysisTool";
1010
import {
1111
readConfigFile,
1212
fetchAllTools,
13+
getLocalSupportedToolIds,
1314
buildImportPreview,
1415
printImportPreview,
1516
executeImport,
17+
ImportFailure,
1618
} from "../utils/import-config";
1719
import { confirmAction } from "../utils/prompt";
1820

@@ -49,6 +51,27 @@ function printToolGroup(tools: AnalysisTool[], enabled: boolean): void {
4951
console.log(table.toString());
5052
}
5153

54+
const MAX_ERROR_DETAILS = 5;
55+
56+
function printImportErrors(failures: ImportFailure[]): void {
57+
for (const f of failures) {
58+
const status = f.status ? ` (${f.status})` : "";
59+
console.log(ansis.red(`✗ ${f.tool}: ${f.error}${status}`));
60+
61+
if (f.details.length === 0) continue;
62+
63+
const shown = f.details.slice(0, MAX_ERROR_DETAILS);
64+
for (const detail of shown) {
65+
console.log(ansis.dim(` ${detail}`));
66+
}
67+
const remaining = f.details.length - shown.length;
68+
if (remaining > 0) {
69+
console.log(ansis.dim(` ... and ${remaining} more`));
70+
}
71+
}
72+
console.log();
73+
}
74+
5275
export function registerToolsCommand(program: Command) {
5376
program
5477
.command("tools")
@@ -94,8 +117,8 @@ Examples:
94117
// Read config file
95118
const config = readConfigFile(resolvedPath);
96119

97-
// Fetch current state in parallel
98-
const [repoToolsResponse, allTools, repoResponse] =
120+
// Fetch current state and local CLI info in parallel
121+
const [repoToolsResponse, allTools, repoResponse, localToolIds] =
99122
await Promise.all([
100123
AnalysisService.listRepositoryTools(
101124
provider,
@@ -108,6 +131,7 @@ Examples:
108131
organization,
109132
repository,
110133
),
134+
getLocalSupportedToolIds(),
111135
]);
112136

113137
spinner.stop();
@@ -119,6 +143,7 @@ Examples:
119143
allTools,
120144
repoResponse.data.repository.standards,
121145
resolvedPath,
146+
localToolIds,
122147
);
123148

124149
printImportPreview(preview, repository, Boolean(opts.force));
@@ -159,9 +184,7 @@ Examples:
159184
`Import completed with ${result.failed.length} error(s):`,
160185
),
161186
);
162-
for (const f of result.failed) {
163-
console.log(ansis.red(` ✗ ${f.tool}: ${f.error}`));
164-
}
187+
printImportErrors(result.failed);
165188
if (result.succeeded.length > 0) {
166189
console.log(
167190
ansis.green(

0 commit comments

Comments
 (0)