From 1bf8d0d01aa4baaab66d1e875f04013fcfe7a0a6 Mon Sep 17 00:00:00 2001 From: Tobias Wilken Date: Tue, 21 Oct 2025 19:48:49 +0200 Subject: [PATCH] fix: prevent parsing repository examples in code blocks The parser was incorrectly treating repository definitions inside markdown code blocks as real repositories, causing the sync script to create repositories from documentation examples. Changes: - Add code block tracking to parse-repositories.js - Skip all content between triple backticks - Add comprehensive unit test suite with 13 tests - Add test workflow for CI that runs on all repos including forks - Restrict drift-detection and sync workflows to worlddriven org only - Add documentation and webapp repositories to REPOSITORIES.md This ensures forks can run CI successfully without access to org secrets, while the sync automation only runs in the main organization. --- .github/workflows/drift-detection.yml | 17 +- .github/workflows/sync-repositories.yml | 2 + .github/workflows/test.yml | 34 ++++ REPOSITORIES.md | 8 + package.json | 1 + scripts/parse-repositories.js | 12 ++ scripts/parse-repositories.test.js | 224 ++++++++++++++++++++++++ 7 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 scripts/parse-repositories.test.js diff --git a/.github/workflows/drift-detection.yml b/.github/workflows/drift-detection.yml index 6d28ff2..62ab60a 100644 --- a/.github/workflows/drift-detection.yml +++ b/.github/workflows/drift-detection.yml @@ -29,7 +29,7 @@ jobs: - name: Run drift detection id: drift env: - WORLDDRIVEN_GITHUB_TOKEN: ${{ secrets.WORLDDRIVEN_GITHUB_TOKEN }} + WORLDDRIVEN_GITHUB_TOKEN: ${{ secrets.WORLDDRIVEN_GITHUB_TOKEN || github.token }} run: | set +e node scripts/detect-drift.js > drift-report.md 2>&1 @@ -41,7 +41,7 @@ jobs: - name: Preview sync actions (dry-run) id: sync env: - WORLDDRIVEN_GITHUB_TOKEN: ${{ secrets.WORLDDRIVEN_GITHUB_TOKEN }} + WORLDDRIVEN_GITHUB_TOKEN: ${{ secrets.WORLDDRIVEN_GITHUB_TOKEN || github.token }} run: | set +e node scripts/sync-repositories.js > sync-preview.md 2>&1 @@ -50,7 +50,20 @@ jobs: set -e continue-on-error: true + - name: Add drift report to workflow summary + if: always() + run: | + echo "## Repository Drift Detection Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + cat drift-report.md >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + cat sync-preview.md >> $GITHUB_STEP_SUMMARY + - name: Comment PR with drift report and sync preview + # Skip commenting on fork PRs (no write permissions), but drift report is available in workflow summary above + if: github.event.pull_request.head.repo.full_name == github.repository uses: actions/github-script@v7 with: script: | diff --git a/.github/workflows/sync-repositories.yml b/.github/workflows/sync-repositories.yml index 6396084..9cfeb47 100644 --- a/.github/workflows/sync-repositories.yml +++ b/.github/workflows/sync-repositories.yml @@ -8,6 +8,8 @@ on: jobs: sync: runs-on: ubuntu-latest + # Only run on the main worlddriven organization, not on forks + if: github.repository_owner == 'worlddriven' permissions: contents: write issues: write diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3d9749c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: Tests + +on: + pull_request: + paths: + - 'scripts/**' + - 'package.json' + - '.github/workflows/test.yml' + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Make scripts executable + run: chmod +x scripts/*.js + + - name: Run unit tests + run: npm test + + - name: Report test results + if: success() + run: echo "✅ All tests passed" diff --git a/REPOSITORIES.md b/REPOSITORIES.md index 05a2edb..c378639 100644 --- a/REPOSITORIES.md +++ b/REPOSITORIES.md @@ -35,3 +35,11 @@ Each repository is defined using markdown headers and properties: ## Current Repositories + +## documentation +- Description: Core documentation repository for worlddriven project +- Topics: documentation, worlddriven + +## webapp +- Description: Web application interface for worlddriven +- Topics: webapp, web, frontend, worlddriven diff --git a/package.json b/package.json index 9227e49..5beb52d 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Vision, philosophy, and organization management for worlddriven", "type": "module", "scripts": { + "test": "node --test scripts/*.test.js", "parse": "node scripts/parse-repositories.js", "fetch-github": "node scripts/fetch-github-state.js", "detect-drift": "node scripts/detect-drift.js", diff --git a/scripts/parse-repositories.js b/scripts/parse-repositories.js index 9d3e42b..cf1f242 100755 --- a/scripts/parse-repositories.js +++ b/scripts/parse-repositories.js @@ -22,10 +22,22 @@ function parseRepositories(content) { const lines = content.split('\n'); let currentRepo = null; + let inCodeBlock = false; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); + // Track code block boundaries + if (line.startsWith('```')) { + inCodeBlock = !inCodeBlock; + continue; + } + + // Skip lines inside code blocks + if (inCodeBlock) { + continue; + } + // Repository name (## heading) if (line.startsWith('## ')) { // Save previous repo if exists diff --git a/scripts/parse-repositories.test.js b/scripts/parse-repositories.test.js new file mode 100644 index 0000000..c366d60 --- /dev/null +++ b/scripts/parse-repositories.test.js @@ -0,0 +1,224 @@ +#!/usr/bin/env node + +import { describe, test } from 'node:test'; +import assert from 'node:assert'; +import { parseRepositories } from './parse-repositories.js'; + +describe('parseRepositories', () => { + test('should return empty array for empty content', () => { + const result = parseRepositories(''); + assert.deepStrictEqual(result, []); + }); + + test('should parse a single repository with description', () => { + const content = ` +## my-repo +- Description: A test repository +`; + const result = parseRepositories(content); + assert.deepStrictEqual(result, [ + { + name: 'my-repo', + description: 'A test repository' + } + ]); + }); + + test('should parse repository with description and topics', () => { + const content = ` +## my-repo +- Description: A test repository +- Topics: topic1, topic2, topic3 +`; + const result = parseRepositories(content); + assert.deepStrictEqual(result, [ + { + name: 'my-repo', + description: 'A test repository', + topics: ['topic1', 'topic2', 'topic3'] + } + ]); + }); + + test('should parse multiple repositories', () => { + const content = ` +## repo-one +- Description: First repository +- Topics: topic1 + +## repo-two +- Description: Second repository +- Topics: topic2, topic3 +`; + const result = parseRepositories(content); + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].name, 'repo-one'); + assert.strictEqual(result[1].name, 'repo-two'); + }); + + test('should skip repositories inside code blocks', () => { + const content = ` +## Format + +Example: + +\`\`\`markdown +## example-repo +- Description: This is inside a code block +- Topics: example, test +\`\`\` + +## Current Repositories + +## real-repo +- Description: This is a real repository +`; + const result = parseRepositories(content); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'real-repo'); + assert.strictEqual(result[0].description, 'This is a real repository'); + }); + + test('should skip repositories without descriptions', () => { + const content = ` +## valid-repo +- Description: Valid repository + +## invalid-repo +- Topics: topic1, topic2 + +## another-valid +- Description: Another valid one +`; + const result = parseRepositories(content); + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].name, 'valid-repo'); + assert.strictEqual(result[1].name, 'another-valid'); + }); + + test('should skip heading with "example" in name', () => { + const content = ` +## Example +- Description: This should be skipped + +## real-repo +- Description: This is real +`; + const result = parseRepositories(content); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'real-repo'); + }); + + test('should skip heading with "format" in name', () => { + const content = ` +## Format +- Description: This should be skipped + +## real-repo +- Description: This is real +`; + const result = parseRepositories(content); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'real-repo'); + }); + + test('should skip heading with "current repositories" in name', () => { + const content = ` +## Current Repositories +- Description: This should be skipped + +## real-repo +- Description: This is real +`; + const result = parseRepositories(content); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'real-repo'); + }); + + test('should handle nested code blocks correctly', () => { + const content = ` +## Documentation + +Here's an example: + +\`\`\`markdown +## worlddriven-core +- Description: Democratic governance system for GitHub pull requests +- Topics: democracy, open-source, governance, automation + +## worlddriven-documentation +- Description: Vision, philosophy, and organization management for worlddriven +- Topics: documentation, organization-management, governance +\`\`\` + +--- + +## Current Repositories + +## actual-repo +- Description: This is the only real repository +- Topics: real, test +`; + const result = parseRepositories(content); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'actual-repo'); + assert.strictEqual(result[0].description, 'This is the only real repository'); + assert.deepStrictEqual(result[0].topics, ['real', 'test']); + }); + + test('should handle topics with extra whitespace', () => { + const content = ` +## my-repo +- Description: Test repository +- Topics: topic1 , topic2 , topic3 +`; + const result = parseRepositories(content); + assert.deepStrictEqual(result[0].topics, ['topic1', 'topic2', 'topic3']); + }); + + test('should handle repositories without topics', () => { + const content = ` +## my-repo +- Description: Test repository without topics +`; + const result = parseRepositories(content); + assert.strictEqual(result[0].topics, undefined); + }); + + test('should match actual REPOSITORIES.md structure', () => { + const content = `# Worlddriven Organization Repositories + +This file serves as the source of truth for all repositories in the worlddriven GitHub organization. + +## Format + +Each repository is defined using markdown headers and properties: + +\`\`\`markdown +## repository-name +- Description: Brief description of the repository +- Topics: topic1, topic2, topic3 +\`\`\` + +## Example + +\`\`\`markdown +## worlddriven-core +- Description: Democratic governance system for GitHub pull requests +- Topics: democracy, open-source, governance, automation + +## worlddriven-documentation +- Description: Vision, philosophy, and organization management for worlddriven +- Topics: documentation, organization-management, governance +\`\`\` + +--- + +## Current Repositories + + +`; + const result = parseRepositories(content); + assert.deepStrictEqual(result, [], 'Should return empty array when no actual repositories are defined'); + }); +});