diff --git a/.github/agents/project-meta-sync.agent.md b/.github/agents/project-meta-sync.agent.md index 4faafbb58..280f90ff7 100644 --- a/.github/agents/project-meta-sync.agent.md +++ b/.github/agents/project-meta-sync.agent.md @@ -1,52 +1,107 @@ --- title: "Project Meta Sync Agent Spec" -version: "v1.0" -last_updated: "2025-10-21" +version: "v1.1" +last_updated: "2025-11-13" author: "LightSpeed" maintainer: "Ash Shaw" -description: "Spec for the Project Meta Sync Agent." -tags: ["lightspeed","project","meta","agents"] +description: "Spec for the Project Meta Sync Agent that maps labels and branch conventions to GitHub ProjectV2 fields." +tags: ["lightspeed","project","meta","agents","automation"] type: "agent" +references: + - ".github/automation/project-fields.yml" + - ".github/automation/project-labeler.yml" + - ".github/automation/labels.yml" + - "schemas/automation/project-fields.schema.json" + - ".github/workflows/project-meta-sync.yml" + - ".github/agents/project-meta-sync.js" --- # Role -Sync project board meta fields (Status, Priority, Type) from labels and branch names. +Sync GitHub ProjectV2 fields (Status, Priority, Type, Area, etc.) from labels and branch name conventions. # Purpose -- Keep GitHub Projects and issues/PRs in sync. -- Automate project field updates based on repo activity. +- Keep GitHub Projects V2 and issues/PRs in sync automatically. +- Automate project field updates based on repository activity. +- Provide a single source of truth for project field mappings via `.github/automation/project-fields.yml`. # Type of Task -- Add new items to project on issue/PR events. -- Map labels/branches to project fields. +- Add new items to project board on issue/PR events. +- Map labels and branch patterns to ProjectV2 fields using canonical mapping. +- Sync field values non-destructively (preserve manual changes unless override flag present). # Process -- Trigger on issue/PR open/edit/label. -- Use mapping rules to set Status, Priority, Type. -- Update project fields via API. +1. Trigger on issue/PR open/edit/label/unlabel/close events. +2. Load canonical field mappings from `.github/automation/project-fields.yml`. +3. Derive field values using: + - Label-based mapping (e.g., `status:in-progress` → Status: "In progress") + - Branch-based mapping (e.g., `feat/*` → Type: "Feature") + - Event-based mapping (e.g., closed + merged → Status: "Done") +4. Update ProjectV2 fields via GraphQL API. +5. Log all changes for audit trail. # Constraints -- Must not overwrite manual changes without warning. -- Support per-project mapping config. +- Must not overwrite manual field changes without explicit override (via `meta:auto-sync` label). +- Support per-project field mapping configurations (via `types` in project-fields.yml). +- Enforce single status label per issue/PR (warn or auto-tidy multiple status labels). +- Validate all mappings against JSON schema (`schemas/automation/project-fields.schema.json`). # What to do -- Ensure project fields are always up to date with labels. +- Ensure ProjectV2 fields are always synchronized with canonical labels and branch conventions. +- Use `.github/automation/project-fields.yml` as the single source of truth for field definitions. +- Reference `.github/automation/project-labeler.yml` for label-to-field mapping rules. +- Log all field updates with timestamps and triggering events. -# What not do -- Do not remove items from project without confirmation. +# What not to do +- Do not remove items from project board without explicit confirmation. +- Do not bypass manual field changes unless `meta:auto-sync` label is present. +- Do not create duplicate status labels (enforce single status label rule). # Best Practices -- Log all changes. -- Allow per-repo/project config. +- Log all changes with event context (issue/PR number, triggering action, old/new values). +- Allow per-repository and per-project configuration overrides. +- Validate project-fields.yml against schema before applying changes. +- Use GraphQL batching for efficient field updates. +- Provide clear error messages and warnings for mapping conflicts. # Guardrails -- Notify maintainers on mapping conflicts. -- Provide rollback/audit if possible. +- Notify maintainers on mapping conflicts or schema validation failures. +- Provide rollback capability via audit log. +- Rate-limit API calls to prevent quota exhaustion. +- Skip updates if field values haven't changed (idempotent operations). # Checklist -- [ ] Items added to project. -- [ ] Meta fields synced. +- [ ] Items added to project board for new issues/PRs. +- [ ] Status field synced from labels and event state. +- [ ] Priority field synced from labels and branch patterns. +- [ ] Type field synced from branch prefix or labels. +- [ ] Area field synced from file changes (if configured). +- [ ] Manual changes preserved (unless override present). +- [ ] Single status label enforced. +- [ ] All changes logged for audit. # Outputs -- Project board updates. -- Sync logs. \ No newline at end of file +- ProjectV2 field updates via GraphQL API. +- Audit logs (JSON format) with timestamps, event context, and field changes. +- Warnings for mapping conflicts or validation errors. + +# Configuration Files +- **`.github/automation/project-fields.yml`**: Canonical project field definitions (source of truth) +- **`.github/automation/project-labeler.yml`**: Label mapping rules (harmonised with labels.yml) +- **`.github/automation/labels.yml`**: Canonical label definitions +- **`schemas/automation/project-fields.schema.json`**: JSON Schema for field validation +- **`.github/workflows/project-meta-sync.yml`**: Workflow trigger configuration +- **`.github/agents/project-meta-sync.js`**: Agent implementation (Node.js) + +# Field Mapping Examples +| Label/Branch | Field | Value | +|--------------|-------|-------| +| `status:in-progress` | Status | "In progress" | +| `status:needs-review` | Status | "In review" | +| `priority:critical` | Priority | "Critical" | +| `feat/*` branch | Type | "Feature" | +| `fix/*` branch | Type | "Bug" | +| `hotfix/*` branch | Priority | "Critical" | +| closed + merged | Status | "Done" | + +# Override Mechanism +Add the `meta:auto-sync` label to allow the agent to overwrite manual field changes. Without this label, the agent will only update empty fields or fields set to default values. \ No newline at end of file diff --git a/.github/agents/project-meta-sync.js b/.github/agents/project-meta-sync.js new file mode 100755 index 000000000..969b38a2f --- /dev/null +++ b/.github/agents/project-meta-sync.js @@ -0,0 +1,176 @@ +#!/usr/bin/env node +/** + * Project Meta Sync Agent + * + * Maps labels/branch conventions to ProjectV2 fields (Status, Priority, Type) + * and updates the corresponding item. Non-destructive by default. + * + * @author LightSpeed + * @requires @octokit/graphql, js-yaml, fs + * @see .github/agents/project-meta-sync.agent.md + */ +const fs = require('fs'); +const path = require('path'); +const { graphql } = require('@octokit/graphql'); +const yaml = require('js-yaml'); + +// Parse CLI args +const args = process.argv.slice(2); +const eventName = args.find(a => a.startsWith('--event'))?.split('=')[1]; +const payloadPath = args.find(a => a.startsWith('--payload'))?.split('=')[1]; + +if ( + !eventName || typeof eventName !== 'string' || eventName.trim() === '' || + !payloadPath || typeof payloadPath !== 'string' || payloadPath.trim() === '' +) { + console.error('Usage: project-meta-sync.js --event= --payload='); + process.exit(1); +} + +// Load event payload +let event; +try { + event = JSON.parse(fs.readFileSync(payloadPath, 'utf8')); +} catch (e) { + console.error('Failed to parse event payload:', e.message); + process.exit(1); +} + +// Environment +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; + +if (!GITHUB_TOKEN) { + console.error('❌ GITHUB_TOKEN not set'); + process.exit(1); +} + +// GraphQL client + +/** + * Load canonical project fields mapping (if present) + * TODO: Implement loading of normalized mapping dictionary + */ +function loadFieldsMapping() { + const fieldsPath = path.resolve('.github/automation/project-fields.yml'); + if (fs.existsSync(fieldsPath)) { + return yaml.load(fs.readFileSync(fieldsPath, 'utf8')); + } + return null; +} + +/** + * Derive Status from labels and event + */ +function deriveStatus(labels, eventName, eventAction, isMerged = false) { + const labelNames = labels.map(l => l.name || l); + + // Check status labels + if (labelNames.some(l => l === 'status:in-progress')) return 'In progress'; + if (labelNames.some(l => l === 'status:needs-review')) return 'In review'; + if (labelNames.some(l => l === 'status:needs-qa')) return 'In QA'; + if (labelNames.some(l => l === 'status:blocked')) return 'Blocked'; + if (labelNames.some(l => l === 'status:ready')) return 'Ready'; + + // Check closed/merged events + if (eventAction === 'closed') { + if (eventName === 'issues') return 'Done'; + if (eventName === 'pull_request' && isMerged) return 'Done'; + } + + // Default to Triage + return 'Triage'; +} + +/** + * Derive Priority from labels + */ +function derivePriority(labels) { + const labelNames = labels.map(l => l.name || l); + + if (labelNames.some(l => l === 'priority:critical')) return 'Critical'; + if (labelNames.some(l => l === 'priority:important')) return 'Important'; + if (labelNames.some(l => l === 'priority:normal')) return 'Normal'; + if (labelNames.some(l => l === 'priority:minor')) return 'Minor'; + + return null; // No priority set +} + +/** + * Derive Type from branch name or labels + */ +function deriveType(branchName, labels) { + // Try branch conventions first + if (branchName) { + if (branchName.startsWith('feat/')) return 'Feature'; + if (branchName.startsWith('fix/')) return 'Bug'; + if (branchName.startsWith('doc/') || branchName.startsWith('docs/')) return 'Documentation'; + if (branchName.startsWith('chore/') || branchName.startsWith('build/')) return 'Task'; + } + + // Try labels + const labelNames = labels.map(l => l.name || l); + if (labelNames.some(l => l === 'type:feature')) return 'Feature'; + if (labelNames.some(l => l === 'type:bug')) return 'Bug'; + if (labelNames.some(l => l === 'type:documentation')) return 'Documentation'; + if (labelNames.some(l => l === 'type:task')) return 'Task'; + + return null; // No type set +} + +/** + * Main sync logic + */ +async function sync() { + console.log(`project-meta-sync: handling ${eventName} event`); + + // Extract item details from event + const item = event.issue || event.pull_request; + if (!item) { + console.log('No issue or PR in event payload'); + return; + } + + const labels = item.labels || []; + const branchName = event.pull_request?.head?.ref || null; + const eventAction = event.action; + const isMerged = event.pull_request?.merged || false; + + // Derive field values + const status = deriveStatus(labels, eventName, eventAction, isMerged); + const priority = derivePriority(labels); + const type = deriveType(branchName, labels); + + console.log(`Derived fields:`, { status, priority, type }); + + // TODO: Enforce single status:* label (warn or auto-tidy) + const statusLabels = labels.filter(l => (l.name || l).startsWith('status:')); + if (statusLabels.length > 1) { + console.warn(`[WARN] Multiple status labels found: ${statusLabels.map(l => l.name || l).join(', ')}`); + console.warn('Consider using label-sync to enforce single status label'); + } + + // TODO: Use GraphQL to upsert ProjectV2 item & fields + // For now, just log what would be updated + console.log(`Would update ProjectV2 item for ${item.html_url}:`); + console.log(` Status: ${status}`); + if (priority) console.log(` Priority: ${priority}`); + if (type) console.log(` Type: ${type}`); + + // TODO: Guard against overwriting manual fields unless override label present + const hasOverride = labels.some(l => (l.name || l) === 'meta:auto-sync'); + if (!hasOverride) { + console.log('ℹ️ No override label (meta:auto-sync) - would check for manual changes'); + } + + // TODO: Load normalized mapping dictionary if present + const fieldsMapping = loadFieldsMapping(); + if (fieldsMapping) { + console.log('✅ Loaded project fields mapping'); + } +} + +// Run +sync().catch(err => { + console.error('❌ Error:', err.message); + process.exit(1); +}); diff --git a/.github/automation/project-fields.yml b/.github/automation/project-fields.yml new file mode 100644 index 000000000..55f30bf10 --- /dev/null +++ b/.github/automation/project-fields.yml @@ -0,0 +1,228 @@ +schema: 1 +types: + product-development: + project_name: product-development + fields: + - key: Theme + slug: theme + type: single_select + options: + - Design System + - Content Management + - Commerce (WooCommerce) + - Editorial UX (Authoring) + - Performance + - Accessibility (A11y) + - Security & Privacy + - Integrations & APIs + - Internationalisation (i18n) + - Analytics & Measurement + - SEO + - Release & Deployment + - key: Area + slug: area + type: single_select + options: + - Frontend + - Backend + - Build & CI + - Deployment/DevOps + - Design System + - Analytics + - A11y + - key: Priority + slug: priority + type: single_select + options: + - High + - Medium + - Low + - key: Severity + slug: severity + type: single_select + options: + - S0  Blocker + - S1  Critical + - S2  Major + - S3  Minor + - S4  Trivial + - key: Size + slug: size + type: single_select + options: + - 0  Unknown + - 1  XS + - 2  S + - 3  M + - 4  L + - 5  XL + - 6  XXL + - key: Release type + slug: release-type + type: single_select + options: + - Major + - Minor + - Patch + - Hotfix + - key: Story Points + slug: story-points + type: number + - key: Due Date + slug: due-date + type: date + - key: Assignee + slug: assignee + type: text + client-delivery: + project_name: client-delivery + fields: + - key: Theme + slug: theme + type: single_select + options: + - Design System + - Content Management + - Commerce (WooCommerce) + - Editorial UX (Authoring) + - Performance + - Accessibility (A11y) + - Security & Privacy + - Integrations & APIs + - Internationalisation (i18n) + - Analytics & Measurement + - SEO + - Release & Deployment + - key: Area + slug: area + type: single_select + options: + - Frontend + - Backend + - Build & CI + - Deployment/DevOps + - Design System + - Content + - Analytics + - A11y + - key: Priority + slug: priority + type: single_select + options: + - High + - Medium + - Low + - key: Severity + slug: severity + type: single_select + options: + - S0  Blocker + - S1  Critical + - S2  Major + - S3  Minor + - S4  Trivial + - key: Size + slug: size + type: single_select + options: + - 0  Unknown + - 1  XS + - 2  S + - 3  M + - 4  L + - 5  XL + - 6  XXL + - key: Phase + slug: phase + type: single_select + options: + - Pre-launch + - Post-launch + - key: Story Points + slug: story-points + type: number + - key: Due Date + slug: due-date + type: date + - key: Assignee + slug: assignee + type: text + project-dev: + project_name: project-dev + fields: + - key: Theme + slug: theme + type: single_select + options: + - Design System + - Content Management + - Commerce (WooCommerce) + - Editorial UX (Authoring) + - Performance + - Accessibility (A11y) + - Security & Privacy + - Integrations & APIs + - Internationalisation (i18n) + - Analytics & Measurement + - SEO + - Release & Deployment + - key: Area + slug: area + type: single_select + options: + - Frontend + - Backend + - Build & CI + - Deployment/DevOps + - Design System + - Analytics + - A11y + - key: Priority + slug: priority + type: single_select + options: + - High + - Medium + - Low + - key: Severity + slug: severity + type: single_select + options: + - S0  Blocker + - S1  Critical + - S2  Major + - S3  Minor + - S4  Trivial + - key: Size + slug: size + type: single_select + options: + - 0  Unknown + - 1  XS + - 2  S + - 3  M + - 4  L + - 5  XL + - 6  XXL + - key: Release type + slug: release-type + type: single_select + options: + - Minor + - Patch + - Hotfix + - key: Milestone + slug: milestone + type: text + - key: Estimate + slug: estimate + type: number + - key: Start Date + slug: start-date + type: date + - key: Deadline + slug: deadline + type: date + - key: Assignee + slug: assignee + type: text diff --git a/.github/automation/project-labeler.yml b/.github/automation/project-labeler.yml new file mode 100644 index 000000000..5cdf856ca --- /dev/null +++ b/.github/automation/project-labeler.yml @@ -0,0 +1,249 @@ +# Canonical Project Labeler (Issues + PRs) +# Maps branch patterns, file changes, and content patterns to canonical labels +# Harmonised with labels.yml and replaces project-pr-labeler.yml +# All labels reference the canonical definitions in labels.yml + +# Status labels (map to Status field in ProjectV2) +"status:needs-review": + - head-branch: ['^feat/.*', '^fix/.*', '^docs?/.*', '^(chore|build|refactor|test|perf|ci|release|hotfix|design|a11y|qa|content|seo|config|migrate|uat|proto|ds|api|schema|telemetry)/.*'] + +# Priority labels (map to Priority field in ProjectV2) +"priority:critical": + - head-branch: ['^hotfix/.*'] +"priority:normal": + - head-branch: ['^feat/.*', '^fix/.*', '^docs/.*', '^chore/.*'] + +# Type labels (map to Type field in ProjectV2) +"type:feature": + - head-branch: ['^feat/.*'] +"type:bug": + - head-branch: ['^fix/.*', '^hotfix/.*'] +"type:documentation": + - head-branch: ['^docs/.*'] +"type:chore": + - head-branch: ['^chore/.*', '^build/.*', '^ci/.*'] +"type:refactor": + - head-branch: ['^refactor/.*'] +"type:test": + - head-branch: ['^test/.*'] +"type:performance": + - head-branch: ['^perf/.*'] +"type:design": + - head-branch: ['^design/.*'] +"type:a11y": + - head-branch: ['^a11y/.*'] +"type:qa": + - head-branch: ['^qa/.*', '^uat/.*'] +"type:security": + - head-branch: ['^security/.*'] + - changed-files: + any-glob-to-any-file: ['security/**/*'] +"type:release": + - head-branch: ['^release/.*'] +"type:content-modelling": + - head-branch: ['^content/.*', '^schema/.*'] +"type:ai-ops": + - head-branch: ['^telemetry/.*', '^analytics/.*'] +"type:integration": + - head-branch: ['^api/.*', '^integration/.*'] +"type:research": + - head-branch: ['^research/.*', '^proto/.*'] + +# Area labels (map to Area field in ProjectV2) +"area:block-editor": + - changed-files: + any-glob-to-any-file: ['src/blocks/**', 'src/block-editor/**', '**/block.json', 'comp:block-editor/**'] +"area:theme": + - changed-files: + any-glob-to-any-file: ['**/theme.json', '**/templates/**', '**/patterns/**', '**/parts/**', 'theme/**/*', 'style.css', 'functions.php', 'assets/**/*', 'design-system/**/*'] +"area:ci": + - changed-files: + any-glob-to-any-file: ['.github/workflows/**', '.github/actions/**', 'ci/**/*'] +"area:documentation": + - changed-files: + any-glob-to-any-file: ['**/*.md', '**/*.txt', 'docs/**', 'README.md', '**/README.md', 'CHANGELOG.md', '**/CHANGELOG.md'] +"area:tests": + - changed-files: + any-glob-to-any-file: ['tests/**/*', '**/*.test.*', '**/*.spec.*', '**/__tests__/**', 'playwright.config.js', '.github/linters/phpunit.xml'] +"area:scripts": + - changed-files: + any-glob-to-any-file: ['scripts/**/*', '**/*.sh', '**/*.bash', 'utility/**/*'] +"area:assets": + - changed-files: + any-glob-to-any-file: ['assets/**/*', 'images/**/*', '**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif', '**/*.svg', '**/*.ico', '**/*.webp'] +"area:woocommerce": + - changed-files: + any-glob-to-any-file: ['woocommerce/**/*', '**/woocommerce*.css', '**/woocommerce*.php', '**/woocommerce*.js'] +"area:core": + - changed-files: + any-glob-to-any-file: ['src/core/**', 'core/**/*'] + +# Language labels +"lang:php": + - changed-files: + any-glob-to-any-file: ['**/*.php', 'composer.json', 'composer.lock', 'phpunit.xml'] +"lang:javascript": + - changed-files: + any-glob-to-any-file: ['**/*.{js,jsx,ts,tsx}', 'package.json', 'package-lock.json', 'tsconfig.json'] +"lang:css": + - changed-files: + any-glob-to-any-file: ['**/*.{css,scss,sass,less}'] + +# Release type labels (map to Release Type field in ProjectV2) +"release:major": + - head-branch: ['^release/major/.*', '^release/v?[0-9]+\\.0\\.0.*'] +"release:minor": + - head-branch: ['^release/minor/.*', '^release/v?[0-9]+\\.[0-9]+\\.0.*'] +"release:patch": + - head-branch: ['^release/patch/.*', '^patch/.*', '^release/v?[0-9]+\\.[0-9]+\\.[1-9].*'] + +# Meta labels +"meta:needs-changelog": + - changed-files: + all-globs-to-any-file: ['!CHANGELOG.md', '!**/CHANGELOG.md', '**/*.{php,js,jsx,ts,tsx,css,scss}'] +"meta:changelog": + - changed-files: + any-glob-to-any-file: ['CHANGELOG.md', '**/CHANGELOG.md'] + +# Contributor labels +"contrib:good-first-issue": + - changed-files: + any-glob-to-any-file: [] +"contrib:help-wanted": + - changed-files: + any-glob-to-any-file: [] + +# Additional area/component labels from project-pr-labeler +"area:analytics": + - changed-files: + any-glob-to-any-file: ['analytics/**/*'] +"area:forms": + - changed-files: + any-glob-to-any-file: ['forms/**/*'] +"area:gallery": + - changed-files: + any-glob-to-any-file: ['gallery/**/*'] +"area:emails": + - changed-files: + any-glob-to-any-file: ['emails/**/*'] +"area:navigation": + - changed-files: + any-glob-to-any-file: ['navigation/**/*'] +"area:configuration": + - changed-files: + any-glob-to-any-file: ['.github/**/*.yml', '.github/**/*.yaml', '.github/linters/**/*', '.stylelintrc.json', '.eslintrc.json', 'phpcs.xml', 'phpunit.xml', 'webpack.config.js', '*.json', '*.yml', '*.yaml', '.editorconfig', '.gitignore'] +"area:monitoring": + - changed-files: + any-glob-to-any-file: ['monitoring/**/*'] + +# Pattern, Template, Block labels +"pattern": + - changed-files: + any-glob-to-any-file: ['patterns/**/*'] +"template": + - changed-files: + any-glob-to-any-file: ['templates/**/*', 'parts/**/*'] +"template-part": + - changed-files: + any-glob-to-any-file: ['template-parts/**/*'] +"block": + - changed-files: + any-glob-to-any-file: ['src/blocks/**/*', '**/block.json'] + +# Theme tokens +"theme:tokens": + - changed-files: + any-glob-to-any-file: ['theme.json', 'tokens/*.json', 'design-system/tokens/**/*'] + +# Build +"build": + - changed-files: + any-glob-to-any-file: ['build/**/*'] + +# Device, Environment, Phase, Page labels +"device:mobile": + - changed-files: + any-glob-to-any-file: ['**/*mobile*.*'] +"device:desktop": + - changed-files: + any-glob-to-any-file: ['**/*desktop*.*'] +"device:tablet-landscape": + - changed-files: + any-glob-to-any-file: ['**/*tablet-landscape*.*'] +"device:tablet-portrait": + - changed-files: + any-glob-to-any-file: ['**/*tablet-portrait*.*'] +"layout:grid": + - changed-files: + any-glob-to-any-file: ['**/grid*.*'] +"env:staging": + - changed-files: + any-glob-to-any-file: ['**/*staging*.*'] +"env:live": + - changed-files: + any-glob-to-any-file: ['**/*live*.*'] +"phase:post-launch": + - changed-files: + any-glob-to-any-file: ['**/*post-launch*.*'] +"phase:pre-launch": + - changed-files: + any-glob-to-any-file: ['**/*pre-launch*.*'] +"page:home": + - changed-files: + any-glob-to-any-file: ['**/*home*.*'] +"page:faq": + - changed-files: + any-glob-to-any-file: ['**/*faq*.*'] + +# Additional type labels +"type:compliance": + - changed-files: + any-glob-to-any-file: ['compliance/**/*'] +"type:accessibility-audit": + - changed-files: + any-glob-to-any-file: ['a11y/**/*', 'accessibility/**/*'] +"type:deprecation": + - changed-files: + any-glob-to-any-file: ['deprecate/**/*'] +"type:audit": + - changed-files: + any-glob-to-any-file: ['audit/**/*'] + +# Additional release labels +"release:hotfix": + - head-branch: ['^hotfix/.*'] + - changed-files: + any-glob-to-any-file: ['hotfix/**/*'] +"release:beta": + - changed-files: + any-glob-to-any-file: ['beta/**/*'] +"release:rc": + - changed-files: + any-glob-to-any-file: ['rc/**/*'] + +# Discussion labels (for reference/documentation) +"discussion:announcement": + - changed-files: + any-glob-to-any-file: [] +"discussion:showcase": + - changed-files: + any-glob-to-any-file: [] +"discussion:community": + - changed-files: + any-glob-to-any-file: [] +"discussion:feedback": + - changed-files: + any-glob-to-any-file: [] +"discussion:support": + - changed-files: + any-glob-to-any-file: [] +"discussion:sponsorship": + - changed-files: + any-glob-to-any-file: [] +"discussion:partnership": + - changed-files: + any-glob-to-any-file: [] + +# Note: Status labels like 'status:in-progress', 'status:blocked', 'status:needs-qa' +# are better managed through the project-meta-sync workflow based on label changes +# and issue/PR state, rather than file patterns. diff --git a/.github/automation/project-pr-labeler.yml b/.github/automation/project-pr-labeler.yml deleted file mode 100644 index b6f3dba25..000000000 --- a/.github/automation/project-pr-labeler.yml +++ /dev/null @@ -1,257 +0,0 @@ -# Expanded PR labeler config for documented workflow - -# Documentation -area:documentation: - - '**/*.md' - - '**/*.txt' - - 'docs/**/*' - - 'README.md' - - 'CHANGELOG.md' - - '**/README.md' - - '**/CHANGELOG.md' - -# Theme & Design System -area:theme: - - 'style.css' - - 'theme.json' - - 'functions.php' - - 'assets/**/*' - - 'theme/**/*' - - 'design-system/**/*' - -theme:tokens: - - 'theme.json' - - 'tokens/*.json' - - 'design-system/tokens/**/*' - -# Patterns & Templates -pattern: - - 'patterns/**/*' -template: - - 'templates/**/*' - - 'parts/**/*' -template-part: - - 'template-parts/**/*' -block: - - 'src/blocks/**/*' - - '**/block.json' - -# CSS & JS -lang:css: - - '**/*.css' - - '**/*.scss' - - '**/*.sass' - - '**/*.less' -lang:js: - - '**/*.js' - - '**/*.jsx' - - '**/*.ts' - - '**/*.tsx' - - 'package.json' - - 'package-lock.json' - - 'tsconfig.json' - -# PHP & Test Files -lang:php: - - '**/*.php' - - 'composer.json' - - 'composer.lock' - - 'phpunit.xml' -test: - - 'tests/**/*' - - 'playwright.config.js' - - '.github/linters/phpunit.xml' - - '**/*.spec.*' - - '**/*.test.*' - - '**/__tests__/**' - -# Build, Scripts, Utility -build: - - 'build/**/*' -area:scripts: - - 'scripts/**/*' - - '**/*.sh' - - '**/*.bash' - - 'utility/**/*' - -# Configuration & Ops -area:configuration: - - '.github/**/*.yml' - - '.github/**/*.yaml' - - '.github/linters/**/*' - - '.stylelintrc.json' - - '.eslintrc.json' - - 'phpcs.xml' - - 'phpunit.xml' - - 'webpack.config.js' - - '*.json' - - '*.yml' - - '*.yaml' - - '.editorconfig' - - '.gitignore' - - 'package.json' - - 'package-lock.json' - - 'composer.json' - - 'composer.lock' - -area:ci: - - '.github/workflows/**' - - '.github/actions/**' - - 'ci/**/*' - -# Assets -area:assets: - - 'assets/**/*' - - 'images/**/*' - - '**/*.png' - - '**/*.jpg' - - '**/*.jpeg' - - '**/*.gif' - - '**/*.svg' - - '**/*.ico' - - '**/*.webp' - -# WooCommerce -area:woocommerce: - - 'woocommerce/**/*' - - '**/woocommerce*.css' - - '**/woocommerce*.php' - - '**/woocommerce*.js' - -# Milestones, Changelog, Release -meta:changelog: - - 'CHANGELOG.md' - - '**/CHANGELOG.md' -release:patch: - - 'patch/**/*' -release:hotfix: - - 'hotfix/**/*' -release:beta: - - 'beta/**/*' -release:rc: - - 'rc/**/*' - -# Security, Compliance, Accessibility, Deprecation -type:security: - - 'security/**/*' -type:compliance: - - 'compliance/**/*' -type:accessibility-audit: - - 'a11y/**/*' - - 'accessibility/**/*' -type:deprecation: - - 'deprecate/**/*' - -# Monitoring, Audit, QA -area:monitoring: - - 'monitoring/**/*' -type:audit: - - 'audit/**/*' -type:qa: - - 'qa/**/*' - -# Device, Layout, Environment, Phase, Page -device:mobile: - - '**/*mobile*.*' -device:desktop: - - '**/*desktop*.*' -device:tablet-landscape: - - '**/*tablet-landscape*.*' -device:tablet-portrait: - - '**/*tablet-portrait*.*' -layout:grid: - - '**/grid*.*' -env:staging: - - '**/staging*.*' -env:live: - - '**/live*.*' -phase:post-launch: - - '**/post-launch*.*' -phase:pre-launch: - - '**/pre-launch*.*' -page:home: - - '**/home*.*' -page:faq: - - '**/faq*.*' - -# Add comp:*/area:* for routing -comp:block-editor: - - 'src/block-editor/**/*' -comp:theme-json: - - 'theme.json' -comp:template-parts: - - 'template-parts/**/*' -area:analytics: - - 'analytics/**/*' -area:forms: - - 'forms/**/*' -area:gallery: - - 'gallery/**/*' -area:integration: - - 'integration/**/*' -area:emails: - - 'emails/**/*' -area:navigation: - - 'navigation/**/*' -area:slider: - - 'slider/**/*' -area:testimonials: - - 'testimonials/**/*' - -# Priority, Status, Type (via branch) -priority:critical: - - head-branch: ['^hotfix/.*'] -priority:normal: - - head-branch: ['^feat/.*', '^fix/.*', '^docs/.*', '^chore/.*'] - -status:needs-review: - - head-branch: ['^feat/.*', '^fix/.*', '^docs?/.*', '^(chore|build|refactor|test|perf|ci|release|hotfix|design|a11y|qa|content|seo|config|migrate|uat|proto|ds|api|schema|telemetry)/.*'] - -type:feature: - - head-branch: ['^feat/.*'] -type:bug: - - head-branch: ['^fix/.*', '^hotfix/.*'] -type:chore: - - head-branch: ['^chore/.*', '^build/.*', '^ci/.*'] -type:refactor: - - head-branch: ['^refactor/.*'] -type:test: - - head-branch: ['^test/.*'] -type:documentation: - - head-branch: ['^docs/.*'] -type:performance: - - head-branch: ['^perf/.*'] -type:design: - - head-branch: ['^design/.*'] -type:a11y: - - head-branch: ['^a11y/.*'] -type:qa: - - head-branch: ['^qa/.*', '^uat/.*'] -type:content: - - head-branch: ['^content/.*'] -type:seo: - - head-branch: ['^seo/.*'] -type:config: - - head-branch: ['^config/.*'] -type:migrate: - - head-branch: ['^migrate/.*'] -type:proto: - - head-branch: ['^proto/.*'] -type:ds: - - head-branch: ['^ds/.*'] -type:api: - - head-branch: ['^api/.*'] -type:schema: - - head-branch: ['^schema/.*'] -type:telemetry: - - head-branch: ['^telemetry/.*', '^analytics/.*'] -type:i18n: - - head-branch: ['^i18n/.*'] -type:ops: - - head-branch: ['^ops/.*'] -type:ux: - - head-branch: ['^ux/.*'] - -# For product-specific prefixes from your workflow (optional) -type:research: - - head-branch: ['^research/.*'] diff --git a/.github/workflows/update-project-fields.yml b/.github/workflows/update-project-fields.yml new file mode 100644 index 000000000..f028c5b07 --- /dev/null +++ b/.github/workflows/update-project-fields.yml @@ -0,0 +1,63 @@ +--- +name: Update Project Fields +on: + push: + branches: [develop] + paths: + - 'scripts/projects/fixtures/**' + - 'scripts/projects/build-project-fields-yml.sh' +permissions: + contents: write +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: develop + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y jq + + - name: Install yq + uses: mikefarah/yq@6d4e6c6e0e2e2c1e2b7bde7cba1cc7a1c1b1c1b1 + + - name: Build canonical YAML + run: bash scripts/projects/build-project-fields-yml.sh + + - name: Check for changes + id: check + run: | + if [[ -n "$(git status --porcelain .github/automation/project-fields.yml)" ]]; then + echo "has_changes=true" >> $GITHUB_OUTPUT + else + echo "has_changes=false" >> $GITHUB_OUTPUT + fi + + - name: Commit updated YAML + if: steps.check.outputs.has_changes == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .github/automation/project-fields.yml + cat > commit-message.txt <<'EOF' + chore: rebuild project-fields.yml from fixtures + + Automated rebuild triggered by changes to CSV fixtures in scripts/projects/fixtures/ + + Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + EOF + git commit -F commit-message.txt + git push + + - name: Summary + run: | + if [[ "${{ steps.check.outputs.has_changes }}" == "true" ]]; then + echo "✅ Updated project-fields.yml" + else + echo "✅ No changes detected" + fi diff --git a/.github/workflows/validate-project-fields.yml b/.github/workflows/validate-project-fields.yml new file mode 100644 index 000000000..229452757 --- /dev/null +++ b/.github/workflows/validate-project-fields.yml @@ -0,0 +1,48 @@ +--- +name: Validate Project Fields +on: + pull_request: + branches: [develop] + paths: + - '.github/automation/project-fields.yml' + - 'schemas/automation/project-fields.schema.json' + - 'scripts/projects/**' +permissions: + contents: read + pull-requests: read +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Check for package.json + id: check_package + run: | + if [[ -f package.json ]]; then + echo "has_package=true" >> $GITHUB_OUTPUT + else + echo "has_package=false" >> $GITHUB_OUTPUT + fi + + - name: Install dependencies (npm ci) + if: steps.check_package.outputs.has_package == 'true' + run: npm ci + + - name: Install dependencies (manual) + if: steps.check_package.outputs.has_package == 'false' + run: | + npm init -y + npm install js-yaml ajv + + - name: Validate YAML against schema + run: node scripts/projects/validate-project-fields.js + + - name: Summary + run: echo "✅ Project fields validation passed" diff --git a/schemas/automation/project-fields.schema.json b/schemas/automation/project-fields.schema.json new file mode 100644 index 000000000..9ce7be0ce --- /dev/null +++ b/schemas/automation/project-fields.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Project Fields", + "type": "object", + "required": ["schema", "types"], + "properties": { + "schema": { "type": "integer", "minimum": 1 }, + "types": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["project_name", "fields"], + "properties": { + "project_name": { "type": "string", "minLength": 1 }, + "fields": { + "type": "array", + "items": { + "type": "object", + "required": ["key", "slug", "type"], + "properties": { + "key": { "type": "string" }, + "slug": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, + "type": { "enum": ["single_select", "text", "number", "multi_select"] }, + "options": { "type": "array", "items": { "type": "string" } } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/scripts/projects/build-project-fields-yml.sh b/scripts/projects/build-project-fields-yml.sh new file mode 100755 index 000000000..b69b91e90 --- /dev/null +++ b/scripts/projects/build-project-fields-yml.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +############################################################################### +# Build canonical project-fields.yml from CSV fixtures +# +# Reads CSV fixtures in scripts/projects/fixtures/*-fields.csv and builds +# canonical YAML at .github/automation/project-fields.yml +# +# Dependencies: yq (v4+), jq +# CSV Format: name,type,options,description,color +# Options are pipe-separated (|) in the CSV +# +# @author LightSpeed +# @requires yq, jq +############################################################################### +set -euo pipefail + +FIX_DIR="scripts/projects/fixtures" +OUT_YML=".github/automation/project-fields.yml" +TMP_JSON="$(mktemp)" +# Ensure cleanup of temporary files on exit, error, or interruption +trap 'rm -f "$TMP_JSON" "$TMP_JSON.new"' EXIT ERR INT TERM + +# Initialize JSON structure +jq -n '{schema:1, types:{}}' > "$TMP_JSON" + +# Find all *-fields.csv files +for csv in "$FIX_DIR"/*-fields.csv; do + [[ ! -f "$csv" ]] && continue + + # Extract type name from filename (e.g., product-development-fields.csv -> product-development) + type_key="$(basename "$csv" .csv | sed 's/-fields$//')" + + echo "Processing $type_key from $csv..." + + # Read CSV and build JSON structure + # Skip comment lines (starting with #) and header + awk -F',' ' + NR==1 || /^#/ { next } # Skip header and comments + NF > 0 { # Only process non-empty lines + # Extract fields + name = $1 + type = $2 + options = $3 + gsub(/^[[:space:]]+|[[:space:]]+$/, "", name) # Trim whitespace + gsub(/^[[:space:]]+|[[:space:]]+$/, "", type) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", options) + gsub(/^"|"$/, "", options) # Remove surrounding quotes + + # Generate slug from name (lowercase, replace spaces with hyphens) + slug = tolower(name) + gsub(/[[:space:]]+/, "-", slug) + + # Print as JSON-compatible line + printf "{\"key\":\"%s\",\"slug\":\"%s\",\"type\":\"%s\"", name, slug, type + + # Add options array if present (pipe-separated) + if (options != "") { + printf ",\"options\":[" + split(options, opts, "|") + for (i in opts) { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", opts[i]) + if (i > 1) printf "," + printf "\"%s\"", opts[i] + } + printf "]" + } + printf "}\n" + } + ' "$csv" | while read -r field_json; do + # Merge into main JSON + jq --arg t "$type_key" \ + --argjson field "$field_json" \ + ' + .types[$t] //= {project_name:$t, fields:[]} + | .types[$t].fields += [$field] + ' "$TMP_JSON" > "$TMP_JSON.new" && mv "$TMP_JSON.new" "$TMP_JSON" + done +done + +# Emit YAML deterministically (sorted keys) +if command -v yq &> /dev/null; then + yq -P 'sort_keys(..)' "$TMP_JSON" > "$OUT_YML" +else + echo "⚠️ yq not found, outputting unsorted YAML" + yq -P "$TMP_JSON" > "$OUT_YML" +fi + +echo "✅ Built $OUT_YML from CSV fixtures" +rm -f "$TMP_JSON" diff --git a/scripts/projects/validate-project-fields.js b/scripts/projects/validate-project-fields.js new file mode 100755 index 000000000..2ea425448 --- /dev/null +++ b/scripts/projects/validate-project-fields.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node +/** + * Validate .github/automation/project-fields.yml against the JSON schema. + * + * @author LightSpeed + * @requires js-yaml, ajv + */ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const Ajv = require('ajv'); + +const SCHEMA_PATH = path.resolve('schemas/automation/project-fields.schema.json'); +const FIELDS_PATH = path.resolve('.github/automation/project-fields.yml'); + +try { + const schema = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8')); + const fields = yaml.load(fs.readFileSync(FIELDS_PATH, 'utf8')); + const ajv = new Ajv({ allErrors: true, strict: true }); + const validate = ajv.compile(schema); + const ok = validate(fields); + + if (!ok) { + console.error('❌ Validation errors:\n', validate.errors); + process.exit(1); + } + console.log('✅ project-fields.yml is valid.'); +} catch (e) { + console.error('❌ Validation failed:', e.message); + process.exit(1); +}