Skip to content

Commit 7b045a4

Browse files
Merge branch 'main' into feat/deploy-show-resource-details
2 parents b9508d8 + f62ffa6 commit 7b045a4

Some content is hidden

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

45 files changed

+1981
-37
lines changed

.github/workflows/release-canary.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ jobs:
1818
with:
1919
node-version: '20.x'
2020
registry-url: 'https://registry.npmjs.org'
21+
- name: Upgrade npm for trusted publishing support
22+
run: |
23+
npm install -g npm@latest
24+
echo "npm version: $(npm --version)"
2125
# Extract the dynamic value from the canary label if present
2226
- name: Extract CANARY_TAG
2327
id: extract-canary

.github/workflows/release.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ jobs:
3939
with:
4040
node-version: '20.x'
4141
registry-url: 'https://registry.npmjs.org'
42-
env:
43-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
42+
- name: Upgrade npm for trusted publishing support
43+
run: |
44+
npm install -g npm@latest
45+
echo "npm version: $(npm --version)"
4446
# Ensure that the README is published with the package
4547
- run: rm -f packages/cli/README.md && cp README.md packages/cli
4648
- run: npm ci
@@ -105,9 +107,11 @@ jobs:
105107
with:
106108
node-version: '20.x'
107109
registry-url: 'https://registry.npmjs.org'
108-
env:
109-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
110-
# Ensure that the README is published with the package
110+
- name: Upgrade npm for trusted publishing support
111+
run: |
112+
npm install -g npm@latest
113+
echo "npm version: $(npm --version)"
114+
# Ensure that the README is published with the package
111115
- run: rm -f packages/cli/README.md && cp README.md packages/cli
112116
- run: npm ci
113117
- name: Set version and publish 'cli' package

CONTRIBUTING.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,18 @@ triggered and a new experimental version can be installed by executing:
8787
npm install checkly@0.0.0-pr.<PR-NUMBER>.<COMMIT_SHORT_SHA>
8888
```
8989

90+
> **Note:** Canary builds authenticate to npm using the `NPM_TOKEN` secret (a long-lived token). This is because the canary workflow (`release-canary.yml`) is a separate workflow file from the one configured as a trusted publisher on npmjs.com.
91+
9092
## Releasing
9193

94+
### NPM authentication
95+
96+
The release workflow (`release.yml`) uses [npm trusted publishing](https://docs.npmjs.com/generating-provenance-statements) with GitHub OIDC — no long-lived npm token is needed. GitHub Actions mints a short-lived OIDC token that npm validates against the trusted publisher configuration on npmjs.com.
97+
98+
Both `checkly` and `create-checkly` packages have trusted publishing configured for `checkly/checkly-cli` / `release.yml`. There is no secret to rotate for releases.
99+
100+
The canary workflow (`release-canary.yml`) still uses the `NPM_TOKEN` secret because npm only allows one trusted publisher entry per package, and it is bound to `release.yml`.
101+
92102
### Releasing `checkly` packages
93103

94104
Both packages [checkly](https://www.npmjs.com/package/checkly) and [create-cli](https://www.npmjs.com/package/create-checkly) are built and published by the corresponding GitHub action [here](https://github.com/checkly/checkly-cli/actions/workflows/release.yml).
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import config from 'config'
2+
import { describe, it, expect } from 'vitest'
3+
4+
import { runChecklyCli } from '../run-checkly'
5+
6+
describe('checkly account plan', () => {
7+
it('should show plan summary with default output', async () => {
8+
const result = await runChecklyCli({
9+
args: ['account', 'plan'],
10+
apiKey: config.get('apiKey'),
11+
accountId: config.get('accountId'),
12+
})
13+
expect(result.status).toBe(0)
14+
expect(result.stdout).toContain('Plan:')
15+
expect(result.stdout).toContain('Metered entitlements')
16+
expect(result.stdout).toContain('additional features enabled')
17+
})
18+
19+
it('should output valid JSON with --output json', async () => {
20+
const result = await runChecklyCli({
21+
args: ['account', 'plan', '--output', 'json'],
22+
apiKey: config.get('apiKey'),
23+
accountId: config.get('accountId'),
24+
})
25+
expect(result.status).toBe(0)
26+
const parsed = JSON.parse(result.stdout)
27+
expect(parsed).toHaveProperty('plan')
28+
expect(parsed).toHaveProperty('planDisplayName')
29+
expect(parsed).toHaveProperty('addons')
30+
expect(parsed).toHaveProperty('entitlements')
31+
expect(Array.isArray(parsed.entitlements)).toBe(true)
32+
expect(parsed.entitlements.length).toBeGreaterThan(0)
33+
expect(parsed.entitlements[0]).toHaveProperty('key')
34+
expect(parsed.entitlements[0]).toHaveProperty('type')
35+
expect(parsed.entitlements[0]).toHaveProperty('enabled')
36+
})
37+
38+
it('should output markdown with --output md', async () => {
39+
const result = await runChecklyCli({
40+
args: ['account', 'plan', '--output', 'md'],
41+
apiKey: config.get('apiKey'),
42+
accountId: config.get('accountId'),
43+
})
44+
expect(result.status).toBe(0)
45+
expect(result.stdout).toContain('# Plan:')
46+
expect(result.stdout).toContain('| Name | Limit |')
47+
})
48+
49+
it('should show detail view for a specific entitlement key', async () => {
50+
const result = await runChecklyCli({
51+
args: ['account', 'plan', 'BROWSER_CHECKS'],
52+
apiKey: config.get('apiKey'),
53+
accountId: config.get('accountId'),
54+
})
55+
expect(result.status).toBe(0)
56+
expect(result.stdout).toContain('BROWSER_CHECKS')
57+
expect(result.stdout).toContain('Browser checks')
58+
})
59+
60+
it('should output single entitlement as JSON', async () => {
61+
const result = await runChecklyCli({
62+
args: ['account', 'plan', 'BROWSER_CHECKS', '--output', 'json'],
63+
apiKey: config.get('apiKey'),
64+
accountId: config.get('accountId'),
65+
})
66+
expect(result.status).toBe(0)
67+
const parsed = JSON.parse(result.stdout)
68+
expect(parsed.key).toBe('BROWSER_CHECKS')
69+
expect(parsed.type).toBe('metered')
70+
})
71+
72+
it('should filter by --type metered', async () => {
73+
const result = await runChecklyCli({
74+
args: ['account', 'plan', '--type', 'metered'],
75+
apiKey: config.get('apiKey'),
76+
accountId: config.get('accountId'),
77+
})
78+
expect(result.status).toBe(0)
79+
expect(result.stdout).toContain('LIMIT')
80+
expect(result.stdout).toContain('entitlement')
81+
})
82+
83+
it('should filter by --type flag', async () => {
84+
const result = await runChecklyCli({
85+
args: ['account', 'plan', '--type', 'flag'],
86+
apiKey: config.get('apiKey'),
87+
accountId: config.get('accountId'),
88+
})
89+
expect(result.status).toBe(0)
90+
expect(result.stdout).toContain('ENABLED')
91+
expect(result.stdout).toContain('entitlement')
92+
})
93+
94+
it('should filter by --search', async () => {
95+
const result = await runChecklyCli({
96+
args: ['account', 'plan', '--search', 'browser'],
97+
apiKey: config.get('apiKey'),
98+
accountId: config.get('accountId'),
99+
})
100+
expect(result.status).toBe(0)
101+
expect(result.stdout).toContain('Browser')
102+
expect(result.stdout).toContain('entitlement')
103+
})
104+
105+
it('should fail for unknown entitlement key', async () => {
106+
const result = await runChecklyCli({
107+
args: ['account', 'plan', 'NONEXISTENT_KEY'],
108+
apiKey: config.get('apiKey'),
109+
accountId: config.get('accountId'),
110+
})
111+
expect(result.status).toBe(2)
112+
expect(result.stderr).toContain('not found')
113+
})
114+
115+
it('should fail when combining key with --type', async () => {
116+
const result = await runChecklyCli({
117+
args: ['account', 'plan', 'BROWSER_CHECKS', '--type', 'metered'],
118+
apiKey: config.get('apiKey'),
119+
accountId: config.get('accountId'),
120+
})
121+
expect(result.status).not.toBe(0)
122+
})
123+
})

packages/cli/e2e/__tests__/checks-empty-account.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,29 @@ describe.skipIf(!apiKey || !accountId)('checks commands on empty account', () =>
4949
expect(result.stdout).toContain('No checks matching')
5050
})
5151

52+
it('should show "No checks found." for checks stats', async () => {
53+
const result = await runChecklyCli({
54+
args: ['checks', 'stats'],
55+
apiKey,
56+
accountId,
57+
})
58+
expect(result.status).toBe(0)
59+
expect(result.stdout).toContain('No checks found.')
60+
})
61+
62+
it('should return empty JSON for checks stats --output json', async () => {
63+
const result = await runChecklyCli({
64+
args: ['checks', 'stats', '--output', 'json'],
65+
apiKey,
66+
accountId,
67+
})
68+
expect(result.status).toBe(0)
69+
const parsed = JSON.parse(result.stdout)
70+
expect(parsed.data).toEqual([])
71+
expect(parsed.pagination.total).toBe(0)
72+
expect(parsed.range).toBe('last24Hours')
73+
})
74+
5275
it('should fail gracefully for checks get on empty account', async () => {
5376
const result = await runChecklyCli({
5477
args: ['checks', 'get', '00000000-0000-0000-0000-000000000000'],
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import config from 'config'
2+
import { describe, it, expect } from 'vitest'
3+
4+
import { runChecklyCli } from '../run-checkly'
5+
6+
describe('checkly checks stats', () => {
7+
it('should show stats with default output', async () => {
8+
const result = await runChecklyCli({
9+
args: ['checks', 'stats'],
10+
apiKey: config.get('apiKey'),
11+
accountId: config.get('accountId'),
12+
})
13+
expect(result.status).toBe(0)
14+
expect(result.stdout).toBeTruthy()
15+
expect(result.stdout).toContain('STATS')
16+
})
17+
18+
it('should output valid JSON with --output json', async () => {
19+
const result = await runChecklyCli({
20+
args: ['checks', 'stats', '--output', 'json'],
21+
apiKey: config.get('apiKey'),
22+
accountId: config.get('accountId'),
23+
})
24+
expect(result.status).toBe(0)
25+
const parsed = JSON.parse(result.stdout)
26+
expect(parsed).toHaveProperty('data')
27+
expect(Array.isArray(parsed.data)).toBe(true)
28+
expect(parsed).toHaveProperty('range')
29+
expect(parsed).toHaveProperty('pagination')
30+
expect(parsed.data.length).toBeGreaterThan(0)
31+
expect(parsed.data[0]).toHaveProperty('checkId')
32+
expect(parsed.data[0]).toHaveProperty('availability')
33+
})
34+
35+
it('should output markdown with --output md', async () => {
36+
const result = await runChecklyCli({
37+
args: ['checks', 'stats', '--output', 'md'],
38+
apiKey: config.get('apiKey'),
39+
accountId: config.get('accountId'),
40+
})
41+
expect(result.status).toBe(0)
42+
expect(result.stdout).toContain('| Name | Type | Status |')
43+
})
44+
45+
it('should respect --limit flag', async () => {
46+
const result = await runChecklyCli({
47+
args: ['checks', 'stats', '--output', 'json', '--limit', '1'],
48+
apiKey: config.get('apiKey'),
49+
accountId: config.get('accountId'),
50+
})
51+
expect(result.status).toBe(0)
52+
const parsed = JSON.parse(result.stdout)
53+
expect(parsed.data).toHaveLength(1)
54+
expect(parsed.pagination.limit).toBe(1)
55+
})
56+
57+
it('should respect --range flag', async () => {
58+
const result = await runChecklyCli({
59+
args: ['checks', 'stats', '--output', 'json', '--range', 'last7Days'],
60+
apiKey: config.get('apiKey'),
61+
accountId: config.get('accountId'),
62+
})
63+
expect(result.status).toBe(0)
64+
const parsed = JSON.parse(result.stdout)
65+
expect(parsed.range).toBe('last7Days')
66+
})
67+
68+
it('should return no results for impossible search filter', async () => {
69+
const result = await runChecklyCli({
70+
args: ['checks', 'stats', '--search', 'zzz-nonexistent-check-name-zzz'],
71+
apiKey: config.get('apiKey'),
72+
accountId: config.get('accountId'),
73+
})
74+
expect(result.status).toBe(0)
75+
expect(result.stdout).toContain('No checks found.')
76+
})
77+
78+
it('should return empty JSON for impossible search filter with --output json', async () => {
79+
const result = await runChecklyCli({
80+
args: ['checks', 'stats', '--search', 'zzz-nonexistent-check-name-zzz', '--output', 'json'],
81+
apiKey: config.get('apiKey'),
82+
accountId: config.get('accountId'),
83+
})
84+
expect(result.status).toBe(0)
85+
const parsed = JSON.parse(result.stdout)
86+
expect(parsed.data).toEqual([])
87+
expect(parsed.pagination.total).toBe(0)
88+
})
89+
})

packages/cli/e2e/__tests__/help.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ describe('help', () => {
4949
trigger Trigger your existing checks on Checkly.`)
5050

5151
expect(stdout).toContain(`ADDITIONAL COMMANDS
52+
account View and manage your Checkly account.
5253
checks List and inspect checks in your Checkly account.
5354
destroy Destroy your project with all its related resources.
5455
env Manage Checkly environment variables.

packages/cli/e2e/__tests__/whoami.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ describe('whomai', () => {
1111
accountId: config.get('accountId'),
1212
})
1313
expect(stdout).toContain(config.get('accountName'))
14+
expect(stdout).toContain('Plan:')
1415
})
1516
})

packages/cli/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@
6464
"message": "<%= config.name %> update available from <%= chalk.greenBright(config.version) %> to <%= chalk.greenBright(latest) %>. To update, run `npm install -D checkly@latest`"
6565
},
6666
"topics": {
67+
"account": {
68+
"description": "View and manage your Checkly account."
69+
},
6770
"checks": {
6871
"description": "List and inspect checks in your Checkly account."
6972
},
@@ -76,9 +79,6 @@
7679
"import": {
7780
"description": "Import existing resources from your Checkly account to your project."
7881
},
79-
"skills": {
80-
"description": "Explore Checkly AI agent skills, actions and reference documentation."
81-
},
8282
"status-pages": {
8383
"description": "List and manage status pages in your Checkly account."
8484
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ Agent Skills are a standardized format for giving AI agents specialized knowledg
2626
- Build dashboards and status pages
2727
- Follow monitoring-as-code best practices with the Checkly CLI
2828

29+
## Installing This Skill
30+
31+
Run `npx checkly skills install` to install the skill into your project. The command supports Claude Code, Cursor, Codex and more out of the box, or you can specify a custom path.
32+
2933
## Using This Skill
3034

3135
AI agents can load this skill to gain expertise in Checkly monitoring. The skill follows the [Agent Skills specification](https://agentskills.io) with:

0 commit comments

Comments
 (0)