From 31f66acb35698bd53e70efb6ffc1ffc0b5145d11 Mon Sep 17 00:00:00 2001 From: Ash Shaw Date: Fri, 19 Jun 2026 12:15:56 +0200 Subject: [PATCH 1/2] fix metadata sync fallback --- .github/workflows/project-meta-sync.yml | 30 +-- agents/project-meta-sync.agent.md | 3 +- docs/AUTOMATION.md | 2 +- docs/CANONICAL_CONFIGS_GUIDE.md | 5 + docs/GITHUB_PROJECT_OPERATIONS_SPEC.md | 7 +- docs/ISSUE_CREATION_GUIDE.md | 2 +- docs/ISSUE_FIELDS.md | 9 +- docs/WORKFLOW_COORDINATION.md | 2 +- .../project-meta-sync.instructions.md | 10 +- .../__tests__/derive-project-fields.test.js | 83 ++++++- .../agents/includes/derive-project-fields.cjs | 213 +++++++++++++++++- .../__tests__/project-meta-sync.test.js | 48 ++++ 12 files changed, 373 insertions(+), 41 deletions(-) create mode 100644 scripts/validation/__tests__/project-meta-sync.test.js diff --git a/.github/workflows/project-meta-sync.yml b/.github/workflows/project-meta-sync.yml index 087a5d37..00404b72 100644 --- a/.github/workflows/project-meta-sync.yml +++ b/.github/workflows/project-meta-sync.yml @@ -1,8 +1,6 @@ name: Projects • Add & Sync meta from labels on: - push: - branches: [develop] issues: types: [opened, edited, labeled, unlabeled, reopened, closed] pull_request: @@ -11,11 +9,11 @@ on: [ opened, edited, + synchronize, labeled, unlabeled, reopened, ready_for_review, - synchronize, closed, ] @@ -91,22 +89,13 @@ jobs: - name: Collect labels from item id: collect-labels if: steps.preflight.outputs.enabled == 'true' - run: | - set -euo pipefail - if [ "${{ github.event_name }}" = "issues" ]; then - NUMBER=${{ github.event.issue.number }} - gh issue view "$NUMBER" --json labels --jq '.labels[].name' > /tmp/labels.txt - else - NUMBER=${{ github.event.pull_request.number }} - gh pr view "$NUMBER" --json labels --jq '.labels[].name' > /tmp/labels.txt - fi - { - echo 'labels<> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const labels = context.payload.issue?.labels || context.payload.pull_request?.labels || []; + const labelNames = labels.map((label) => label.name).filter(Boolean).join('\n'); + core.setOutput('labels', labelNames); - name: Derive Status/Priority/Type from canonical issue fields id: derive @@ -119,6 +108,9 @@ jobs: PR_MERGED: ${{ github.event.pull_request.merged }} ITEM_CREATED_AT: ${{ github.event.issue.created_at || github.event.pull_request.created_at }} ITEM_MILESTONE_DUE_ON: ${{ github.event.issue.milestone.due_on || github.event.pull_request.milestone.due_on }} + ITEM_TITLE: ${{ github.event.issue.title || github.event.pull_request.title }} + ITEM_BODY: ${{ github.event.issue.body || github.event.pull_request.body }} + PR_HEAD_REF: ${{ github.event.pull_request.head.ref || '' }} ISSUE_FIELDS_CONFIG: .github/issue-fields.yml - name: Update project fields diff --git a/agents/project-meta-sync.agent.md b/agents/project-meta-sync.agent.md index 4272bc2e..0f2c3495 100644 --- a/agents/project-meta-sync.agent.md +++ b/agents/project-meta-sync.agent.md @@ -68,5 +68,6 @@ permissions: - github:issues metadata: guardrails: Compatibility only. Do not treat this spec as the active contract; - use the workflow and helper scripts referenced above. + use the workflow and helper scripts referenced above. The live contract keeps + issue labels, issue types, and project fields in sync via project-meta sync. --- diff --git a/docs/AUTOMATION.md b/docs/AUTOMATION.md index df7f3848..b40ef1ca 100644 --- a/docs/AUTOMATION.md +++ b/docs/AUTOMATION.md @@ -75,7 +75,7 @@ If your project allows hotfixes directly to `main`, ensure validation workflows | **metadata-governance.yml** | issues / pull_request_target | Apply assignee, milestone, and relationship metadata | issue-pr-metadata.cjs | | **planner.yml** | develop | Post merge-readiness checklists and exit criteria to PRs | planner.agent.js | | **reviewer.yml** | develop | Automated PR review and quality feedback | reviewer.agent.js | -| **project-meta-sync.yml** | push / issues / pull_request | Sync project board fields from labels and kickoff metadata | derive-project-fields.cjs | +| **project-meta-sync.yml** | issues / pull_request | Sync project board fields from labels, title/body fallbacks, and kickoff metadata | derive-project-fields.cjs | | **checklist-finalisation.yml** | issues.closed / pull_request_target.closed | Final checklist sync for completed issues and merged PRs | workflow backstop | | **release.yml** | main | Versioning, changelog generation, tagging, and release notes | release.agent.js | | **reporting.yml** | develop | Generate metrics and activity reports | reporting.agent.js | diff --git a/docs/CANONICAL_CONFIGS_GUIDE.md b/docs/CANONICAL_CONFIGS_GUIDE.md index e515fcb9..c30cf093 100644 --- a/docs/CANONICAL_CONFIGS_GUIDE.md +++ b/docs/CANONICAL_CONFIGS_GUIDE.md @@ -34,6 +34,10 @@ This guide documents the canonical relationship between: It also maps end-to-end data flow from issue/PR creation to automation completion. +Project metadata sync is label-driven, but it also treats issue type selection +and safe content/branch fallbacks as first-class signals so project fields can +catch up when labels are added after creation. + ## Canonical Roles | File | Canonical Role | Primary Consumers | @@ -89,6 +93,7 @@ sequenceDiagram 2. `status:*` and `priority:*` mappings in `.github/issue-fields.yml` must resolve to labels defined in `.github/labels.yml`. 3. `.github/labeler.yml` may only emit canonical labels defined in `.github/labels.yml`. 4. `.github/issue-types.yml` display types should map to canonical `type:*` labels that can be projected into project field Type values. +5. `project-meta-sync.yml` should reprocess label changes so late `type:*` labels or corrected issue types update the project field projection. ## Current Risks Observed diff --git a/docs/GITHUB_PROJECT_OPERATIONS_SPEC.md b/docs/GITHUB_PROJECT_OPERATIONS_SPEC.md index feb46591..701cd39d 100644 --- a/docs/GITHUB_PROJECT_OPERATIONS_SPEC.md +++ b/docs/GITHUB_PROJECT_OPERATIONS_SPEC.md @@ -81,13 +81,14 @@ Template labels must remain canonical and pass: - `Priority` - `Type` - `Effort` -- `Start date` and `Target date` only when `status:ready` or `status:in-progress` is present +- `Start date` and `Target date` when `status:ready` or `status:in-progress` is present Derivation source notes: - `Status`, `Priority`, and `Type` are mapped from labels via `.github/issue-fields.yml` mappings. - `Effort` uses the configured default from canonical issue-fields configuration. -- `Start date` and `Target date` stay empty at creation time and are only populated after kickoff-ready metadata is present (`status:ready` or `status:in-progress`). +- `Type` and `Priority` now fall back to safe content-based inference when labels are missing or late. +- `Start date` and `Target date` are populated when kickoff-ready metadata is present (`status:ready` or `status:in-progress`) and the workflow reprocesses label changes so fields can catch up after triage. - Metadata governance for assignees, milestones, and relationships is handled separately by `.github/workflows/metadata-governance.yml`. Current preflight conditions must be satisfied before sync runs: @@ -98,7 +99,7 @@ Current preflight conditions must be satisfied before sync runs: Safe automation boundary: -- Active write path is limited to the five core derived fields plus kickoff-aware date handling. +- Active write path covers the five core derived fields plus kickoff-aware date handling. - Additional direct issue-field writes are out of scope until a dedicated follow-up verification approves extension. Verification reference: `.github/reports/audits/2026-06-07-private-project-issue-field-write-verification-879.md`. diff --git a/docs/ISSUE_CREATION_GUIDE.md b/docs/ISSUE_CREATION_GUIDE.md index c41c183b..7e3bd943 100644 --- a/docs/ISSUE_CREATION_GUIDE.md +++ b/docs/ISSUE_CREATION_GUIDE.md @@ -122,7 +122,7 @@ Click **Submit new issue**. Your issue is now visible to the team and ready for - Unified labeling agent applies canonicalization, one-hot enforcement, defaults, and content-based type detection - PR automation remains stronger due to branch/file signals available in PR context - `metadata-governance.yml` automatically adds new issues and PRs to the configured project, assigns the requester when possible, attaches or creates an appropriate milestone, and records relationships when they are present in the body -- `project-meta-sync.yml` keeps the project fields in sync and leaves `Start date` and `Target date` empty until work is explicitly marked `status:ready` or `status:in-progress` +- `project-meta-sync.yml` keeps the project fields in sync, re-runs on label changes, and backfills `Status`, `Priority`, `Type`, `Effort`, `Start date`, and `Target date` from canonical labels plus safe fallbacks ### ⚠️ Practical Implication diff --git a/docs/ISSUE_FIELDS.md b/docs/ISSUE_FIELDS.md index 61086fc9..6d890241 100644 --- a/docs/ISSUE_FIELDS.md +++ b/docs/ISSUE_FIELDS.md @@ -436,10 +436,13 @@ The following project field features are enabled across all LightSpeed repositor Managed via `.github/workflows/project-meta-sync.yml` automation. -Current live write boundary (verified 2026-06-07): +Current workflow contract: -- Workflow writes `Status`, `Priority`, `Type`, `Effort`, and `Start date`. -- `Target date` remains part of canonical config and governance but is currently deferred from automated writes. +- Workflow writes `Status`, `Priority`, `Type`, `Effort`, `Start date`, and `Target date`. +- `Start date` and `Target date` are only populated when kickoff metadata is present. +- If labels arrive after creation, the sync workflow reprocesses the item and backfills + `Type` and `Priority` from canonical labels, issue type intent, or safe content + fallbacks. Verification record: `.github/reports/audits/2026-06-07-private-project-issue-field-write-verification-879.md`. diff --git a/docs/WORKFLOW_COORDINATION.md b/docs/WORKFLOW_COORDINATION.md index 97de54df..30def397 100644 --- a/docs/WORKFLOW_COORDINATION.md +++ b/docs/WORKFLOW_COORDINATION.md @@ -49,7 +49,7 @@ Always-run workflows trigger automatically on push/PR events without agent invol | `issues.yml` | issue opened/edited | Validate issue templates | ❌ No (validation only) | | `meta.yml` | PR opened/issues | Apply frontmatter validation | ❌ No (metadata only) | | `metadata-governance.yml` | issues / pull_request_target | Assign assignees, milestones, and relationship metadata | ❌ No (metadata only) | -| `project-meta-sync.yml` | push / issues / pull_request | Sync GitHub Project board fields | ❌ No (metadata only) | +| `project-meta-sync.yml` | issues / pull_request | Sync GitHub Project board fields | ❌ No (metadata only) | | `readme-regen.yml` | push/PR on `.md` files | Validate/regenerate README indices | ❌ No (informational) | ### When to Use diff --git a/instructions/project-meta-sync.instructions.md b/instructions/project-meta-sync.instructions.md index 19322b0d..5580c52b 100644 --- a/instructions/project-meta-sync.instructions.md +++ b/instructions/project-meta-sync.instructions.md @@ -5,7 +5,7 @@ description: Deprecated compatibility guidance for the legacy project meta sync entrypoint. The active contract lives in the project-meta-sync and metadata-governance workflows plus their helper scripts. version: v1.1 -last_updated: '2026-05-29' +last_updated: '2026-06-19' owners: - LightSpeed Engineering tags: @@ -23,4 +23,12 @@ apply_to: - .github/workflows/metadata-governance.yml - scripts/agents/includes/derive-project-fields.cjs - scripts/agents/includes/issue-pr-metadata.cjs + +## Active Contract Summary + +- Keep `status:*`, `priority:*`, and `type:*` labels canonical and one-hot. +- Treat issue types as the source of truth for canonical `type:*` intent and let + `project-meta-sync.yml` project that intent into GitHub Project fields. +- Allow the workflow to re-run on label changes so late-applied labels and issue + type corrections catch up in the project board. --- diff --git a/scripts/agents/includes/__tests__/derive-project-fields.test.js b/scripts/agents/includes/__tests__/derive-project-fields.test.js index 6b7cd70b..db71f2ac 100644 --- a/scripts/agents/includes/__tests__/derive-project-fields.test.js +++ b/scripts/agents/includes/__tests__/derive-project-fields.test.js @@ -1,7 +1,11 @@ const fs = require("fs"); +const os = require("os"); const path = require("path"); const yaml = require("js-yaml"); -const { deriveProjectFieldValues } = require("../derive-project-fields.cjs"); +const { + deriveProjectFieldValues, + run, +} = require("../derive-project-fields.cjs"); const issueFieldsConfig = yaml.load( fs.readFileSync( @@ -48,4 +52,81 @@ describe("derive-project-fields.cjs", () => { targetDate: "2026-07-18", }); }); + + test("falls back to PR defaults when labels are missing", () => { + const result = deriveProjectFieldValues({ + cfg: issueFieldsConfig, + labels: [], + eventName: "pull_request", + eventAction: "opened", + title: "Refine metadata sync behaviour", + body: "Improve how project metadata is allocated.", + }); + + expect(result).toMatchObject({ + status: "In review", + priority: "Normal", + type: "Chore", + startDate: "", + targetDate: "", + }); + }); + + test("infers type and priority from content when labels are missing", () => { + const result = deriveProjectFieldValues({ + cfg: issueFieldsConfig, + labels: [], + eventName: "pull_request", + eventAction: "opened", + title: "Urgent bug: project metadata stays blank", + body: "This bug blocks release and should be fixed immediately.", + headRef: "fix/metadata-governance-sync", + }); + + expect(result).toMatchObject({ + status: "In review", + priority: "Critical", + type: "Bug", + }); + }); + + test("writes derived values to GITHUB_OUTPUT when run as a script", () => { + const tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), "derive-project-fields-"), + ); + const outputPath = path.join(tmpDir, "github-output.txt"); + const originalEnv = { + ISSUE_FIELDS_CONFIG: process.env.ISSUE_FIELDS_CONFIG, + EVENT_NAME: process.env.EVENT_NAME, + EVENT_ACTION: process.env.EVENT_ACTION, + LABELS: process.env.LABELS, + ITEM_TITLE: process.env.ITEM_TITLE, + ITEM_BODY: process.env.ITEM_BODY, + PR_HEAD_REF: process.env.PR_HEAD_REF, + GITHUB_OUTPUT: process.env.GITHUB_OUTPUT, + }; + + try { + process.env.EVENT_NAME = "pull_request"; + process.env.EVENT_ACTION = "opened"; + process.env.LABELS = ""; + process.env.ITEM_TITLE = "Urgent bug: project metadata stays blank"; + process.env.ITEM_BODY = + "This bug blocks release and should be fixed immediately."; + process.env.PR_HEAD_REF = "fix/metadata-governance-sync"; + process.env.GITHUB_OUTPUT = outputPath; + + run(); + } finally { + for (const [key, value] of Object.entries(originalEnv)) { + if (typeof value === "undefined") delete process.env[key]; + else process.env[key] = value; + } + } + + const output = fs.readFileSync(outputPath, "utf8"); + expect(output).toContain("status=In review"); + expect(output).toContain("priority=Critical"); + expect(output).toContain("type=Bug"); + }); }); diff --git a/scripts/agents/includes/derive-project-fields.cjs b/scripts/agents/includes/derive-project-fields.cjs index 5d0e310b..1e3d3074 100644 --- a/scripts/agents/includes/derive-project-fields.cjs +++ b/scripts/agents/includes/derive-project-fields.cjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +/* global console, process */ /* eslint-disable no-console */ const fs = require("fs"); const path = require("path"); @@ -19,6 +20,170 @@ function firstMatch(labels, mapping) { return ""; } +const BRANCH_PREFIX_TYPE_MAP = { + "feat/": "type:feature", + "feature/": "type:feature", + "fix/": "type:bug", + "bugfix/": "type:bug", + "hotfix/": "type:bug", + "docs/": "type:documentation", + "doc/": "type:documentation", + "test/": "type:test", + "tests/": "type:test", + "perf/": "type:performance", + "refactor/": "type:refactor", + "chore/": "type:chore", + "ci/": "type:ci", + "deps/": "type:dependency", + "build/": "type:build", + "security/": "type:security", + "a11y/": "type:a11y", +}; + +const TYPE_KEYWORDS = [ + { + label: "type:bug", + patterns: [/\b(bug|defect|error|crash|broken|failure|fix)\b/i], + }, + { + label: "type:feature", + patterns: [ + /\b(feature|enhancement|improvement|add|implement|build|develop)\b/i, + ], + }, + { + label: "type:documentation", + patterns: [ + /\b(documentation|docs|readme|guide|tutorial|document|explain|clarify)\b/i, + ], + }, + { + label: "type:test", + patterns: [/\b(test|testing|coverage|unit test|integration test|qa)\b/i], + }, + { + label: "type:performance", + patterns: [ + /\b(performance|perf|optimi[sz]e|latency|slow|bottleneck|speed)\b/i, + ], + }, + { + label: "type:security", + patterns: [ + /\b(security|vulnerability|exploit|cve|ghsa|credential|token|password)\b/i, + ], + }, + { + label: "type:refactor", + patterns: [/\b(refactor|restructure|cleanup|simplify|moderni[sz]e)\b/i], + }, + { + label: "type:chore", + patterns: [/\b(chore|maintenance|cleanup|housekeeping|tidy)\b/i], + }, + { + label: "type:automation", + patterns: [/\b(automation|workflow|action|bot|script|pipeline)\b/i], + }, + { + label: "type:integration", + patterns: [/\b(integration|dependency|compatibility|interop)\b/i], + }, + { + label: "type:release", + patterns: [/\b(release|version bump|tag|publish)\b/i], + }, + { + label: "type:a11y", + patterns: [/\b(a11y|accessibility|wcag)\b/i], + }, +]; + +const PRIORITY_KEYWORDS = [ + { + label: "priority:critical", + patterns: [/\b(critical|urgent|blocking|asap|severe)\b/i], + }, + { + label: "priority:important", + patterns: [/\b(high priority|important|priority\s*1|p1)\b/i], + }, + { + label: "priority:minor", + patterns: [/\b(low priority|minor|cosmetic|nice to have|p3)\b/i], + }, +]; + +function inferMappedValueFromText(text, rules, mapping) { + const haystack = String(text || ""); + for (const rule of rules) { + if (rule.patterns.some((pattern) => pattern.test(haystack))) { + return mapping?.[rule.label] || ""; + } + } + return ""; +} + +function inferTypeFromContext({ + labels = [], + title = "", + body = "", + headRef = "", + mappings = {}, +} = {}) { + const labelType = firstMatch(labels, mappings.Type); + if (labelType) return labelType; + + const lowerHeadRef = String(headRef || "").toLowerCase(); + for (const [prefix, typeLabel] of Object.entries(BRANCH_PREFIX_TYPE_MAP)) { + if (lowerHeadRef.startsWith(prefix)) { + return mappings.Type?.[typeLabel] || ""; + } + } + + const titleMatch = inferMappedValueFromText( + title, + TYPE_KEYWORDS, + mappings.Type, + ); + if (titleMatch) return titleMatch; + + const bodyMatch = inferMappedValueFromText( + body, + TYPE_KEYWORDS, + mappings.Type, + ); + if (bodyMatch) return bodyMatch; + + return ""; +} + +function inferPriorityFromContext({ + labels = [], + title = "", + body = "", + mappings = {}, +} = {}) { + const labelPriority = firstMatch(labels, mappings.Priority); + if (labelPriority) return labelPriority; + + const titleMatch = inferMappedValueFromText( + title, + PRIORITY_KEYWORDS, + mappings.Priority, + ); + if (titleMatch) return titleMatch; + + const bodyMatch = inferMappedValueFromText( + body, + PRIORITY_KEYWORDS, + mappings.Priority, + ); + if (bodyMatch) return bodyMatch; + + return mappings.Priority?.["priority:normal"] || "Normal"; +} + function deriveProjectFieldValues({ cfg, labels = [], @@ -27,18 +192,23 @@ function deriveProjectFieldValues({ prMerged = false, itemCreatedAt = "", milestoneDueOn = "", + title = "", + body = "", + headRef = "", } = {}) { const mappings = cfg.project_field_mappings || {}; const orgFields = cfg.organization_issue_fields || {}; const customFields = Array.isArray(orgFields.custom_fields) ? orgFields.custom_fields : []; - const effortField = customFields.find((field) => field?.key === "Effort") || {}; - const effortDefault = typeof effortField.default === "string" ? effortField.default : ""; + const effortField = + customFields.find((field) => field?.key === "Effort") || {}; + const effortDefault = + typeof effortField.default === "string" ? effortField.default : ""; let status = firstMatch(labels, mappings.Status); - const priority = firstMatch(labels, mappings.Priority); - let type = firstMatch(labels, mappings.Type); + let priority = inferPriorityFromContext({ labels, title, body, mappings }); + let type = inferTypeFromContext({ labels, title, body, headRef, mappings }); let effort = effortDefault; let startDate = ""; let targetDate = ""; @@ -47,16 +217,33 @@ function deriveProjectFieldValues({ labels.includes("status:ready") || labels.includes("status:in-progress"); if (eventName === "issues" && eventAction === "closed") { - status = mappings.Status?.[cfg.defaults?.issue?.status_label_closed] || "Done"; + status = + mappings.Status?.[cfg.defaults?.issue?.status_label_closed] || "Done"; } if (eventName === "pull_request" && eventAction === "closed" && prMerged) { status = - mappings.Status?.[cfg.defaults?.pull_request?.status_label_merged] || "Done"; + mappings.Status?.[cfg.defaults?.pull_request?.status_label_merged] || + "Done"; } - if (!status) status = mappings.Status?.["status:needs-triage"] || "Triage"; - if (!type) type = ""; + if (!status) { + if (eventName === "pull_request") { + status = + mappings.Status?.[cfg.defaults?.pull_request?.status_label_open] || + mappings.Status?.["status:needs-review"] || + "In review"; + } else { + status = mappings.Status?.["status:needs-triage"] || "Triage"; + } + } + + if (!type) { + type = + eventName === "pull_request" + ? mappings.Type?.[cfg.defaults?.pull_request?.type_label] || "Chore" + : mappings.Type?.[cfg.defaults?.issue?.type_label] || "Task"; + } if (itemCreatedAt && isKickoff && status !== "Done") { startDate = itemCreatedAt.slice(0, 10); @@ -76,7 +263,7 @@ function deriveProjectFieldValues({ }; } -function main() { +function run() { const configPath = process.env.ISSUE_FIELDS_CONFIG ? path.resolve(process.env.ISSUE_FIELDS_CONFIG) : path.resolve(".github/issue-fields.yml"); @@ -95,6 +282,9 @@ function main() { prMerged: (process.env.PR_MERGED || "false").toLowerCase() === "true", itemCreatedAt: process.env.ITEM_CREATED_AT || "", milestoneDueOn: process.env.ITEM_MILESTONE_DUE_ON || "", + title: process.env.ITEM_TITLE || "", + body: process.env.ITEM_BODY || "", + headRef: process.env.PR_HEAD_REF || "", }); const output = process.env.GITHUB_OUTPUT; @@ -115,11 +305,14 @@ function main() { } if (require.main === module) { - main(); + run(); } module.exports = { deriveProjectFieldValues, + inferPriorityFromContext, + inferTypeFromContext, firstMatch, readConfig, + run, }; diff --git a/scripts/validation/__tests__/project-meta-sync.test.js b/scripts/validation/__tests__/project-meta-sync.test.js new file mode 100644 index 00000000..fc010a22 --- /dev/null +++ b/scripts/validation/__tests__/project-meta-sync.test.js @@ -0,0 +1,48 @@ +/** + * @jest-environment jsdom + */ + +const fs = require("fs"); +const path = require("path"); +const yaml = require("js-yaml"); + +describe("project-meta-sync workflow contract", () => { + const workflowPath = path.join( + __dirname, + "../../../.github/workflows/project-meta-sync.yml", + ); + + test("runs on issue and PR label churn instead of push", () => { + const workflow = yaml.load(fs.readFileSync(workflowPath, "utf8")); + + expect(workflow.on.push).toBeUndefined(); + expect(workflow.on.issues.types).toEqual( + expect.arrayContaining(["opened", "edited", "labeled", "unlabeled"]), + ); + expect(workflow.on.pull_request.types).toEqual( + expect.arrayContaining([ + "opened", + "edited", + "synchronize", + "labeled", + "unlabeled", + "ready_for_review", + "closed", + ]), + ); + }); + + test("collects labels from the event payload", () => { + const workflow = yaml.load(fs.readFileSync(workflowPath, "utf8")); + const collectStep = workflow.jobs["add-and-sync"].steps.find( + (step) => step.id === "collect-labels", + ); + + expect(collectStep).toBeDefined(); + expect(collectStep.uses).toBe("actions/github-script@v7"); + expect(collectStep.with.script).toContain("context.payload.issue?.labels"); + expect(collectStep.with.script).toContain( + "context.payload.pull_request?.labels", + ); + }); +}); From 5a1f6f83f0e2b9d218c011db589e8bf228f81a5e Mon Sep 17 00:00:00 2001 From: Ash Shaw Date: Fri, 19 Jun 2026 12:18:49 +0200 Subject: [PATCH 2/2] docs refresh metadata headers --- docs/AUTOMATION.md | 4 ++-- docs/CANONICAL_CONFIGS_GUIDE.md | 2 +- docs/ISSUE_CREATION_GUIDE.md | 4 ++-- docs/ISSUE_FIELDS.md | 4 ++-- docs/WORKFLOW_COORDINATION.md | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/AUTOMATION.md b/docs/AUTOMATION.md index b40ef1ca..30de06fd 100644 --- a/docs/AUTOMATION.md +++ b/docs/AUTOMATION.md @@ -2,8 +2,8 @@ file_type: "documentation" title: "Automation & Workflows" description: "Strategy, governance, and workflow documentation for GitHub automation in LightSpeed repositories." -version: "v1.0.4" -last_updated: "2026-06-18" +version: "v1.0.5" +last_updated: "2026-06-19" owners: ["LightSpeedWP Team"] tags: ["automation", "workflows", "governance", "agents"] status: "active" diff --git a/docs/CANONICAL_CONFIGS_GUIDE.md b/docs/CANONICAL_CONFIGS_GUIDE.md index c30cf093..41cefecf 100644 --- a/docs/CANONICAL_CONFIGS_GUIDE.md +++ b/docs/CANONICAL_CONFIGS_GUIDE.md @@ -2,7 +2,7 @@ file_type: documentation title: Canonical Config File Interdependencies Guide description: Canonical reference for how labels.yml, issue-types.yml, labeler.yml, and issue-fields.yml interact from issue creation through automation completion. -version: v1.0.3 +version: v1.0.4 created_date: "2026-06-03" last_updated: "2026-06-19" authors: diff --git a/docs/ISSUE_CREATION_GUIDE.md b/docs/ISSUE_CREATION_GUIDE.md index 7e3bd943..4a74e3c8 100644 --- a/docs/ISSUE_CREATION_GUIDE.md +++ b/docs/ISSUE_CREATION_GUIDE.md @@ -2,9 +2,9 @@ title: GitHub Issue Creation Guide description: How to create well-formed issues, select templates, and trigger automation file_type: documentation -version: "1.0.5" +version: "1.0.6" created_date: "2026-05-31" -last_updated: "2026-06-18" +last_updated: "2026-06-19" author: Claude Code maintainer: Ash Shaw owners: diff --git a/docs/ISSUE_FIELDS.md b/docs/ISSUE_FIELDS.md index 6d890241..d0745cad 100644 --- a/docs/ISSUE_FIELDS.md +++ b/docs/ISSUE_FIELDS.md @@ -2,9 +2,9 @@ title: Issue Fields Specification description: Canonical specification for GitHub organization issue fields, type mappings, and project automation configuration file_type: documentation -version: v1.0.5 +version: v1.0.6 created_date: '2026-05-31' -last_updated: '2026-06-18' +last_updated: '2026-06-19' authors: - LightSpeed Team maintainer: LightSpeed Team diff --git a/docs/WORKFLOW_COORDINATION.md b/docs/WORKFLOW_COORDINATION.md index 30def397..993caa72 100644 --- a/docs/WORKFLOW_COORDINATION.md +++ b/docs/WORKFLOW_COORDINATION.md @@ -2,8 +2,8 @@ title: "Workflow Coordination Patterns" description: "Canonical reference for GitHub Actions workflow patterns: always-run vs. agent-triggered, coordination between agents and workflows, and orchestration strategies." created_date: "2026-05-28" -last_updated: "2026-06-18" -version: "v1.1.2" +last_updated: "2026-06-19" +version: "v1.1.3" file_type: "documentation" maintainer: "LightSpeed Team" tags: ["workflows", "automation", "agents", "coordination", "ci-cd"]