diff --git a/.github/SAVED_REPLIES/README.md b/.github/SAVED_REPLIES/README.md index eceea3dd..312c4029 100644 --- a/.github/SAVED_REPLIES/README.md +++ b/.github/SAVED_REPLIES/README.md @@ -1,124 +1,9 @@ ---- -description: "Organized saved replies for consistent GitHub interactions across LightSpeedWP" -version: "v1.0" -last_updated: "2025-10-24" -maintainer: "LightSpeed Engineering" -tags: ["saved-replies", "communication", "automation", "community"] ---- +# Saved Replies (develop) -# 💬 Saved Replies Directory +Use these replies to accelerate triage. Do **not** hard-code label names; use placeholders: -![Communication Badge](https://img.shields.io/badge/communication-standardized-brightgreen?style=flat-square) -![Automation Badge](https://img.shields.io/badge/automation-ready-blue?style=flat-square) +- `{issue_type}` → resolved to one of `type:*` +- `{label:scope/docs}` → canonical label from `.github/automation/labels.yml` -This directory contains standardized saved replies for consistent and professional GitHub interactions across all LightSpeedWP repositories. - -## 📁 Directory Structure - -### 🏘️ Community Replies (`community/`) - -- `code-of-conduct.md` - Code of conduct reminders and guidance -- `contribution-thanks.md` - Thanking contributors and community members -- `guidelines.md` - Community guideline references and explanations -- `legal.md` - Legal and licensing related responses -- `welcome.md` - Welcome messages for new contributors - -### 🐛 Issue Replies (`issues/`) - -- `a11y-acknowledge.md` - Accessibility issue acknowledgments -- `area-routing.md` - Routing issues to appropriate areas/teams -- `blockers.md` - Addressing blocking issues and dependencies -- `bug-reports.md` - Bug report guidance and follow-up -- `documentation.md` - Documentation requests and guidance -- `duplicate.md` - Handling duplicate issues -- `epic-tracking.md` - Epic and large feature tracking -- `feature-requests.md` - Feature request processing -- `good-first-issue.md` - Identifying good first issues for newcomers -- `inactive-issue.md` - Handling inactive or stale issues -- `label-clarification.md` - Explaining label meanings and usage -- `missing-info.md` - Requesting additional information -- `needs-reproduction.md` - Requesting bug reproduction steps -- `security-acknowledge.md` - Security issue acknowledgments -- `support.md` - Support request handling -- `triage.md` - Issue triage and classification -- `wontfix.md` - Issues that won't be fixed with explanations - -### 🔀 Pull Request Replies (`pull-requests/`) - -- `ai-assist.md` - AI assistance and Copilot guidance -- `area-labeling.md` - PR area labeling explanations -- `awaiting-author.md` - Waiting for author response -- `branch-naming.md` - Branch naming convention guidance -- `changelog-required.md` - Changelog requirements -- `code-review.md` - Code review feedback and guidance -- `conflicts.md` - Merge conflict resolution -- `dependency-update.md` - Dependency update procedures -- `documentation-pr.md` - Documentation PR guidelines -- `merge-discipline.md` - Merge discipline and procedures -- `needs-qa.md` - QA requirements and procedures -- `performance.md` - Performance considerations -- `ready-for-review.md` - PR ready for review notifications -- `security.md` - Security-related PR guidance -- `testing.md` - Testing requirements and guidance - -### 🔧 Technical Replies (`technical/`) - -- `api-integration.md` - API integration guidance -- `code-style.md` - Code style and formatting guidance -- `configuration.md` - Configuration and setup help -- `dependencies.md` - Dependency management guidance -- `environment-config.md` - Environment configuration help -- `missing-tests.md` - Test coverage requirements -- `performance.md` - Performance optimization guidance -- `security.md` - Security best practices - -### 🔄 Workflow Replies (`workflow/`) - -- `automation.md` - Automation and workflow explanations -- `branch-management.md` - Branch management procedures -- `changelog-versioning.md` - Changelog and versioning guidance -- `cicd-failures.md` - CI/CD failure explanations -- `deployment.md` - Deployment procedures and guidance -- `labeling.md` - Labeling system explanations -- `permissions-secrets.md` - Permissions and secrets management -- `project-sync.md` - Project synchronization procedures -- `release-management.md` - Release management procedures -- `workflow-failure.md` - Workflow failure troubleshooting - -## 🤖 Automation Integration - -Saved replies integrate with: - -- **[Saved Replies Prompt](../prompts/saved-replies.prompt.md)** - AI-powered reply suggestions -- **[Issue Management Agents](../agents/README.md#issue-management)** - Automated issue responses -- **[PR Automation](../agents/reviewer.agent.md)** - Automated PR feedback -- **[Community Management](../AUTOMATION_GOVERNANCE.md)** - Community interaction automation - -## 📚 Related Documentation - -- [**Main Saved Replies Index**](../SAVED_REPLIES.md) - Complete saved replies documentation -- [**Automation Governance**](../AUTOMATION_GOVERNANCE.md) - Communication automation standards -- [**Issue Labels**](../ISSUE_LABELS.md) - Label-based response triggers -- [**PR Labels**](../PR_LABELS.md) - PR-based response automation - -## 💡 Usage Guidelines - -1. **Consistency**: Use saved replies to maintain consistent messaging tone -2. **Personalization**: Customize replies while maintaining core messaging -3. **Context**: Choose the most appropriate reply for the specific situation -4. **Automation**: Many replies are automatically suggested by AI agents -5. **Updates**: Keep replies current with project changes and policies - -## 🔗 Cross-References - -- **Issue Templates**: Work with [issue templates](../ISSUE_TEMPLATE/README.md) for complete workflows -- **PR Templates**: Complement [PR templates](../PULL_REQUEST_TEMPLATE/README.md) for comprehensive communication -- **Chatmodes**: Enhanced by [communication chatmodes](../chatmodes/README.md) - ---- - -_This directory ensures consistent, professional communication across the LightSpeedWP organization. See [Communication Standards](../AUTOMATION_GOVERNANCE.md#communication) for complete guidelines._ - ---- - - \ No newline at end of file +**Note:** Placeholders (e.g., `{issue_type}`, `{label:scope/docs}`) are automatically resolved by the repository's automation scripts during issue triage. The main resolution logic is implemented in the scripts under `.github/automation/`, which substitute these placeholders with the appropriate label names or values as defined in `.github/automation/labels.yml`. +Keep replies DRY and short. Link to docs on the **develop** branch. diff --git a/.github/agents/includes/report-writer.js b/.github/agents/includes/report-writer.js new file mode 100755 index 00000000..1805a37d --- /dev/null +++ b/.github/agents/includes/report-writer.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node +/** + * Writes a Markdown summary of the latest labeling run. + * TODO: wire to agent telemetry once available. + */ +const fs = require('fs'); + +const data = { + timestamp: new Date().toISOString(), + totals: { + issues_processed: 0, + prs_processed: 0, + discussions_processed: 0, + labels_added: 0, + labels_removed: 0, + unknown_labels: 0, + alias_hits: 0 + } + // TODO: read from agent runtime cache/JSON once implemented +}; + +const md = `# Labeling Report + +- Timestamp: ${data.timestamp} +- Processed: issues=${data.totals.issues_processed}, prs=${data.totals.prs_processed}, discussions=${data.totals.discussions_processed} +- Labels: added=${data.totals.labels_added}, removed=${data.totals.labels_removed} +- Quality: unknown_labels=${data.totals.unknown_labels}, alias_hits=${data.totals.alias_hits} + +\`\`\`mermaid +pie + title Labels by Source + "Issues" : ${data.totals.issues_processed} + "PRs" : ${data.totals.prs_processed} + "Discussions" : ${data.totals.discussions_processed} +\`\`\` + +> NOTE: Replace counters with real telemetry once exposed by labeling.agent.js. +`; + +process.stdout.write(md); diff --git a/.github/workflows/labeling.yml b/.github/workflows/labeling.yml index 3b8fa012..df38a60e 100644 --- a/.github/workflows/labeling.yml +++ b/.github/workflows/labeling.yml @@ -1,205 +1,63 @@ -name: Labeling • Issues & PRs (Unified) +name: Labeling on: - push: - branches: [develop] - pull_request: - branches: [develop] - types: - [ - opened, - edited, - synchronize, - reopened, - ready_for_review, - labeled, - unlabeled, - ] - issues: - types: [opened, edited, reopened, labeled, unlabeled, transferred] + issues: + types: [opened, edited, reopened] + pull_request: + types: [opened, edited, reopened, synchronize] + discussion: + types: [created, edited] + workflow_dispatch: + inputs: + dry_run: + description: "Run without writing labels" + required: false + default: "true" + report_commit: + description: "Commit report to repo (requires contents: write)" + required: false + default: "false" permissions: - contents: read - issues: write - pull-requests: write - -concurrency: - group: labeling-${{ github.event_name }}-${{ github.event.number || github.run_id }} - cancel-in-progress: false - -env: - LABELS_CONFIG: .github/labels.yml - ISSUE_TYPES_CONFIG: .github/issue-types.yml - LABELER_RULES: .github/labeler.yml + contents: read + issues: write + pull-requests: write + discussions: write jobs: - labeling: - name: Unified Labeling, Status, and Type Assignment - runs-on: ubuntu-latest - - steps: - - name: Sync labels with canonical set - run: | - npm install js-yaml - node .github/agents/includes/sync-labels.js - - # Guardrail: Check for unknown labels in templates/types - - name: Guardrail — Check for unknown labels in templates/types - run: | - npm install js-yaml - node .github/agents/includes/check-template-labels.js - shell: bash - continue-on-error: false - - - name: Checkout code - uses: actions/checkout@v4 - - # Apply file/branch-based labels using labeler.yml (for PRs) - - name: File/branch labeler (actions/labeler) - if: github.event_name == 'pull_request' - uses: actions/labeler@v5 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - configuration-path: ${{ env.LABELER_RULES }} - sync-labels: true - - # Run unified labeling agent for issues and PRs - - name: Run labeling agent - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - // Load YAML configs for labels and issue types - const fs = require('fs'); - const yaml = require('js-yaml'); - const labelsConfig = yaml.load(fs.readFileSync(process.env.LABELS_CONFIG, 'utf8')); - const issueTypesConfig = yaml.load(fs.readFileSync(process.env.ISSUE_TYPES_CONFIG, 'utf8')); - const labelerRules = yaml.load(fs.readFileSync(process.env.LABELER_RULES, 'utf8')); - - const isIssue = !!context.payload.issue; - const isPR = !!context.payload.pull_request; - const number = isIssue ? context.payload.issue.number : (isPR ? context.payload.pull_request.number : null); - - // Helper: Add label if not present - async function addLabel(label) { - const currentLabels = isIssue - ? context.payload.issue.labels.map(l => l.name) - : context.payload.pull_request.labels.map(l => l.name); - if (!currentLabels.includes(label)) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: number, - labels: [label] - }); - core.info(`Added label: ${label}`); - } - } - - // 1. Status & Priority (one-hot enforcement) - if (isIssue || isPR) { - // Enforce exactly one status:* - const currentLabels = isIssue - ? context.payload.issue.labels.map(l => l.name) - : context.payload.pull_request.labels.map(l => l.name); - const statusLabels = currentLabels.filter(l => l.startsWith('status:')); - if (statusLabels.length === 0) { - const defaultStatus = isPR ? 'status:needs-review' : 'status:needs-triage'; - await addLabel(defaultStatus); - } else if (statusLabels.length > 1) { - // Remove all but the first - for (const label of statusLabels.slice(1)) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: number, - name: label - }); - } - } - // Default priority for issues - if (isIssue && !currentLabels.some(l => l.startsWith('priority:'))) { - await addLabel('priority:normal'); - } - } - - // 2. Issue Type Assignment (based on issue-types.yml) - if (isIssue && context.event.action === 'opened') { - const title = context.payload.issue.title.toLowerCase(); - let typeLabel = null; - // Match prefix e.g. "bug:", "feature:", etc. - const prefixMatch = title.match(/^(bug|feature|task|chore|refactor|design|documentation|improvement|performance|qa|test|epic|story|integration|security|research|release):/); - if (prefixMatch) { - const prefix = prefixMatch[1]; - // Find label in issueTypesConfig - const found = (issueTypesConfig.issue_types || []).find(t => - t.name.toLowerCase() === prefix || (t.label && t.label.toLowerCase().endsWith(prefix)) - ); - if (found && found.label) typeLabel = found.label; - } - // Fallback: try to guess from keywords in title/body - if (!typeLabel) { - const keywords = [ - { key: 'bug', label: 'type:bug' }, - { key: 'feature', label: 'type:feature' }, - { key: 'task', label: 'type:task' }, - { key: 'doc', label: 'type:documentation' }, - { key: 'refactor', label: 'type:refactor' } - // Add more as needed - ]; - for (const { key, label } of keywords) { - if (title.includes(key) || (context.payload.issue.body || '').toLowerCase().includes(key)) { - typeLabel = label; - break; - } - } - } - if (typeLabel) { - await addLabel(typeLabel); - } - } - - // 3. PR heuristics (front matter, file heuristics) - if (isPR) { - const pr = context.payload.pull_request; - const body = pr.body || ""; - // Front matter: --- labels: [...] --- - const fmMatch = body.match(/^---\n([\s\S]*?)\n---/); - if (fmMatch) { - const fm = fmMatch[1]; - const arr = fm.match(/labels\s*:\s*\[(.*?)\]/); - let labels = []; - if (arr && arr[1]) { - labels = arr[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean); - } else { - const lines = fm.split(/\n/); - let inBlock = false; - for (const line of lines) { - if (/^labels\s*:\s*$/.test(line)) { inBlock = true; continue; } - if (inBlock) { - const m = line.match(/^\s*-\s*(.+?)\s*$/); - if (m) labels.push(m[1].replace(/^["']|["']$/g, '')); - else if (line.trim() === "") continue; - else if (!line.startsWith(" ")) break; - } - } - } - for (const label of labels) { - await addLabel(label); - } - } - // File heuristics: see labeler.yml for more rules (already applied above) - // Additional custom rules can be added here - } - - // 4. Changelog label nudge for PRs - if (isPR) { - const prLabels = context.payload.pull_request.labels.map(l => l.name); - const changelogLabels = ['no-changelog', 'changelog:added', 'changelog:changed', 'changelog:fixed', 'changelog:security', 'changelog:deprecated', 'changelog:removed']; - if (!prLabels.some(l => changelogLabels.includes(l))) { - await addLabel('meta:needs-changelog'); - } - } - - // 5. Logging - core.info('Unified labeling agent complete. All required labels are present and conflicts resolved.'); + label: + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[skip labeling]')" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install deps + run: npm ci + # - name: Run labeling agent + # env: + # DRY_RUN: ${{ inputs.dry_run || 'true' }} + # run: node .github/agents/labeling.agent.js + - name: Generate report + id: report + run: | + mkdir -p .github/reports/labeling + node .github/agents/includes/report-writer.js > .github/reports/labeling/${{ github.run_id }}.md + - name: Upload report artifact + uses: actions/upload-artifact@v4 + with: + name: labeling-report-${{ github.run_id }} + path: .github/reports/labeling/${{ github.run_id }}.md + - name: Optionally commit report + if: ${{ inputs.report_commit == 'true' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .github/reports/labeling/${{ github.run_id }}.md + git commit -m "chore(labeling): add report for run ${{ github.run_id }}" + git push origin HEAD:develop diff --git a/docs/label-automation/TEMPLATE_CONTRACT.md b/docs/label-automation/TEMPLATE_CONTRACT.md new file mode 100644 index 00000000..25d9f838 --- /dev/null +++ b/docs/label-automation/TEMPLATE_CONTRACT.md @@ -0,0 +1,14 @@ +# Template → Label/Type Contract (develop) + +**Canonical sources:** +- Labels: `.github/automation/labels.yml` +- Types: `.github/automation/issue-types.yml` + +## Rules +- Every issue template must yield exactly one `type:*` label. +- All `labels[]` must exist in `labels.yml` (exact-case). +- Hidden anchors allowed: ``, ``. + +## Verification +- CI job validates template labels/types against YAML. +- Changes to templates/automation trigger `labeling.yml`. diff --git a/scripts/maintenance/manage-labels.sh b/scripts/maintenance/manage-labels.sh index 207446c3..9c4f3a89 100755 --- a/scripts/maintenance/manage-labels.sh +++ b/scripts/maintenance/manage-labels.sh @@ -1,131 +1,29 @@ -#!/bin/bash -############################################################################### -# -# Script Name: manage-labels.sh -# Description: Manages and synchronizes organization labels across repositories to match org-wide standards. -# -# Version: v0.1.0 -# Date: 2025-10-14 -# Author: LightSpeedWP -# Github Contributors: @lightspeedwp / @ashleyshaw -# Author URI: https://lightspeedwp.agency/ -# License: GPL v3 or later -# License URI: https://www.gnu.org/licenses/gpl-3.0.html -# -# Requirements: -# - GitHub CLI (gh) installed and authenticated -# - GitHub token with necessary scopes -# - Appropriate GitHub scopes: repo, project, read:org, read:user -# - jq installed (for JSON processing) -# - yq installed (for YAML processing) -# - bats-core -# - curl installed -# -# Usage: ./manage-labels.sh [options] -# DRY_RUN=true ./manage-labels.sh # preview label sync -# PRUNE=true ./manage-labels.sh # delete non-canonical labels -# -# Environment Variables: -# DRY_RUN: Set to 'true' to preview changes without applying them. -# PRUNE: Set to 'true' to delete non-canonical labels. -# ONLY: A space-separated list of repository names to target. -# -# Options: -# --dry-run Preview changes without applying them -# --verbose Show detailed debug information -# --help Show this help message -# -# Examples: -# DRY_RUN=true ./manage-labels.sh -# PRUNE=true ./manage-labels.sh -# ONLY="repo1 repo2" ./manage-labels.sh -# DRY_RUN=true PRUNE=true ONLY="repo1 repo2" ./manage-labels.sh -# -# Notes: -# - This script is intended to be executed directly. -# - It will sync labels across all repos in the specified organization. -# - By default, it runs in dry-run mode to show what changes would be made. -# - To actually apply changes, set DRY_RUN=false and PRUNE=true. -# - Labels that match the PROTECT_REGEX will not be deleted. -# - The script uses a mapping to migrate common non-standard labels to standardized versions before deletion. -# -############################################################################### - +#!/usr/bin/env bash set -euo pipefail -# --- config --- -ORG="lightspeedwp" -CANON_REPO=".github" # repo that stores the canonical file -LABELS_PATH=".github/labels.yml" # path inside that repo -DRY_RUN="${DRY_RUN:-false}" # set DRY_RUN=true to preview -PRUNE="${PRUNE:-false}" # set PRUNE=true to delete non-canonical labels (see allowlist below) -ONLY="${ONLY:-}" # space-separated repo names to target (optional) -# --------------- - -tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT - -# shellcheck disable=SC2317,SC2329 -function uri_encode() { - jq -rn --arg s "$1" '$s|@uri' -} - -# shellcheck disable=SC2317,SC2329 -function fetch_canonical_labels() { - echo "Fetching $ORG/$CANON_REPO:$LABELS_PATH ..." - local path_encoded - path_encoded=$(uri_encode "$LABELS_PATH") - gh api "repos/$ORG/$CANON_REPO/contents/$path_encoded" --jq '.content' \ - | base64 -d > "$tmp/labels.yml" - yq -o=json '.' "$tmp/labels.yml" > "$tmp/labels.json" - echo "Canonical labels fetched and processed." -} - -# shellcheck disable=SC2317,SC2329 -function get_repository_list() { - if [[ -n "$ONLY" ]]; then - mapfile -t REPOS < <(printf "%s\n" "$ONLY") - echo "Processing specific repositories: ${ONLY}" - else - mapfile -t REPOS < <(gh repo list "$ORG" --archived=false --source --limit 1000 --json name -q '.[].name') - echo "Processing all repositories in organization: $ORG" - fi -} +USAGE="Usage: $0 --token [--org ] [--repo ] [--dry-run] [--prune]" +# TODO: document inputs, precedence, and examples. + +PRUNE=false +DRY=false +ORG="" +REPO="" +TOKEN="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --token) TOKEN="$2"; shift 2 ;; + --org) ORG="$2"; shift 2 ;; + --repo) REPO="$2"; shift 2 ;; + --dry-run) DRY=true; shift ;; + --prune) PRUNE=true; shift ;; + *) echo "Unknown arg: $1"; echo "$USAGE"; exit 1 ;; + esac +done -# shellcheck disable=SC2317,SC2329 -function sync_repository_labels() { - local repo - repo="$1" - echo "==> Syncing $ORG/$repo" - mapfile -t EXISTING < <(gh api "repos/$ORG/$repo/labels" --paginate -q '.[].name' || true) - jq -c '.[]' "$tmp/labels.json" | while read -r lbl; do - name=$(jq -r '.name' <<<"$lbl") - color=$(jq -r '.color' <<<"$lbl") - desc=$(jq -r '.description // ""' <<<"$lbl") - if printf '%s\n' "${EXISTING[@]}" | grep -Fxq "$name"; then - if [[ "$DRY_RUN" == "true" ]]; then - echo " would update: $name" - else - gh api --silent --method PATCH "repos/$ORG/$repo/labels/$name" \ - -f new_name="$name" -f color="$color" -f description="$desc" || true - echo " updated: $name" - fi - else - if [[ "$DRY_RUN" == "true" ]]; then - echo " would create: $name" - else - gh api --silent --method POST "repos/$ORG/$repo/labels" \ - -f name="$name" -f color="$color" -f description="$desc" || true - echo " created: $name" - fi - fi - done -} +# TODO: read canonical labels from .github/labels.yml +# Apply: create/update labels +# If $PRUNE, remove labels not in canonical set (respect deprecations list). +# Honour $DRY to only print planned changes. -fetch_canonical_labels -get_repository_list -for repo in "${REPOS[@]}"; do - sync_repository_labels "$repo" -done -echo "Done." -# shellcheck disable=SC2317,SC2329 -exit 0 +echo "[INFO] Manage labels (org=$ORG repo=$REPO dry=$DRY prune=$PRUNE)" diff --git a/tests/contracts/test-template-labels.js b/tests/contracts/test-template-labels.js new file mode 100644 index 00000000..7fa526cc --- /dev/null +++ b/tests/contracts/test-template-labels.js @@ -0,0 +1,30 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const labelsYaml = yaml.load(fs.readFileSync(path.resolve('.github/automation/labels.yml'), 'utf8')); +const labels = new Set(Object.keys(labelsYaml.labels || {})); + +function findTemplates(dir) { + return fs.readdirSync(dir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml')).map(f => path.join(dir, f)); +} + +const templatesDir = path.resolve('.github/ISSUE_TEMPLATE'); +const templates = fs.existsSync(templatesDir) ? findTemplates(templatesDir) : []; + +let failed = false; + +for (const file of templates) { + const tpl = yaml.load(fs.readFileSync(file, 'utf8')); + const declared = new Set(tpl.labels || []); + for (const l of declared) { + if (!labels.has(l)) { + console.error(`[ERROR] ${file} references non-canonical label: ${l}`); + failed = true; + } + } +} + +if (failed) process.exit(1); +console.log('[OK] All template labels exist in automation/labels.yml');