From 5cba863ce08e7c63cc904599bf64b502bc2b7f68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:03:45 +0000 Subject: [PATCH 1/2] chore: add CLAUDE.md generation workflow and script --- .claude/README.md | 49 ++++ ...-beforeeach-in-unit-service-level-tests.md | 51 ++++ .github/workflows/generate-claude-md.yml | 106 ++++++++ CLAUDE.md | 9 + CONTRIBUTING.md | 16 ++ script/generate-claude-md.ts | 235 ++++++++++++++++++ 6 files changed, 466 insertions(+) create mode 100644 .claude/README.md create mode 100644 .claude/instructions/setup-function-instead-of-beforeeach-in-unit-service-level-tests.md create mode 100644 .github/workflows/generate-claude-md.yml create mode 100644 CLAUDE.md create mode 100755 script/generate-claude-md.ts diff --git a/.claude/README.md b/.claude/README.md new file mode 100644 index 0000000000..7aa0bcf42b --- /dev/null +++ b/.claude/README.md @@ -0,0 +1,49 @@ +# Contribution Guidelines + +This directory contains AI instructions extracted from RFC (Request for Comments) discussions that have been approved and landed in the project. + +## How It Works + +This directory is automatically maintained by the GitHub Actions workflow `.github/workflows/generate-claude-md.yml`. The workflow: + +1. Monitors discussions in the "Contribution RFC" category +2. When a discussion receives the `RFC:Landed` label, its "## AI Instructions" section is extracted +3. Creates/updates a markdown file in this directory with the extracted content +4. Generates the root `CLAUDE.md` file with links to all RFC files +5. Creates a PR for human review + +## File Naming Convention + +RFC files are named based on the discussion title: +- Convert to lowercase +- Replace spaces with hyphens +- Remove special characters + +Example: "Testing Strategy" → `testing-strategy.md` + +## Structure + +Each RFC file contains: +- Title of the RFC as a header +- AI Instructions extracted from the discussion + +## Adding New RFCs + +To add a new RFC to this system: + +1. Create or edit a discussion in the "Contribution RFC" category +2. Add a section titled `## AI Instructions` with the guidance for AI coding assistants +3. Add the `RFC:Landed` label to the discussion +4. The workflow will automatically create a PR with the changes + +## Removing RFCs + +To remove an RFC: +1. Remove the `RFC:Landed` label from the discussion +2. The workflow will automatically create a PR that removes the file + +## Manual Updates + +While files are auto-generated, if you need to make manual corrections: +1. Edit the source discussion on GitHub +2. The workflow will regenerate the files on the next trigger diff --git a/.claude/instructions/setup-function-instead-of-beforeeach-in-unit-service-level-tests.md b/.claude/instructions/setup-function-instead-of-beforeeach-in-unit-service-level-tests.md new file mode 100644 index 0000000000..30e371f941 --- /dev/null +++ b/.claude/instructions/setup-function-instead-of-beforeeach-in-unit-service-level-tests.md @@ -0,0 +1,51 @@ +--- +description: +globs: **/*.spec.tsx,**/*.spec.ts +alwaysApply: false +--- +## Use setup function instead of beforeEach + +## Description +- Use `setup` function instead of `beforeEach` +- `setup` function must be at the bottom of the root `describe` block +- `setup` function creates an object under test and returns it +- `setup` function should accept a single parameter with inline type definition +- Don't use shared state in `setup` function +- Don't specify return type of `setup` function + +## Examples + +### Good +```typescript +describe("UserProfile", () => { + it("renders user name when provided", () => { + setup({ name: "John Doe" }); + expect(screen.queryByText("John Doe")).toBeInTheDocument(); + }); + + function setup(input: { name?: string; email?: string; isLoading?: boolean; error?: string }) { + render(); + return input; + } +}); +``` + +### Bad +```typescript +describe("UserProfile", () => { + let props: UserProfileProps; + + beforeEach(() => { + props = { name: "John Doe" }; + render(); + }); + + it("renders user name when provided", () => { + expect(screen.getByText("John Doe")).toBeInTheDocument(); + }); +}); +``` + +## References +- [DeploymentName.spec.tsx](mdc:apps/deploy-web/src/components/deployments/DeploymentName/DeploymentName.spec.tsx) +- https://github.com/akash-network/console/discussions/910 \ No newline at end of file diff --git a/.github/workflows/generate-claude-md.yml b/.github/workflows/generate-claude-md.yml new file mode 100644 index 0000000000..5204f08441 --- /dev/null +++ b/.github/workflows/generate-claude-md.yml @@ -0,0 +1,106 @@ +name: Generate CLAUDE.md + +on: + discussion: + types: [labeled, unlabeled, edited] + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +env: + CONTRIBUTION_BRANCH: automated/claude-md-update + +jobs: + generate: + name: Generate CLAUDE.md + if: github.event.discussion.category.name == 'Contribution RFC' + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Setup Node.js + uses: volta-cli/action@v4 + + - name: Generate CLAUDE.md + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + node --experimental-strip-types --no-warnings script/generate-claude-md.ts + + - name: Check for Changes + id: check-changes + run: | + if git diff --quiet CLAUDE.md .claude/instructions/; then + echo "has_changes=false" >> "$GITHUB_OUTPUT" + echo "No changes detected" + else + echo "has_changes=true" >> "$GITHUB_OUTPUT" + echo "Changes detected" + fi + + - name: Configure Git + if: steps.check-changes.outputs.has_changes == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Commit and Push Changes + if: steps.check-changes.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Create or switch to the branch + git fetch origin "${{ env.CONTRIBUTION_BRANCH }}" 2>/dev/null || true + git checkout -B "${{ env.CONTRIBUTION_BRANCH }}" + + # Stage and commit changes + git add CLAUDE.md .claude/instructions/ + git commit -m "chore: update CLAUDE.md from RFC discussions" \ + -m "Discussion #${{ github.event.discussion.number }} was ${{ github.event.action }}" + + # Push changes + git push -f origin "${{ env.CONTRIBUTION_BRANCH }}" + + - name: Create or Update Pull Request + if: steps.check-changes.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Check if PR already exists + PR_NUMBER=$(gh pr list \ + --base main \ + --head "${{ env.CONTRIBUTION_BRANCH }}" \ + --state open \ + --limit 1 \ + --json number \ + --jq ".[0].number" || echo "") + + if [ -z "$PR_NUMBER" ]; then + echo "Creating new PR..." + + # Create PR body + printf "This PR automatically updates CLAUDE.md and contribution guidelines based on RFC discussions.\n\n" > /tmp/pr-body.md + printf "Triggered by: Discussion #%s (%s)\n\n" "${{ github.event.discussion.number }}" "${{ github.event.action }}" >> /tmp/pr-body.md + printf "Changes:\n- Generated/updated CLAUDE.md\n- Updated RFC files in .contribution-guidelines/\n\n" >> /tmp/pr-body.md + printf "Review Notes:\n- Verify that the extracted AI Instructions are correct\n" >> /tmp/pr-body.md + printf -- "- Check that all RFC:Landed discussions are included\n" >> /tmp/pr-body.md + printf -- "- Ensure removed RFCs are properly cleaned up\n" >> /tmp/pr-body.md + + gh pr create \ + --base main \ + --head "${{ env.CONTRIBUTION_BRANCH }}" \ + --title "chore: update CLAUDE.md from RFC discussions" \ + --body-file /tmp/pr-body.md + else + echo "PR #$PR_NUMBER already exists and has been updated" + fi diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..8266689977 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,9 @@ +# Contribution Guidelines + +This file aggregates all RFC (Request for Comments) contribution guidelines that have landed. + + +## `setup` function instead of `beforeEach` in unit & service level tests + +See detailed guidelines: +@./.claude/instructions/setup-function-instead-of-beforeeach-in-unit-service-level-tests.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c8d1a99d0..6105023a62 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,4 +64,20 @@ If you're ready to contribute, follow our guidelines: Note that this process allows multiple developers to collaborate effectively and maintain high-quality code for a long-lasting entity; the project. +### AI Coding Guidelines + +This repository uses AI-assisted coding tools (GitHub Copilot, Cursor, Claude, etc.). To help these tools generate better code: + +- **CLAUDE.md**: Contains aggregated contribution guidelines extracted from approved RFCs +- **Contribution RFCs**: Guidelines are sourced from discussions in the [Contribution RFC category](https://github.com/akash-network/console/discussions/categories/contribution-rfc) +- **Auto-Generated**: The CLAUDE.md file is automatically generated when RFC discussions receive the `RFC:Landed` label + +To contribute new AI guidelines: +1. Create a discussion in the "Contribution RFC" category +2. Include a section titled `## AI Instructions` with guidance for AI coding assistants +3. Once approved, add the `RFC:Landed` label +4. The guidelines will be automatically added to CLAUDE.md + +See [`.contribution-guidelines/README.md`](./.contribution-guidelines/README.md) for more details. + *That your commitment to following these specs will make a difference.* \ No newline at end of file diff --git a/script/generate-claude-md.ts b/script/generate-claude-md.ts new file mode 100755 index 0000000000..6b38ef696e --- /dev/null +++ b/script/generate-claude-md.ts @@ -0,0 +1,235 @@ +#!/usr/bin/env node + +import { execSync } from "child_process"; +import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from "fs"; +import { join } from "path"; + +// Configuration +const REPO = process.env.GITHUB_REPOSITORY || "akash-network/console"; +const CATEGORY_NAME = "Contribution RFC"; +const LABEL_NAME = "RFC:Landed"; +const GUIDELINES_DIR = ".claude/instructions"; +const OUTPUT_FILE = "CLAUDE.md"; + +// Colors for output +const colors = { + red: "\x1b[0;31m", + green: "\x1b[0;32m", + yellow: "\x1b[1;33m", + reset: "\x1b[0m" +}; + +function log(level: "INFO" | "WARN" | "ERROR", message: string): void { + const color = level === "INFO" ? colors.green : level === "WARN" ? colors.yellow : colors.red; + console.log(`${color}[${level}]${colors.reset} ${message}`); +} + +// Convert RFC title to filename +function titleToFilename(title: string): string { + return title + .toLowerCase() + .replace(/[^a-z0-9 -]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); +} + +// Extract AI Instructions section from discussion body +function extractAIInstructions(body: string): string | null { + const lines = body.split("\n"); + let found = false; + const content: string[] = []; + + for (const line of lines) { + if (line.match(/^##\s*AI\s*Instructions?/)) { + found = true; + continue; + } + if (found && (/^/.test(line) || line.startsWith("## "))) { + break; + } + if (found) { + // reduce heading level by 1 + content.push(line.startsWith("#") ? line.slice(1) : line); + } + } + + const result = content.join("\n").trim(); + return result || null; +} + +// Execute gh CLI command +function ghCommand(args: string[]): string { + try { + return execSync(`gh ${args.join(" ")}`, { + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"] + }).trim(); + } catch (error: any) { + log("ERROR", `gh command failed: ${error.message}`); + throw error; + } +} + +interface Discussion { + title: string; + body: string; + labels: { nodes: Array<{ name: string }> }; +} + +interface DiscussionsResponse { + data: { + repository: { + discussionCategories: { + nodes: Array<{ id: string; name: string }>; + }; + discussions: { + nodes: Discussion[]; + }; + }; + }; +} + +function main(): void { + log("INFO", "Starting CLAUDE.md generation..."); + + // Check if gh CLI is available + try { + execSync("gh --version", { stdio: "ignore" }); + } catch { + log("ERROR", "gh CLI is not installed. Please install it first."); + process.exit(1); + } + + // Create guidelines directory if it doesn't exist + if (!existsSync(GUIDELINES_DIR)) { + mkdirSync(GUIDELINES_DIR, { recursive: true }); + } + + const [owner, repo] = REPO.split("/"); + + // Get the category ID for "Contribution RFC" + log("INFO", `Fetching category ID for '${CATEGORY_NAME}'...`); + + const categoryQuery = gql` + query ($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + discussionCategories(first: 100) { + nodes { + id + name + } + } + } + } + `; + + const categoryResult: DiscussionsResponse = JSON.parse( + ghCommand(["api", "graphql", "-f", `query='${categoryQuery}'`, "-F", `owner=${owner}`, "-F", `repo=${repo}`]) + ); + + const category = categoryResult.data.repository.discussionCategories.nodes.find(c => c.name === CATEGORY_NAME); + + if (!category) { + log("ERROR", `Could not find category '${CATEGORY_NAME}'`); + process.exit(1); + } + + log("INFO", `Found category ID: ${category.id}`); + + // Fetch all discussions with RFC:Landed label + log("INFO", `Fetching discussions with label '${LABEL_NAME}'...`); + + const discussionsQuery = gql` + query ($owner: String!, $repo: String!, $categoryId: ID!) { + repository(owner: $owner, name: $repo) { + discussions(first: 100, categoryId: $categoryId) { + nodes { + title + body + labels(first: 10) { + nodes { + name + } + } + } + } + } + } + `; + + const discussionsResult: DiscussionsResponse = JSON.parse( + ghCommand(["api", "graphql", "-f", `query='${discussionsQuery}'`, "-F", `owner=${owner}`, "-F", `repo=${repo}`, "-F", `categoryId=${category.id}`]) + ); + + const discussions = discussionsResult.data.repository.discussions.nodes || []; + + // Track existing files to clean up removed RFCs + const existingFiles = existsSync(GUIDELINES_DIR) + ? readdirSync(GUIDELINES_DIR) + .filter(f => f.endsWith(".md") && f !== "README.md") + .map(f => join(GUIDELINES_DIR, f)) + : []; + + // Process each discussion + log("INFO", "Processing discussions..."); + const processedFiles: string[] = []; + let claudeContent = "# Contribution Guidelines\n\nThis file aggregates all RFC (Request for Comments) contribution guidelines that have landed.\n\n"; + let rfcCount = 0; + + for (const discussion of discussions) { + const hasLabel = discussion.labels.nodes.some(l => l.name === LABEL_NAME); + + if (hasLabel) { + log("INFO", `Processing RFC: ${discussion.title}`); + + // Extract AI Instructions section + const aiInstructions = extractAIInstructions(discussion.body); + + if (!aiInstructions) { + log("WARN", `No '## AI Instructions' section found in '${discussion.title}'. Skipping...`); + continue; + } + + // Generate filename + const filename = `${titleToFilename(discussion.title)}.md`; + const filepath = join(GUIDELINES_DIR, filename); + processedFiles.push(filepath); + + writeAiInstruction(filepath, aiInstructions); + + log("INFO", `Created/Updated: ${filepath}`); + + // Add to CLAUDE.md content + claudeContent += `\n## ${discussion.title}\n\nSee detailed guidelines:\n@./${GUIDELINES_DIR}/${filename}\n`; + rfcCount++; + } + } + + // Remove files for RFCs that no longer have RFC:Landed label + for (const existingFile of existingFiles) { + if (!processedFiles.includes(existingFile)) { + log("INFO", `Removing obsolete file: ${existingFile}`); + unlinkSync(existingFile); + } + } + + // Generate root CLAUDE.md + log("INFO", `Generating root ${OUTPUT_FILE}...`); + writeFileSync(OUTPUT_FILE, claudeContent, "utf-8"); + + log("INFO", `Successfully generated ${OUTPUT_FILE} with ${rfcCount} RFC(s)`); +} + +function gql(strings: TemplateStringsArray, ...values: string[]): string { + return strings + .reduce((acc, str, i) => acc + str + (values[i] || ""), "") + .replace(/\s+/g, " ") + .trim(); +} + +function writeAiInstruction(filepath: string, aiInstructions: string): void { + writeFileSync(filepath, aiInstructions, "utf-8"); +} + +main(); From ff330585e9e33afc199bf870780bdf436a14a5cd Mon Sep 17 00:00:00 2001 From: Serhii Stotskyi Date: Tue, 13 Jan 2026 07:57:06 +0100 Subject: [PATCH 2/2] Apply suggestion from @stalniy Signed-off-by: Serhii Stotskyi --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6105023a62..03790b6ce7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,6 +78,6 @@ To contribute new AI guidelines: 3. Once approved, add the `RFC:Landed` label 4. The guidelines will be automatically added to CLAUDE.md -See [`.contribution-guidelines/README.md`](./.contribution-guidelines/README.md) for more details. +See [`.claude/README.md`](./.claude/README.md) for more details. *That your commitment to following these specs will make a difference.* \ No newline at end of file