Skip to content

Commit 600e5f9

Browse files
martzoukosclaude
andcommitted
Merge branch 'main' into martzoukos/ai-445-pending-mappings-on-pr
Conflict in the confirmation protocol docs: #1410 landed a paragraph on defaults being omitted from the confirmCommand, in the same spot as this branch's pinned-target section. Both are wanted — kept the general note first, then the pinning section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 parents 976269e + 635573a commit 600e5f9

54 files changed

Lines changed: 2669 additions & 339 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,48 @@ concurrency:
1818
cancel-in-progress: true
1919

2020
jobs:
21+
# ---------------------------------------------------------------------------
22+
# Detects whether anything outside the ignore list below changed. Meta files
23+
# (agent instructions, docs, license, .gitignore) cannot affect lint,
24+
# packaging or test outcomes, so a change touching only those skips the whole
25+
# matrix.
26+
#
27+
# The filter runs inside a job rather than as an `on: paths-ignore` trigger:
28+
# a workflow skipped by a trigger-level path filter never reports a status, so
29+
# required checks stay pending forever and block merge. The CI gates below
30+
# depend on this job, so if it fails, the gates fail rather than passing on a
31+
# matrix that never ran.
32+
# ---------------------------------------------------------------------------
33+
changes:
34+
name: detect changes
35+
runs-on: ubuntu-latest
36+
permissions:
37+
pull-requests: read
38+
outputs:
39+
code: ${{ steps.filter.outputs.code }}
40+
steps:
41+
- uses: actions/checkout@v3
42+
- uses: dorny/paths-filter@v3
43+
id: filter
44+
with:
45+
# 'every' makes a file match `code` only when it matches *all* the
46+
# patterns, i.e. when it is none of the ignored paths. `code` is
47+
# therefore true when at least one changed file lives outside the
48+
# ignore list.
49+
predicate-quantifier: 'every'
50+
filters: |
51+
code:
52+
- '!.claude/**'
53+
- '!AGENTS.md'
54+
- '!CLAUDE.md'
55+
- '!CONTRIBUTING.md'
56+
- '!LICENSE'
57+
- '!README.md'
58+
- '!.gitignore'
59+
2160
lint:
61+
needs: changes
62+
if: needs.changes.outputs.code == 'true'
2263
runs-on: ubuntu-latest
2364
steps:
2465
- uses: actions/checkout@v3
@@ -39,6 +80,8 @@ jobs:
3980
# ---------------------------------------------------------------------------
4081
test-ubuntu:
4182
name: test - ubuntu
83+
needs: changes
84+
if: needs.changes.outputs.code == 'true'
4285
runs-on: ubuntu-latest
4386
steps:
4487
- uses: actions/checkout@v3
@@ -61,6 +104,8 @@ jobs:
61104
retention-days: 1
62105
test-windows:
63106
name: test - windows
107+
needs: changes
108+
if: needs.changes.outputs.code == 'true'
64109
runs-on: blacksmith-4vcpu-windows-2025
65110
steps:
66111
- uses: actions/checkout@v3
@@ -88,7 +133,8 @@ jobs:
88133
# ---------------------------------------------------------------------------
89134
test-e2e-checkly-ubuntu:
90135
name: e2e - checkly - ubuntu
91-
if: github.event_name == 'pull_request'
136+
needs: changes
137+
if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'
92138
runs-on: ubuntu-latest
93139
steps:
94140
- uses: actions/checkout@v3
@@ -110,7 +156,8 @@ jobs:
110156
CHECKLY_EMPTY_API_KEY: ${{ secrets.E2E_EMPTY_CHECKLY_API_KEY }}
111157
test-e2e-checkly-windows:
112158
name: e2e - checkly - windows (${{ matrix.shard }})
113-
if: github.event_name == 'pull_request'
159+
needs: changes
160+
if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'
114161
strategy:
115162
fail-fast: false
116163
matrix:
@@ -140,7 +187,8 @@ jobs:
140187
# ---------------------------------------------------------------------------
141188
test-e2e-create-checkly-ubuntu:
142189
name: e2e - create-checkly - ubuntu
143-
if: github.event_name == 'pull_request'
190+
needs: changes
191+
if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'
144192
runs-on: ubuntu-latest
145193
steps:
146194
- uses: actions/checkout@v3
@@ -155,7 +203,8 @@ jobs:
155203
- run: pnpm --filter create-checkly run test:e2e
156204
test-e2e-create-checkly-windows:
157205
name: e2e - create-checkly - windows
158-
if: github.event_name == 'pull_request'
206+
needs: changes
207+
if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'
159208
runs-on: blacksmith-4vcpu-windows-2025
160209
steps:
161210
- uses: actions/checkout@v3
@@ -179,6 +228,7 @@ jobs:
179228
name: CI gate - ubuntu
180229
if: always()
181230
needs:
231+
- changes
182232
- lint
183233
- test-ubuntu
184234
- test-e2e-checkly-ubuntu
@@ -189,8 +239,11 @@ jobs:
189239
run: |
190240
results="${{ join(needs.*.result, ', ') }}"
191241
echo "Ubuntu job results: $results"
192-
# 'skipped' is allowed (e2e jobs are skipped on push-to-main); only
193-
# 'failure'/'cancelled' should fail the gate.
242+
# 'skipped' is allowed (e2e jobs are skipped on push-to-main, and
243+
# every job is skipped for meta-file-only changes); only
244+
# 'failure'/'cancelled' should fail the gate. `changes` is a
245+
# dependency so that a failure to compute the path filter fails the
246+
# gate instead of green-lighting a matrix that never ran.
194247
if [[ "$results" == *failure* || "$results" == *cancelled* ]]; then
195248
echo "::error::One or more Ubuntu jobs did not succeed"
196249
exit 1
@@ -199,6 +252,7 @@ jobs:
199252
name: CI gate - windows
200253
if: always()
201254
needs:
255+
- changes
202256
- test-windows
203257
- test-e2e-checkly-windows
204258
- test-e2e-create-checkly-windows

eslint.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ const checklyStyle = {
4343

4444
export default defineConfig([
4545
globalIgnores([
46+
// Holds git worktrees, each a full checkout of the repo with its own
47+
// in-progress code that must not be linted as part of this checkout.
48+
'**/.claude/',
4649
'**/dist/',
4750
`**/gen/`,
4851
'**/__checks__/**/*.spec.{js,ts,mjs}',

packages/cli/src/ai-context/references/communicate.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ Write commands (`incidents create`, `incidents update`, `incidents resolve`, `de
2020
**Rules for agents:**
2121

2222
1. When exit code is 2 and output contains `"status": "confirmation_required"`, **always present the `changes` array to the user** and ask for confirmation.
23-
2. **Never auto-append `--force`** — only run the `confirmCommand` after the user explicitly approves.
23+
2. **Run the `confirmCommand` verbatim, and only after the user explicitly approves.** It is normally the command you just ran, plus `--force` to skip the second prompt. Don't append `--force` to anything yourself, and don't edit the `confirmCommand` — changing its flags changes what you were authorized to do.
2424
3. This applies to **every** write command, not just the first one. Incident updates and resolutions also require confirmation.
2525
4. Use `--dry-run` to preview what a command will do without executing or prompting.
2626
5. Read-only commands (`incidents list`, `status-pages list`) execute immediately without confirmation.
2727

28+
The `confirmCommand` omits flags left at their default, so a bare `npx checkly deploy` confirms as `checkly deploy --force` rather than echoing back every boolean the parser filled in. Treat every flag you see there as deliberate.
29+
2830
### Commands that pin a resolved target
2931

3032
A command that picks its own target before confirming writes that target back into the `confirmCommand`. `import commit` and `import cancel` do this: run without `--plan-id` they select the only candidate plan themselves, and confirm as `checkly import commit --plan-id="<resolved-id>" --force` — carrying a flag you never passed. That is deliberate: the pinned ID guarantees the approved run acts on the plan whose `changes` you showed the user, not on whatever happens to be pending by then. Run the `confirmCommand` exactly as returned; do not strip the flag or fall back to the bare command.

packages/cli/src/ai-context/references/configure-traceroute-monitors.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Use `request.maxHops` (default: 30) and `request.maxUnknownHops` (default: 15) to control trace depth.
99
- Use `degradedResponseTime` and `maxResponseTime` (milliseconds) to configure response time thresholds.
1010
- Traceroute assertions support `RESPONSE_TIME`, `HOP_COUNT`, and `PACKET_LOSS` sources.
11+
- `TracerouteAssertionBuilder.responseTime()` takes an optional property — `'avg'` (default), `'min'`, `'max'`, or `'stdDev'`. `hopCount()` and `packetLoss()` take no property.
1112
- **Plan-gated properties:** `retryStrategy`, `runParallel`, and higher frequencies are not available on all plans. Check entitlements matching `UPTIME_CHECKS_*` before using these. Omit any property whose entitlement is disabled. See `npx checkly skills manage` for details.
1213

1314
<!-- EXAMPLE: TRACEROUTE_MONITOR -->

packages/cli/src/ai-context/references/configure.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,26 @@ Run `npx checkly skills manage plan` for the full reference.
7575
## Deploying
7676

7777
- Deploy checks using the `npx checkly deploy` command. Use `--output` to see the created, updated, and deleted resources. Use `--verbose` to also include each resource's name and physical ID (UUID), which is useful for programmatically referencing deployed resources (e.g. `npx checkly checks get <id>`).
78+
- Use `--preview` to see what a deploy would change without applying it.
79+
80+
### Deleted resources
81+
82+
A deploy makes the account match the code. **Any resource that was deployed before and is no longer in the code gets deleted, along with its run history.** Pass `--preserve-resources` to keep those resources and their history in the Checkly account instead, where the user can manage them from the web app.
83+
84+
This matters when the local project isn't the whole picture — a partial checkout, or a project whose checks were also edited elsewhere. If you're not sure the code is the complete source of truth, say so before deploying.
85+
86+
### Confirmation
87+
88+
`deploy` is a write command: without `--force` it returns exit code 2 and a `confirmation_required` envelope. Present its `changes` to the user and run the `confirmCommand` verbatim only after they approve.
89+
90+
That confirmation happens **before the project is parsed**, so it cannot tell you which resources would be deleted — its `changes` only warn that deletion is possible. There is a second, itemised guard that lists each doomed resource by name, but it is skipped by `--force`, and `--force` is exactly what the `confirmCommand` carries. **An agent following the confirmation protocol never sees that list.** It is a prompt for humans deploying by hand.
91+
92+
So when resources may have been removed from the code, don't rely on the confirmation to surface it — preview first:
93+
94+
```bash
95+
npx checkly deploy --preview
96+
```
97+
98+
This is a dry run: nothing is applied and nothing is confirmed. It prints the resources that would be created, updated, and deleted (add `--verbose` for names and IDs). Show the user what would be deleted, and only then deploy.
99+
100+
Run `npx checkly skills communicate` for the full protocol.

packages/cli/src/ai-context/skill.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ Run `npx checkly skills manage` for the full reference.
3434

3535
## Confirmation Protocol
3636

37-
Write commands (e.g. `incidents create`, `deploy`, `destroy`) return exit code 2 with a `confirmation_required` JSON envelope instead of executing. **Always present the `changes` to the user and wait for approval before running the `confirmCommand`.** Never auto-append `--force`. This applies to every write command individually — updates and resolutions need confirmation too, not just the initial create.
37+
Write commands (e.g. `incidents create`, `deploy`, `destroy`) return exit code 2 with a `confirmation_required` JSON envelope instead of executing. **Always present the `changes` to the user and wait for approval before running the `confirmCommand`.** This applies to every write command individually — updates and resolutions need confirmation too, not just the initial create.
3838

39-
Run `npx checkly skills communicate` for the full protocol details.
39+
The `confirmCommand` is the approved command, ready to run verbatim: it repeats the flags you passed and already ends in `--force`. Run it as-is once the user approves — don't add `--force` to a command yourself, and don't add flags the user didn't ask for.
40+
41+
Run `npx checkly skills communicate` for the full protocol details, or `npx checkly skills configure` for what `deploy` confirms.
4042

4143
## API Pass-Through (fallback for any endpoint)
4244

packages/cli/src/commands/__tests__/confirm-flow-deploy.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Parser } from '@oclif/core'
12
import { beforeEach, describe, expect, it, vi } from 'vitest'
23

34
vi.mock('../../helpers/cli-mode', () => ({
@@ -39,11 +40,23 @@ vi.mock('prompts', () => ({
3940
}))
4041

4142
import { detectCliMode } from '../../helpers/cli-mode.js'
43+
import { buildConfirmCommand } from '../../helpers/command-preview.js'
4244
import * as api from '../../rest/api.js'
4345
import { parseProject } from '../../services/project-parser.js'
4446
import { AuthCommand } from '../authCommand.js'
4547
import Deploy from '../deploy.js'
4648

49+
/** Turn a generated confirmCommand back into argv, so oclif can re-parse it. */
50+
function flagArgv (confirmCommand: string): string[] {
51+
return [...confirmCommand.matchAll(/--[a-zA-Z0-9-]+(?:="[^"]*")?/g)]
52+
.map(([token]) => token.replace(/="(.*)"$/, '=$1'))
53+
}
54+
55+
async function confirmCommandFor (argv: string[]): Promise<string> {
56+
const { flags, metadata } = await Parser.parse(argv, { flags: Deploy.flags, strict: true })
57+
return buildConfirmCommand('deploy', flags, undefined, metadata.flags)
58+
}
59+
4760
function createConfirmContext () {
4861
const logged: string[] = []
4962
let exitCodeValue: number | undefined
@@ -156,6 +169,17 @@ describe('deploy confirmation flow', () => {
156169
'debug-bundle': false,
157170
'debug-bundle-output-file': './debug-bundle.json',
158171
},
172+
metadata: {
173+
flags: {
174+
'preview': { setFromDefault: true },
175+
'output': { setFromDefault: true },
176+
'verbose': { setFromDefault: true },
177+
'schedule-on-deploy': { setFromDefault: true },
178+
'verify-runtime-dependencies': { setFromDefault: true },
179+
'debug-bundle': { setFromDefault: true },
180+
'debug-bundle-output-file': { setFromDefault: true },
181+
},
182+
},
159183
})
160184

161185
await expect(
@@ -172,5 +196,28 @@ describe('deploy confirmation flow', () => {
172196
expect(output.command).toBe('deploy')
173197
expect(output.classification.idempotent).toBe(true)
174198
expect(output.confirmCommand).toContain('--force')
199+
expect(output.confirmCommand).not.toContain('--no-preview')
200+
})
201+
})
202+
203+
describe('deploy confirmCommand', () => {
204+
it('echoes only the flags the user typed', async () => {
205+
expect(await confirmCommandFor([])).toBe('checkly deploy --force')
206+
expect(await confirmCommandFor(['--preserve-resources'])).toBe('checkly deploy --preserve-resources --force')
207+
})
208+
209+
it('keeps an explicit --no-<flag> for flags that allow it', async () => {
210+
expect(await confirmCommandFor(['--no-schedule-on-deploy']))
211+
.toBe('checkly deploy --no-schedule-on-deploy --force')
212+
})
213+
214+
it('generates a command oclif can parse back', async () => {
215+
for (const argv of [[], ['--preserve-resources'], ['--no-schedule-on-deploy'], ['--verbose']]) {
216+
const confirmCommand = await confirmCommandFor(argv)
217+
await expect(
218+
Parser.parse(flagArgv(confirmCommand), { flags: Deploy.flags, strict: true }),
219+
`confirmCommand for "checkly deploy ${argv.join(' ')}" must be runnable: ${confirmCommand}`,
220+
).resolves.toBeDefined()
221+
}
175222
})
176223
})

packages/cli/src/commands/__tests__/confirm-flow-destroy.spec.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Parser } from '@oclif/core'
12
import { beforeEach, describe, expect, it, vi } from 'vitest'
23

34
vi.mock('../../helpers/cli-mode', () => ({
@@ -29,14 +30,26 @@ vi.mock('prompts', () => ({
2930
import { detectCliMode } from '../../helpers/cli-mode.js'
3031
import prompts from 'prompts'
3132
import * as api from '../../rest/api.js'
33+
import { buildConfirmCommand } from '../../helpers/command-preview.js'
3234
import { AuthCommand } from '../authCommand.js'
3335
import Destroy from '../destroy.js'
3436

35-
function createCommandContext (parsed: unknown) {
37+
/** Turn a generated confirmCommand back into argv, so oclif can re-parse it. */
38+
function flagArgv (confirmCommand: string): string[] {
39+
return [...confirmCommand.matchAll(/--[a-zA-Z0-9-]+(?:="[^"]*")?/g)]
40+
.map(([token]) => token.replace(/="(.*)"$/, '=$1'))
41+
}
42+
43+
async function confirmCommandFor (argv: string[]): Promise<string> {
44+
const { flags, metadata } = await Parser.parse(argv, { flags: Destroy.flags, strict: true })
45+
return buildConfirmCommand('destroy', flags, undefined, metadata.flags)
46+
}
47+
48+
function createCommandContext (parsed: { flags: Record<string, unknown>, metadata?: unknown }) {
3649
const logged: string[] = []
3750
let exitCodeValue: number | undefined
3851
return {
39-
parse: vi.fn().mockResolvedValue(parsed),
52+
parse: vi.fn().mockResolvedValue({ metadata: { flags: {} }, ...parsed }),
4053
log: vi.fn((msg?: string) => {
4154
if (msg) logged.push(msg)
4255
}),
@@ -172,3 +185,20 @@ describe('destroy confirmation flow', () => {
172185
expect(Destroy.idempotent).toBe(false)
173186
})
174187
})
188+
189+
describe('destroy confirmCommand', () => {
190+
it('echoes only the flags the user typed', async () => {
191+
expect(await confirmCommandFor([])).toBe('checkly destroy --force')
192+
expect(await confirmCommandFor(['--preserve-resources'])).toBe('checkly destroy --preserve-resources --force')
193+
})
194+
195+
it('generates a command oclif can parse back', async () => {
196+
for (const argv of [[], ['--preserve-resources'], ['--cancel-in-progress-deployment']]) {
197+
const confirmCommand = await confirmCommandFor(argv)
198+
await expect(
199+
Parser.parse(flagArgv(confirmCommand), { flags: Destroy.flags, strict: true }),
200+
`confirmCommand for "checkly destroy ${argv.join(' ')}" must be runnable: ${confirmCommand}`,
201+
).resolves.toBeDefined()
202+
}
203+
})
204+
})

packages/cli/src/commands/deploy.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export default class Deploy extends AuthCommand {
110110
}
111111

112112
async run (): Promise<void> {
113-
const { flags } = await this.parse(Deploy)
113+
const { flags, metadata } = await this.parse(Deploy)
114114
const {
115115
force,
116116
preview,
@@ -146,6 +146,7 @@ export default class Deploy extends AuthCommand {
146146
: 'Delete any resources removed from code, losing their run history. Pass --preserve-resources to keep them in your Checkly account instead',
147147
],
148148
flags,
149+
flagMetadata: metadata.flags,
149150
classification: {
150151
readOnly: Deploy.readOnly,
151152
destructive: Deploy.destructive,
@@ -316,6 +317,7 @@ export default class Deploy extends AuthCommand {
316317
`Delete ${PRETTY_RESOURCE_TYPES[resourceType] ?? resourceType}: ${logicalId}`),
317318
],
318319
flags,
320+
flagMetadata: metadata.flags,
319321
classification: {
320322
readOnly: Deploy.readOnly,
321323
destructive: Deploy.destructive,

packages/cli/src/commands/destroy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default class Destroy extends AuthCommand {
3131
}
3232

3333
async run (): Promise<void> {
34-
const { flags } = await this.parse(Destroy)
34+
const { flags, metadata } = await this.parse(Destroy)
3535
const {
3636
config: configFilename,
3737
'preserve-resources': preserveResources,
@@ -52,6 +52,7 @@ export default class Destroy extends AuthCommand {
5252
: `PERMANENTLY delete ALL resources associated with the project "${checklyConfig.projectName}" in account "${account.name}"`,
5353
],
5454
flags,
55+
flagMetadata: metadata.flags,
5556
classification: {
5657
readOnly: Destroy.readOnly,
5758
destructive: Destroy.destructive,

0 commit comments

Comments
 (0)