From 06a44886a819663e806f2a3cf5dfde6b7e02cd79 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 14:35:53 -0700 Subject: [PATCH 01/15] refactor: rename label workflows and add generic uniqueness enforcement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename sync-squad-labels.yml → issue-labels-sync.yml - Delete squad-label-enforce.yml (hardcoded per-category logic) - Add issue-labels-enforce-unique.yml: generic enforcement for any category:value labels, reads sync file for priority ordering, exempts squad:* labels - Update doc references in 3 files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .copilot/skills/init-mode/SKILL.md | 2 +- .../workflows/issue-labels-enforce-unique.yml | 98 ++++++++++ ...squad-labels.yml => issue-labels-sync.yml} | 0 .github/workflows/squad-label-enforce.yml | 181 ------------------ .squad/templates/skills/init-mode/SKILL.md | 2 +- .squad/templates/squad.agent.md.template | 4 +- 6 files changed, 102 insertions(+), 185 deletions(-) create mode 100644 .github/workflows/issue-labels-enforce-unique.yml rename .github/workflows/{sync-squad-labels.yml => issue-labels-sync.yml} (100%) delete mode 100644 .github/workflows/squad-label-enforce.yml diff --git a/.copilot/skills/init-mode/SKILL.md b/.copilot/skills/init-mode/SKILL.md index 4dce6628..5c8ed7e7 100644 --- a/.copilot/skills/init-mode/SKILL.md +++ b/.copilot/skills/init-mode/SKILL.md @@ -58,7 +58,7 @@ No team exists yet. Propose one — but **DO NOT create any files until the user **Seeding:** Each agent's `history.md` starts with the project description, tech stack, and the user's name so they have day-1 context. Agent folder names are the cast name in lowercase (e.g., `.squad/agents/ripley/`). The Scribe's charter includes maintaining `decisions.md` and cross-agent context sharing. -**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `sync-squad-labels.yml`) for label automation. If the header is missing or titled differently, label routing breaks. +**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `issue-labels-sync.yml`) for label automation. If the header is missing or titled differently, label routing breaks. **Merge driver for append-only files:** Create or update `.gitattributes` at the repo root to enable conflict-free merging of `.squad/` state across branches: ``` diff --git a/.github/workflows/issue-labels-enforce-unique.yml b/.github/workflows/issue-labels-enforce-unique.yml new file mode 100644 index 00000000..3eaf2c4b --- /dev/null +++ b/.github/workflows/issue-labels-enforce-unique.yml @@ -0,0 +1,98 @@ +name: Enforce Unique Category Labels + +on: + issues: + types: [labeled] + +permissions: + issues: write + contents: read + +jobs: + enforce: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Enforce one label per category + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const issue = context.payload.issue; + const appliedLabel = context.payload.label.name; + + // Only handle "category:value" labels + const match = appliedLabel.match(/^([^:]+):/); + if (!match) { + core.info(`Label "${appliedLabel}" has no category prefix — skipping`); + return; + } + + const category = match[1]; + + // squad: labels are exempt from uniqueness enforcement + if (category === 'squad') { + core.info(`squad: labels are exempt — skipping`); + return; + } + + // Collect all labels in the same category currently on the issue + const allLabels = issue.labels.map(l => l.name); + const sameCategory = allLabels.filter(l => l.startsWith(category + ':')); + + if (sameCategory.length <= 1) { + core.info(`Only one "${category}:" label present — nothing to enforce`); + return; + } + + // Read the sync workflow to determine canonical label ordering. + // Labels listed earlier in issue-labels-sync.yml have higher priority. + const syncPath = '.github/workflows/issue-labels-sync.yml'; + let labelOrder = []; + if (fs.existsSync(syncPath)) { + const syncContent = fs.readFileSync(syncPath, 'utf8'); + const nameRegex = /name:\s*'([^']+)'/g; + let m; + while ((m = nameRegex.exec(syncContent)) !== null) { + if (m[1].startsWith(category + ':')) { + labelOrder.push(m[1]); + } + } + } + + // Determine winner: first label (by sync-file order) that is on the issue. + // If the category isn't in the sync file, fall back to keeping whichever + // label appeared first in the issue's current label list. + let winner = null; + if (labelOrder.length > 0) { + for (const ordered of labelOrder) { + if (sameCategory.includes(ordered)) { + winner = ordered; + break; + } + } + } + if (!winner) { + winner = sameCategory[0]; + } + + const toRemove = sameCategory.filter(l => l !== winner); + core.info(`Conflict in "${category}:" — keeping "${winner}", removing ${toRemove.join(', ')}`); + + for (const label of toRemove) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: label + }); + core.info(`Removed: ${label}`); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `🏷️ Label conflict resolved in \`${category}:\` — kept \`${winner}\`, removed ${toRemove.map(l => '`' + l + '`').join(', ')}.` + }); diff --git a/.github/workflows/sync-squad-labels.yml b/.github/workflows/issue-labels-sync.yml similarity index 100% rename from .github/workflows/sync-squad-labels.yml rename to .github/workflows/issue-labels-sync.yml diff --git a/.github/workflows/squad-label-enforce.yml b/.github/workflows/squad-label-enforce.yml deleted file mode 100644 index 633d220d..00000000 --- a/.github/workflows/squad-label-enforce.yml +++ /dev/null @@ -1,181 +0,0 @@ -name: Squad Label Enforce - -on: - issues: - types: [labeled] - -permissions: - issues: write - contents: read - -jobs: - enforce: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Enforce mutual exclusivity - uses: actions/github-script@v7 - with: - script: | - const issue = context.payload.issue; - const appliedLabel = context.payload.label.name; - - // Namespaces with mutual exclusivity rules - const EXCLUSIVE_PREFIXES = ['go:', 'release:', 'type:', 'priority:']; - - // Skip if not a managed namespace label - if (!EXCLUSIVE_PREFIXES.some(p => appliedLabel.startsWith(p))) { - core.info(`Label ${appliedLabel} is not in a managed namespace — skipping`); - return; - } - - const allLabels = issue.labels.map(l => l.name); - - // Handle go: namespace (mutual exclusivity) - if (appliedLabel.startsWith('go:')) { - const otherGoLabels = allLabels.filter(l => - l.startsWith('go:') && l !== appliedLabel - ); - - if (otherGoLabels.length > 0) { - // Remove conflicting go: labels - for (const label of otherGoLabels) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - name: label - }); - core.info(`Removed conflicting label: ${label}`); - } - - // Post update comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `🏷️ Triage verdict updated → \`${appliedLabel}\`` - }); - } - - // Auto-apply release:backlog if go:yes and no release target - if (appliedLabel === 'go:yes') { - const hasReleaseLabel = allLabels.some(l => l.startsWith('release:')); - if (!hasReleaseLabel) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: ['release:backlog'] - }); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `📋 Marked as \`release:backlog\` — assign a release target when ready.` - }); - - core.info('Applied release:backlog for go:yes issue'); - } - } - - // Remove release: labels if go:no - if (appliedLabel === 'go:no') { - const releaseLabels = allLabels.filter(l => l.startsWith('release:')); - if (releaseLabels.length > 0) { - for (const label of releaseLabels) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - name: label - }); - core.info(`Removed release label from go:no issue: ${label}`); - } - } - } - } - - // Handle release: namespace (mutual exclusivity) - if (appliedLabel.startsWith('release:')) { - const otherReleaseLabels = allLabels.filter(l => - l.startsWith('release:') && l !== appliedLabel - ); - - if (otherReleaseLabels.length > 0) { - // Remove conflicting release: labels - for (const label of otherReleaseLabels) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - name: label - }); - core.info(`Removed conflicting label: ${label}`); - } - - // Post update comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `🏷️ Release target updated → \`${appliedLabel}\`` - }); - } - } - - // Handle type: namespace (mutual exclusivity) - if (appliedLabel.startsWith('type:')) { - const otherTypeLabels = allLabels.filter(l => - l.startsWith('type:') && l !== appliedLabel - ); - - if (otherTypeLabels.length > 0) { - for (const label of otherTypeLabels) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - name: label - }); - core.info(`Removed conflicting label: ${label}`); - } - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `🏷️ Issue type updated → \`${appliedLabel}\`` - }); - } - } - - // Handle priority: namespace (mutual exclusivity) - if (appliedLabel.startsWith('priority:')) { - const otherPriorityLabels = allLabels.filter(l => - l.startsWith('priority:') && l !== appliedLabel - ); - - if (otherPriorityLabels.length > 0) { - for (const label of otherPriorityLabels) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - name: label - }); - core.info(`Removed conflicting label: ${label}`); - } - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `🏷️ Priority updated → \`${appliedLabel}\`` - }); - } - } - - core.info(`Label enforcement complete for ${appliedLabel}`); diff --git a/.squad/templates/skills/init-mode/SKILL.md b/.squad/templates/skills/init-mode/SKILL.md index f0a7831a..3f4bf997 100644 --- a/.squad/templates/skills/init-mode/SKILL.md +++ b/.squad/templates/skills/init-mode/SKILL.md @@ -58,7 +58,7 @@ No team exists yet. Propose one — but **DO NOT create any files until the user **Seeding:** Each agent's `history.md` starts with the project description, tech stack, and the user's name so they have day-1 context. Agent folder names are the cast name in lowercase (e.g., `.squad/agents/ripley/`). The Scribe's charter includes maintaining `decisions.md` and cross-agent context sharing. -**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `sync-squad-labels.yml`) for label automation. If the header is missing or titled differently, label routing breaks. +**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `issue-labels-sync.yml`) for label automation. If the header is missing or titled differently, label routing breaks. **Merge driver for append-only files:** Create or update `.gitattributes` at the repo root to enable conflict-free merging of `.squad/` state across branches: ``` diff --git a/.squad/templates/squad.agent.md.template b/.squad/templates/squad.agent.md.template index 1e7cf934..f2c1a57a 100644 --- a/.squad/templates/squad.agent.md.template +++ b/.squad/templates/squad.agent.md.template @@ -92,7 +92,7 @@ No team exists yet. Propose one — but **DO NOT create any files until the user **Seeding:** Each agent's `history.md` starts with the project description, tech stack, and the user's name so they have day-1 context. Agent folder names are the cast name in lowercase (e.g., `.squad/agents/ripley/`). The Scribe's charter includes maintaining `decisions.md` and cross-agent context sharing. Rai's charter is seeded from the `Rai-charter.md` template, and `.squad/rai/policy.md` is seeded from `rai-policy.md`. -**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `sync-squad-labels.yml`) for label automation. If the header is missing or titled differently, label routing breaks. +**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `issue-labels-sync.yml`) for label automation. If the header is missing or titled differently, label routing breaks. **Merge driver for append-only files:** Create or update `.gitattributes` at the repo root to enable conflict-free merging of `.squad/` state across branches: ``` @@ -186,7 +186,7 @@ For each squad member with assigned issues, note them in the session context. Wh **Proactive issue pickup:** If a user starts a session and there are open `squad:{member}` issues, mention them: *"Hey {user}, {AgentName} has an open issue — #42: Fix auth endpoint timeout. Want them to pick it up?"* -**Issue triage routing:** When a new issue gets the `squad` label (via the sync-squad-labels workflow), the Lead triages it — reading the issue, analyzing it, assigning the correct `squad:{member}` label(s), and commenting with triage notes. The Lead can also reassign by swapping labels. +**Issue triage routing:** When a new issue gets the `squad` label (via the issue-labels-sync workflow), the Lead triages it — reading the issue, analyzing it, assigning the correct `squad:{member}` label(s), and commenting with triage notes. The Lead can also reassign by swapping labels. **⚡ Read `.squad/team.md` (roster), `.squad/routing.md` (routing), and `.squad/casting/registry.json` (persistent names) as parallel tool calls in a single turn. Do NOT read these sequentially.** From 8e791913e58f59e9ea51e96b1ff42aaa124f5f11 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 14:57:16 -0700 Subject: [PATCH 02/15] feat: add close: label group, type:enhancement, and legacy label migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CLOSE_LABELS: close:fixed, close:wont-fix, close:duplicate, close:question-answered - Add type:enhancement to TYPE_LABELS - Remove standalone 'bug' from SIGNAL_LABELS (replaced by type:bug) - Add migration step to rename legacy labels (bug, question, enhancement) to their type: equivalents — rename preserves issue associations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/issue-labels-sync.yml | 43 +++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-labels-sync.yml b/.github/workflows/issue-labels-sync.yml index 6f6d0ae9..d216adf1 100644 --- a/.github/workflows/issue-labels-sync.yml +++ b/.github/workflows/issue-labels-sync.yml @@ -86,6 +86,7 @@ jobs: const TYPE_LABELS = [ { name: 'type:feature', color: 'DDD1F2', description: 'New capability' }, + { name: 'type:enhancement', color: 'A2EEEF', description: 'Improvement to existing functionality' }, { name: 'type:bug', color: 'FF0422', description: 'Something broken' }, { name: 'type:question', color: 'D876E3', description: 'Questions about usage or behavior' }, { name: 'type:documentation', color: '0075CA', description: 'Documentation issues or requests' }, @@ -95,6 +96,13 @@ jobs: { name: 'type:epic', color: 'CC4455', description: 'Parent issue that decomposes into sub-issues' } ]; + const CLOSE_LABELS = [ + { name: 'close:fixed', color: '0E8A16', description: 'Fixed by a previous PR or release' }, + { name: 'close:wont-fix', color: 'FFFFFF', description: 'Will not be addressed' }, + { name: 'close:duplicate', color: 'CFD3D7', description: 'Duplicate of another issue' }, + { name: 'close:question-answered', color: 'D876E3', description: 'Question has been answered' } + ]; + const EFFORT_LABELS = [ { name: 'effort:S', color: '0E8A16', description: 'Small effort (< 1 day)' }, { name: 'effort:M', color: 'FBCA04', description: 'Medium effort (1-3 days)' }, @@ -104,7 +112,6 @@ jobs: // High-signal labels — these MUST visually dominate all others const SIGNAL_LABELS = [ - { name: 'bug', color: 'FF0422', description: 'Something isn\'t working' }, { name: 'feedback', color: '00E5FF', description: 'User feedback — high signal, needs attention' } ]; @@ -144,15 +151,47 @@ jobs: }); } - // Add go:, release:, type:, effort:, priority:, high-signal, and lifecycle labels + // Add go:, release:, type:, close:, effort:, priority:, high-signal, and lifecycle labels labels.push(...GO_LABELS); labels.push(...RELEASE_LABELS); labels.push(...TYPE_LABELS); + labels.push(...CLOSE_LABELS); labels.push(...EFFORT_LABELS); labels.push(...PRIORITY_LABELS); labels.push(...SIGNAL_LABELS); labels.push(...LIFECYCLE_LABELS); + // Migrate legacy labels → type: equivalents (rename preserves issue associations) + const MIGRATIONS = [ + { from: 'bug', to: 'type:bug' }, + { from: 'question', to: 'type:question' }, + { from: 'enhancement', to: 'type:enhancement' } + ]; + + for (const { from, to } of MIGRATIONS) { + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: from + }); + // Old label exists — rename it (this moves all issues to the new name) + await github.rest.issues.updateLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: from, + new_name: to + }); + core.info(`Migrated label: ${from} → ${to}`); + } catch (err) { + if (err.status === 404) { + core.info(`Legacy label "${from}" not found — no migration needed`); + } else { + core.warning(`Failed to migrate ${from}: ${err.message}`); + } + } + } + // Sync labels (create or update) for (const label of labels) { try { From 1e7b287edc738adc1bd9f14b3480cf878bcefc92 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 14:59:58 -0700 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20add=20documentation=20=E2=86=92?= =?UTF-8?q?=20type:documentation=20label=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/issue-labels-sync.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-labels-sync.yml b/.github/workflows/issue-labels-sync.yml index d216adf1..1992f0bd 100644 --- a/.github/workflows/issue-labels-sync.yml +++ b/.github/workflows/issue-labels-sync.yml @@ -165,7 +165,8 @@ jobs: const MIGRATIONS = [ { from: 'bug', to: 'type:bug' }, { from: 'question', to: 'type:question' }, - { from: 'enhancement', to: 'type:enhancement' } + { from: 'enhancement', to: 'type:enhancement' }, + { from: 'documentation', to: 'type:documentation' } ]; for (const { from, to } of MIGRATIONS) { From 2c3d700322a00f75fb44ad22790b1cc53f5060b8 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 15:01:24 -0700 Subject: [PATCH 04/15] fix: run label migration after sync to ensure targets exist Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/issue-labels-sync.yml | 65 +++++++++++++------------ 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/.github/workflows/issue-labels-sync.yml b/.github/workflows/issue-labels-sync.yml index 1992f0bd..dba1581c 100644 --- a/.github/workflows/issue-labels-sync.yml +++ b/.github/workflows/issue-labels-sync.yml @@ -161,38 +161,6 @@ jobs: labels.push(...SIGNAL_LABELS); labels.push(...LIFECYCLE_LABELS); - // Migrate legacy labels → type: equivalents (rename preserves issue associations) - const MIGRATIONS = [ - { from: 'bug', to: 'type:bug' }, - { from: 'question', to: 'type:question' }, - { from: 'enhancement', to: 'type:enhancement' }, - { from: 'documentation', to: 'type:documentation' } - ]; - - for (const { from, to } of MIGRATIONS) { - try { - await github.rest.issues.getLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: from - }); - // Old label exists — rename it (this moves all issues to the new name) - await github.rest.issues.updateLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: from, - new_name: to - }); - core.info(`Migrated label: ${from} → ${to}`); - } catch (err) { - if (err.status === 404) { - core.info(`Legacy label "${from}" not found — no migration needed`); - } else { - core.warning(`Failed to migrate ${from}: ${err.message}`); - } - } - } - // Sync labels (create or update) for (const label of labels) { try { @@ -228,3 +196,36 @@ jobs: } core.info(`Label sync complete: ${labels.length} labels synced`); + + // Migrate legacy labels → type: equivalents AFTER sync ensures targets exist. + // Rename preserves issue associations. + const MIGRATIONS = [ + { from: 'bug', to: 'type:bug' }, + { from: 'question', to: 'type:question' }, + { from: 'enhancement', to: 'type:enhancement' }, + { from: 'documentation', to: 'type:documentation' } + ]; + + for (const { from, to } of MIGRATIONS) { + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: from + }); + // Old label exists — rename it (this moves all issues to the new name) + await github.rest.issues.updateLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: from, + new_name: to + }); + core.info(`Migrated label: ${from} → ${to}`); + } catch (err) { + if (err.status === 404) { + core.info(`Legacy label "${from}" not found — no migration needed`); + } else { + core.warning(`Failed to migrate ${from}: ${err.message}`); + } + } + } From 12e9d5112d2631da40bd635a1cf688c562ab1df8 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 15:04:50 -0700 Subject: [PATCH 05/15] refactor: move label migration to its own daily workflow Extracts migration logic into issue-labels-migrate.yml (runs daily at 07:00 UTC). Removes migration from issue-labels-sync.yml. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/issue-labels-migrate.yml | 52 ++++++++++++++++++++++ .github/workflows/issue-labels-sync.yml | 33 -------------- 2 files changed, 52 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/issue-labels-migrate.yml diff --git a/.github/workflows/issue-labels-migrate.yml b/.github/workflows/issue-labels-migrate.yml new file mode 100644 index 00000000..627a9177 --- /dev/null +++ b/.github/workflows/issue-labels-migrate.yml @@ -0,0 +1,52 @@ +name: Migrate Legacy Labels + +on: + schedule: + - cron: '0 7 * * *' # Daily at 07:00 UTC (runs before label sync at 08:00) + workflow_dispatch: + +permissions: + issues: write + +jobs: + migrate: + runs-on: ubuntu-latest + steps: + - name: Rename legacy labels to type: equivalents + uses: actions/github-script@v7 + with: + script: | + // Legacy label → canonical type: label. + // Rename preserves all issue associations automatically. + const MIGRATIONS = [ + { from: 'bug', to: 'type:bug' }, + { from: 'question', to: 'type:question' }, + { from: 'enhancement', to: 'type:enhancement' }, + { from: 'documentation', to: 'type:documentation' } + ]; + + for (const { from, to } of MIGRATIONS) { + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: from + }); + // Old label exists — rename it + await github.rest.issues.updateLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: from, + new_name: to + }); + core.info(`Migrated label: ${from} → ${to}`); + } catch (err) { + if (err.status === 404) { + core.info(`Legacy label "${from}" not found — no migration needed`); + } else { + core.warning(`Failed to migrate ${from}: ${err.message}`); + } + } + } + + core.info('Label migration complete'); diff --git a/.github/workflows/issue-labels-sync.yml b/.github/workflows/issue-labels-sync.yml index dba1581c..c83aa855 100644 --- a/.github/workflows/issue-labels-sync.yml +++ b/.github/workflows/issue-labels-sync.yml @@ -196,36 +196,3 @@ jobs: } core.info(`Label sync complete: ${labels.length} labels synced`); - - // Migrate legacy labels → type: equivalents AFTER sync ensures targets exist. - // Rename preserves issue associations. - const MIGRATIONS = [ - { from: 'bug', to: 'type:bug' }, - { from: 'question', to: 'type:question' }, - { from: 'enhancement', to: 'type:enhancement' }, - { from: 'documentation', to: 'type:documentation' } - ]; - - for (const { from, to } of MIGRATIONS) { - try { - await github.rest.issues.getLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: from - }); - // Old label exists — rename it (this moves all issues to the new name) - await github.rest.issues.updateLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: from, - new_name: to - }); - core.info(`Migrated label: ${from} → ${to}`); - } catch (err) { - if (err.status === 404) { - core.info(`Legacy label "${from}" not found — no migration needed`); - } else { - core.warning(`Failed to migrate ${from}: ${err.message}`); - } - } - } From b34a09e3249df5546303bad01a5ecc9ba318c0d7 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 22:20:28 +0000 Subject: [PATCH 06/15] chore: trigger issue-labels-sync workflow From 62916ad0c1b4eb8cdf2cfb62ba4a05de8a9014e3 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 22:21:30 +0000 Subject: [PATCH 07/15] chore: trigger issue-labels-sync via team roster touch --- .squad/team.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.squad/team.md b/.squad/team.md index f4088c90..3fb793f4 100644 --- a/.squad/team.md +++ b/.squad/team.md @@ -65,3 +65,4 @@ - **Project:** apiops-cli - **Created:** 2026-04-07 + From 8ee8ab51cca0e5ace12048ab246df1b4084cca68 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 15:23:12 -0700 Subject: [PATCH 08/15] fix: quote YAML step name containing colon Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/issue-labels-migrate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-labels-migrate.yml b/.github/workflows/issue-labels-migrate.yml index 627a9177..c22a07e4 100644 --- a/.github/workflows/issue-labels-migrate.yml +++ b/.github/workflows/issue-labels-migrate.yml @@ -12,7 +12,7 @@ jobs: migrate: runs-on: ubuntu-latest steps: - - name: Rename legacy labels to type: equivalents + - name: "Rename legacy labels to type: equivalents" uses: actions/github-script@v7 with: script: | From de550d43808d17cd1e0abf3e546a56b7bcf9f956 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 22:25:38 +0000 Subject: [PATCH 09/15] Revert "chore: trigger issue-labels-sync via team roster touch" This reverts commit 62916ad0c1b4eb8cdf2cfb62ba4a05de8a9014e3. --- .squad/team.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.squad/team.md b/.squad/team.md index 3fb793f4..f4088c90 100644 --- a/.squad/team.md +++ b/.squad/team.md @@ -65,4 +65,3 @@ - **Project:** apiops-cli - **Created:** 2026-04-07 - From 44f6637dfd59e233eb3ed85295603825cc4efa29 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 22:40:10 +0000 Subject: [PATCH 10/15] fix: make issue-labels-migrate idempotent when target labels already exist --- .github/workflows/issue-labels-migrate.yml | 70 ++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/.github/workflows/issue-labels-migrate.yml b/.github/workflows/issue-labels-migrate.yml index c22a07e4..78eb5e4e 100644 --- a/.github/workflows/issue-labels-migrate.yml +++ b/.github/workflows/issue-labels-migrate.yml @@ -25,6 +25,57 @@ jobs: { from: 'documentation', to: 'type:documentation' } ]; + function isAlreadyExistsError(err) { + return err?.status === 422 + && Array.isArray(err?.errors) + && err.errors.some(e => e.code === 'already_exists' && e.field === 'name'); + } + + async function relabelItems(from, to) { + const items = await github.paginate(github.rest.issues.listForRepo, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'all', + labels: from, + per_page: 100 + }); + + if (items.length === 0) { + core.info(`No issues or PRs found with legacy label "${from}"`); + return; + } + + for (const item of items) { + const existing = (item.labels || []) + .map(l => (typeof l === 'string' ? l : l.name)) + .filter(Boolean); + + if (!existing.includes(to)) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: item.number, + labels: [to] + }); + } + + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: item.number, + name: from + }); + } catch (err) { + if (err.status !== 404) { + throw err; + } + } + } + + core.info(`Re-labeled ${items.length} issues/PRs: ${from} → ${to}`); + } + for (const { from, to } of MIGRATIONS) { try { await github.rest.issues.getLabel({ @@ -43,6 +94,25 @@ jobs: } catch (err) { if (err.status === 404) { core.info(`Legacy label "${from}" not found — no migration needed`); + } else if (isAlreadyExistsError(err)) { + core.info(`Target label "${to}" already exists; applying fallback migration for "${from}"`); + + await relabelItems(from, to); + + try { + await github.rest.issues.deleteLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: from + }); + core.info(`Deleted legacy label "${from}" after fallback migration`); + } catch (deleteErr) { + if (deleteErr.status === 404) { + core.info(`Legacy label "${from}" already removed`); + } else { + throw deleteErr; + } + } } else { core.warning(`Failed to migrate ${from}: ${err.message}`); } From 1d9ae7f00d9f84d14be388e13c3a68317b9ca489 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 22:42:02 +0000 Subject: [PATCH 11/15] fix: handle label already_exists errors robustly and use github-script v8 --- .github/workflows/issue-labels-migrate.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/issue-labels-migrate.yml b/.github/workflows/issue-labels-migrate.yml index 78eb5e4e..e0aa04c4 100644 --- a/.github/workflows/issue-labels-migrate.yml +++ b/.github/workflows/issue-labels-migrate.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Rename legacy labels to type: equivalents" - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | // Legacy label → canonical type: label. @@ -26,9 +26,18 @@ jobs: ]; function isAlreadyExistsError(err) { - return err?.status === 422 - && Array.isArray(err?.errors) - && err.errors.some(e => e.code === 'already_exists' && e.field === 'name'); + const errors = err?.errors + || err?.response?.data?.errors + || err?.data?.errors + || []; + + const hasTypedError = Array.isArray(errors) + && errors.some(e => e?.code === 'already_exists' && e?.field === 'name'); + + const message = String(err?.message || ''); + const messageHasAlreadyExists = message.includes('already_exists') && message.includes('Label'); + + return (err?.status === 422 && hasTypedError) || messageHasAlreadyExists; } async function relabelItems(from, to) { From bef5f16258b0528000d79f7ae734d448355781e0 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 22:47:42 +0000 Subject: [PATCH 12/15] fix: preserve legacy labels in issue-labels-migrate workflow --- .github/workflows/issue-labels-migrate.yml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/.github/workflows/issue-labels-migrate.yml b/.github/workflows/issue-labels-migrate.yml index e0aa04c4..0755e778 100644 --- a/.github/workflows/issue-labels-migrate.yml +++ b/.github/workflows/issue-labels-migrate.yml @@ -108,20 +108,7 @@ jobs: await relabelItems(from, to); - try { - await github.rest.issues.deleteLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: from - }); - core.info(`Deleted legacy label "${from}" after fallback migration`); - } catch (deleteErr) { - if (deleteErr.status === 404) { - core.info(`Legacy label "${from}" already removed`); - } else { - throw deleteErr; - } - } + core.info(`Retained legacy label "${from}" after fallback migration`); } else { core.warning(`Failed to migrate ${from}: ${err.message}`); } From 029de51ada2993bdfa68875e16c07eb83b9275b8 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 22:48:19 +0000 Subject: [PATCH 13/15] Revert "fix: preserve legacy labels in issue-labels-migrate workflow" This reverts commit bef5f16258b0528000d79f7ae734d448355781e0. --- .github/workflows/issue-labels-migrate.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-labels-migrate.yml b/.github/workflows/issue-labels-migrate.yml index 0755e778..e0aa04c4 100644 --- a/.github/workflows/issue-labels-migrate.yml +++ b/.github/workflows/issue-labels-migrate.yml @@ -108,7 +108,20 @@ jobs: await relabelItems(from, to); - core.info(`Retained legacy label "${from}" after fallback migration`); + try { + await github.rest.issues.deleteLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: from + }); + core.info(`Deleted legacy label "${from}" after fallback migration`); + } catch (deleteErr) { + if (deleteErr.status === 404) { + core.info(`Legacy label "${from}" already removed`); + } else { + throw deleteErr; + } + } } else { core.warning(`Failed to migrate ${from}: ${err.message}`); } From f5448a554aabe279dc34804b8e8b77f7e7cb99da Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 22:51:02 +0000 Subject: [PATCH 14/15] updating workflow to manual-only --- .github/workflows/issue-labels-migrate.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/issue-labels-migrate.yml b/.github/workflows/issue-labels-migrate.yml index e0aa04c4..bd4e4e4e 100644 --- a/.github/workflows/issue-labels-migrate.yml +++ b/.github/workflows/issue-labels-migrate.yml @@ -1,8 +1,6 @@ name: Migrate Legacy Labels on: - schedule: - - cron: '0 7 * * *' # Daily at 07:00 UTC (runs before label sync at 08:00) workflow_dispatch: permissions: From 6d1d93ef745ee012af4548cda635376853668cf1 Mon Sep 17 00:00:00 2001 From: Elizabeth Maher Date: Fri, 26 Jun 2026 23:09:52 +0000 Subject: [PATCH 15/15] updating actions to use newer versions. --- .github/workflows/ci.yml | 4 ++-- .github/workflows/codeql.yml | 2 +- .github/workflows/issue-labels-enforce-unique.yml | 4 ++-- .github/workflows/issue-labels-sync.yml | 13 ++++++------- .github/workflows/squad-heartbeat.yml | 6 +++--- .github/workflows/squad-issue-assign.yml | 4 ++-- .github/workflows/squad-preview.yml | 4 ++-- .github/workflows/squad-release.yml | 4 ++-- 8 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bab75d5..ee39a479 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,9 +13,9 @@ jobs: name: Run APIOps Tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: '22' cache: 'npm' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e87f2223..604a5bb8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -57,7 +57,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` diff --git a/.github/workflows/issue-labels-enforce-unique.yml b/.github/workflows/issue-labels-enforce-unique.yml index 3eaf2c4b..096ba965 100644 --- a/.github/workflows/issue-labels-enforce-unique.yml +++ b/.github/workflows/issue-labels-enforce-unique.yml @@ -12,10 +12,10 @@ jobs: enforce: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Enforce one label per category - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const fs = require('fs'); diff --git a/.github/workflows/issue-labels-sync.yml b/.github/workflows/issue-labels-sync.yml index c83aa855..e7a7778b 100644 --- a/.github/workflows/issue-labels-sync.yml +++ b/.github/workflows/issue-labels-sync.yml @@ -17,10 +17,10 @@ jobs: sync-labels: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Parse roster and sync labels - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const fs = require('fs'); @@ -85,15 +85,14 @@ jobs: ]; const TYPE_LABELS = [ + { name: 'type:bug', color: 'FF0422', description: 'Something broken' }, { name: 'type:feature', color: 'DDD1F2', description: 'New capability' }, { name: 'type:enhancement', color: 'A2EEEF', description: 'Improvement to existing functionality' }, - { name: 'type:bug', color: 'FF0422', description: 'Something broken' }, - { name: 'type:question', color: 'D876E3', description: 'Questions about usage or behavior' }, - { name: 'type:documentation', color: '0075CA', description: 'Documentation issues or requests' }, { name: 'type:spike', color: 'F2DDD4', description: 'Research/investigation — produces a plan, not code' }, - { name: 'type:docs', color: 'D4E5F7', description: 'Documentation work' }, { name: 'type:chore', color: 'D4E5F7', description: 'Maintenance, refactoring, cleanup' }, - { name: 'type:epic', color: 'CC4455', description: 'Parent issue that decomposes into sub-issues' } + { name: 'type:epic', color: 'CC4455', description: 'Parent issue that decomposes into sub-issues' }, + { name: 'type:documentation', color: '0075CA', description: 'Documentation issues or requests' }, + { name: 'type:question', color: 'D876E3', description: 'Questions about usage or behavior' } ]; const CLOSE_LABELS = [ diff --git a/.github/workflows/squad-heartbeat.yml b/.github/workflows/squad-heartbeat.yml index 5494296f..5a70db11 100644 --- a/.github/workflows/squad-heartbeat.yml +++ b/.github/workflows/squad-heartbeat.yml @@ -25,7 +25,7 @@ jobs: heartbeat: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check triage script id: check-script @@ -48,7 +48,7 @@ jobs: - name: Ralph — Apply triage decisions if: steps.check-script.outputs.has_script == 'true' && hashFiles('triage-results.json') != '' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const fs = require('fs'); @@ -100,7 +100,7 @@ jobs: # Copilot auto-assign step (uses PAT if available) - name: Ralph — Assign @copilot issues if: success() - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/squad-issue-assign.yml b/.github/workflows/squad-issue-assign.yml index 022aab99..ddce206b 100644 --- a/.github/workflows/squad-issue-assign.yml +++ b/.github/workflows/squad-issue-assign.yml @@ -57,7 +57,7 @@ jobs: core.info(`Verified ${sender} as a ${permission.permission} collaborator`); - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # Step 1: Assign issue to the maintainer who applied "go:yes" - name: Assign to label sender @@ -233,7 +233,7 @@ jobs: needs: assign-issue runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Verify assignment integrity uses: actions/github-script@v8 diff --git a/.github/workflows/squad-preview.yml b/.github/workflows/squad-preview.yml index 9df39e07..a69d324b 100644 --- a/.github/workflows/squad-preview.yml +++ b/.github/workflows/squad-preview.yml @@ -11,9 +11,9 @@ jobs: validate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22 diff --git a/.github/workflows/squad-release.yml b/.github/workflows/squad-release.yml index f04d85f0..d9271919 100644 --- a/.github/workflows/squad-release.yml +++ b/.github/workflows/squad-release.yml @@ -11,11 +11,11 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22