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'); + }); +});