diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a380732ae..49b7437fd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 - 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 }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index afb9e2517..b2fc172d9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,18 +105,18 @@ 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. -2. When release is published the Github action is triggered. It builds and publishes `#.#.#-prerelease` prereleases (for both packages). +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). 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 If you notice an issue when testing the prerelease you can still roll everything back. Simply delete the GitHub release, and delete the corresponding tags from the GitHub UI (both `#.#.#` and `v#.#.#`). -After resolving the issues, you can create another Github release and go through the process again. +After resolving the issues, you can create another GitHub release and go through the process again. ## Style Guide diff --git a/packages/cli/e2e/__tests__/help.spec.ts b/packages/cli/e2e/__tests__/help.spec.ts index db8a79c53..14a91d72b 100644 --- a/packages/cli/e2e/__tests__/help.spec.ts +++ b/packages/cli/e2e/__tests__/help.spec.ts @@ -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. diff --git a/packages/cli/scripts/prepare-ai-context.ts b/packages/cli/scripts/prepare-ai-context.ts index 0fc23f957..f60729ddb 100644 --- a/packages/cli/scripts/prepare-ai-context.ts +++ b/packages/cli/scripts/prepare-ai-context.ts @@ -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') @@ -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 { await mkdir(dir, { recursive: true }) const outputPath = join(dir, filename) @@ -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) { @@ -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( @@ -144,29 +123,15 @@ async function prepareContext () { .replace('', 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 diff --git a/packages/cli/src/commands/__tests__/rules.spec.ts b/packages/cli/src/commands/__tests__/rules.spec.ts new file mode 100644 index 000000000..2928d8647 --- /dev/null +++ b/packages/cli/src/commands/__tests__/rules.spec.ts @@ -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).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('is deprecated'))).toBe(true) + expect(logged.some(m => m.includes('checkly skills'))).toBe(true) + }) +}) diff --git a/packages/cli/src/commands/rules.ts b/packages/cli/src/commands/rules.ts index 3ed224ac0..0e1ade66d 100644 --- a/packages/cli/src/commands/rules.ts +++ b/packages/cli/src/commands/rules.ts @@ -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 { - // 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 { - 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 { - 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 { + this.log('The `rules` command is deprecated. Use `checkly skills` instead.') + return Promise.resolve() } }