Skip to content
Open
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
11 changes: 1 addition & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,22 +171,13 @@ jobs:
SLACK_TITLE: ':white_check_mark: NPM publish succeeded'
SLACK_MESSAGE: by ${{ github.actor }}
SLACK_FOOTER: ''
# Attach checkly.rules.md to the GitHub Release and mark it as latest
# Mark the GitHub Release as latest if it's the highest version
finalize-release:
runs-on: ubuntu-latest
needs: release
permissions:
contents: write
steps:
- name: Download LLM rules artifact

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is here to make the rules accessible on the docs via a marketing site redirect. I'll clean that all up.

uses: actions/download-artifact@v4
with:
name: llm-rules-release
path: llm-rules-release
- name: Upload checkly.rules.md to GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload ${{ github.event.release.tag_name }} llm-rules-release/checkly.rules.md --repo ${{ github.repository }}
- name: Mark release as latest if version is highest
env:
GH_TOKEN: ${{ github.token }}
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ Both packages [checkly](https://www.npmjs.com/package/checkly) and [create-cli](

To release packages to NPM:

1. Publish a Github Release with a valid tag `#.#.#` (do **not** include a `v` prefix) and click the `Generate release notes` button to auto-generate notes following format defined [here](https://github.com/checkly/checkly-cli/blob/main/.github/release.yml). **Uncheck "Set as the latest release"** — the workflow will mark it as latest automatically after attaching the `checkly.rules.md` asset.
1. Publish a Github Release with a valid tag `#.#.#` (do **not** include a `v` prefix) and click the `Generate release notes` button to auto-generate notes following format defined [here](https://github.com/checkly/checkly-cli/blob/main/.github/release.yml). **Uncheck "Set as the latest release"** — the workflow will mark it as latest automatically.
2. When release is published the Github action is triggered. It builds and publishes `#.#.#-prerelease` prereleases (for both packages).
Comment on lines +108 to 109
3. Test the prerelease version to make sure that it's working.
* To test `npm create checkly`, run `CHECKLY_CLI_VERSION=4.6.2 npm create checkly@4.6.2-prerelease-c6e8165` (substituting `4.6.2` and `4.6.2-prerelease` for your versions, which you can find at https://www.npmjs.com/package/checkly?activeTab=versions). `CHECKLY_CLI_VERSION` is needed since the `create-checkly` package looks up the corresponding tag on GitHub to pull project templates.
* Ensure your project `package.json` has `"checkly": "4.6.2-prerelease-c6e8165"`
4. A `production` deployment will be waiting for approval. After it's approved, the `#.#.#` version will be published. The workflow will then automatically attach `checkly.rules.md` to the GitHub Release and mark it as latest (if the version is higher than the current latest).
4. A `production` deployment will be waiting for approval. After it's approved, the `#.#.#` version will be published. The workflow will then automatically mark the GitHub Release as latest (if the version is higher than the current latest).

### Catching issues in prerelease

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/e2e/__tests__/help.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe('help', () => {
logout Log out and clear any local credentials.
members List account members and pending invites.
rca Trigger and retrieve root cause analyses.
rules Generate a rules file to use with AI IDEs and Copilots.
rules Deprecated. Use \`checkly skills\` instead.
runtimes List all supported runtimes and dependencies.
skills Show Checkly AI skills, actions and their references.
status-pages List and manage status pages in your Checkly account.
Expand Down
41 changes: 3 additions & 38 deletions packages/cli/scripts/prepare-ai-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ACTIONS, EXAMPLE_CONFIGS } from '../src/ai-context/context'

const EXAMPLES_DIR = join(__dirname, '../gen/')
const AI_CONTEXT_DIR = join(__dirname, '../src/ai-context')
const RULES_OUTPUT_DIR = join(__dirname, '../dist/ai-context')
const DIST_AI_CONTEXT_DIR = join(__dirname, '../dist/ai-context')

// Reference files served by the CLI's `checkly skills [action] [reference]` command
const COMMAND_REFERENCES_DIR = join(__dirname, '../dist/ai-context/skills-command/references')
Expand All @@ -13,21 +13,6 @@ const COMMAND_REFERENCES_DIR = join(__dirname, '../dist/ai-context/skills-comman
const PUBLIC_SKILLS_DIR = join(__dirname, '../dist/ai-context/public-skills')
const PUBLIC_SKILL_DIR = join(PUBLIC_SKILLS_DIR, 'checkly')

function stripYamlFrontmatter (content: string): string {
const frontmatterRegex = /^---\r?\n[\s\S]*?\r?\n---\r?\n+/
return content.replace(frontmatterRegex, '')
}

// Demote headings by two levels (# -> ###, ## -> ####) to maintain proper
// heading hierarchy in checkly.rules.md.
function demoteHeadings (content: string): string {
return content.replace(/^(#+)/gm, '##$1')
}

function normalizeBlankLines (content: string): string {
return content.replace(/\n{3,}/g, '\n\n')
}

async function writeOutput (content: string, dir: string, filename: string): Promise<void> {
await mkdir(dir, { recursive: true })
const outputPath = join(dir, filename)
Expand Down Expand Up @@ -102,8 +87,6 @@ async function prepareContext () {
const examples = await readExampleCode()

// Process all actions — reference files, action headers, and standalone actions
const configureReferenceContents: string[] = []

for (const action of ACTIONS) {
if ('references' in action) {
for (const ref of action.references) {
Expand All @@ -113,10 +96,6 @@ async function prepareContext () {
)
refContent = replaceExamples(refContent, examples)
await writeOutput(refContent, COMMAND_REFERENCES_DIR, `${ref.id}.md`)

if (action.id === 'configure') {
configureReferenceContents.push(refContent)
}
}

let actionContent = await readFile(
Expand Down Expand Up @@ -144,29 +123,15 @@ async function prepareContext () {
.replace('<!-- SKILL_COMMANDS -->', generateSkillCommands())
await writeOutput(skillTemplate, PUBLIC_SKILL_DIR, 'SKILL.md')

// Generate checkly.rules.md (configure header + all configure-* references concatenated)
const configureContent = await readFile(
join(COMMAND_REFERENCES_DIR, 'configure.md'),
'utf8',
)
const demotedReferences = configureReferenceContents
.map(demoteHeadings).join('\n\n')
const rulesContent = normalizeBlankLines(stripYamlFrontmatter(
configureContent
+ '\n'
+ demotedReferences,
))
await writeOutput(rulesContent, RULES_OUTPUT_DIR, 'checkly.rules.md')

// Copy README
const readme = await readFile(join(AI_CONTEXT_DIR, 'README.md'), 'utf8')
await writeOutput(readme, PUBLIC_SKILL_DIR, 'README.md')

// Copy onboarding assets to dist
for (const dir of ['onboarding-boilerplate', 'onboarding-prompts']) {
await cp(join(AI_CONTEXT_DIR, dir), join(RULES_OUTPUT_DIR, dir), { recursive: true })
await cp(join(AI_CONTEXT_DIR, dir), join(DIST_AI_CONTEXT_DIR, dir), { recursive: true })
// eslint-disable-next-line no-console
console.log(`Copied ${dir} to ${join(RULES_OUTPUT_DIR, dir)}`)
console.log(`Copied ${dir} to ${join(DIST_AI_CONTEXT_DIR, dir)}`)
}
} catch (error) {
// eslint-disable-next-line no-console
Expand Down
31 changes: 31 additions & 0 deletions packages/cli/src/commands/__tests__/rules.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it, vi } from 'vitest'

import Rules from '../rules.js'

const mockConfig = {
runHook: vi.fn().mockResolvedValue({ successes: [], failures: [] }),
} as any

function createCommand (...args: string[]) {
const cmd = new Rules(args, mockConfig)
cmd.log = vi.fn() as any
return cmd
}

function getLogged (cmd: Rules): string[] {
return (cmd.log as ReturnType<typeof vi.fn>).mock.calls.map(
(call: string[]) => call[0],
)
}

describe('rules', () => {
it('prints the deprecation message pointing to skills', async () => {
const cmd = createCommand()

await cmd.run()

const logged = getLogged(cmd)
expect(logged.some(m => m.includes('Rules were deprecated.'))).toBe(true)
expect(logged.some(m => m.includes('npx checkly skills'))).toBe(true)
Comment on lines +27 to +29
})
})
142 changes: 5 additions & 137 deletions packages/cli/src/commands/rules.ts
Original file line number Diff line number Diff line change
@@ -1,147 +1,15 @@
import { BaseCommand } from './baseCommand.js'
import { readFile, writeFile, access, mkdir } from 'fs/promises'
import path, { join } from 'path'
import { constants } from 'fs'
import prompts from 'prompts'
import { fileURLToPath } from 'node:url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const BASE_RULES_FILE_PATH = join(__dirname, '../ai-context/checkly.rules.md')

// AI IDE configurations mapping
const AI_IDE_CONFIGS = {
'Windsurf': {
rulesFolder: '.windsurf/rules',
rulesFileName: 'checkly.md',
},
'GitHub Copilot': {
rulesFolder: '.github/instructions',
rulesFileName: 'checkly.instructions.md',
},
'Cursor': {
rulesFolder: '.cursor/rules',
rulesFileName: 'checkly.mdc',
},
'Plain Markdown (checkly.md)': {
rulesFolder: '.',
rulesFileName: 'checkly.md',
},
} as const

export default class Rules extends BaseCommand {
static hidden = false
static readOnly = true
static idempotent = true
static state = 'deprecated'
static description =
'Generate a rules file to use with AI IDEs and Copilots.'

async run (): Promise<void> {
// Read the base rules file
const rulesContent = await this.readBaseRulesFile()
if (!rulesContent) {
this.error(`Failed to read rules file at ${BASE_RULES_FILE_PATH}`)
}

// In non-interactive mode, print rules to stdout and exit
const isNonInteractive = !process.stdin.isTTY
|| !process.stdout.isTTY
|| process.env.CI
|| process.env.CHECKLY_NON_INTERACTIVE
if (isNonInteractive) {
this.log(rulesContent)
return
}

try {
// Create options for multiselect - offer all configs from AI_IDE_CONFIGS
const choices = Object.entries(AI_IDE_CONFIGS).map(([ideName, ideConfig]) => {
return {
title: `${ideName} (${path.join(ideConfig.rulesFolder, ideConfig.rulesFileName)})`,
value: ideConfig,
selected: false,
}
})

const isNonInteractive = !process.stdin.isTTY
|| !process.stdout.isTTY
|| process.env.CI
|| process.env.CHECKLY_NON_INTERACTIVE

// Interactive mode - show multiselect
const { configs: selectedConfig } = await prompts({
type: 'select',
name: 'configs',
message: 'Select the AI IDE configurations to generate rules for:',
choices,
initial: 0,
})

if (!selectedConfig) {
this.log('Operation cancelled.')
return
}

this.log(`Generating rules`)

// Create rules directory if it doesn't exist
const rulesDir = join(process.cwd(), selectedConfig.rulesFolder)
try {
await mkdir(rulesDir, { recursive: true })
} catch {
// Directory might already exist, ignore error
}

// Determine the target file path
const rulesFilePath = join(rulesDir, selectedConfig.rulesFileName)

// Check if file already exists and ask for confirmation (only in interactive mode)
let shouldOverwrite = true
if (!isNonInteractive) {
shouldOverwrite = await this.confirmOverwrite(rulesFilePath)
}

if (!shouldOverwrite) {
this.log(`Skipped ${rulesFilePath}`)
return
}

// Save the rules file
await writeFile(rulesFilePath, rulesContent, 'utf8')

this.log(`✅ Successfully saved Checkly rules file to: ${rulesFilePath}`)
} catch (error) {
this.error(`Failed to generate rules file: ${error}`)
}
}

private async readBaseRulesFile (): Promise<string> {
try {
return await readFile(BASE_RULES_FILE_PATH, 'utf8')
} catch (error) {
throw new Error(
`Failed to read base rules file at ${BASE_RULES_FILE_PATH}: ${error}`,
{ cause: error },
)
}
}

private async confirmOverwrite (targetPath: string): Promise<boolean> {
try {
await access(targetPath, constants.F_OK)

// File exists, ask for confirmation
const { overwrite } = await prompts({
type: 'confirm',
name: 'overwrite',
message: `Rules file already exists at ${targetPath}. Do you want to overwrite it?`,
initial: false,
})
'Deprecated. Use `checkly skills` instead.'

return overwrite ?? false
} catch {
// File doesn't exist, no need to confirm
return true
}
run (): Promise<void> {
this.log('Rules were deprecated. Use `npx checkly skills`.')
Comment on lines 8 to +12
return Promise.resolve()
}
}
Loading