Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .copilot/skills/init-mode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
```
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
98 changes: 98 additions & 0 deletions .github/workflows/issue-labels-enforce-unique.yml
Original file line number Diff line number Diff line change
@@ -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@v6

- name: Enforce one label per category
uses: actions/github-script@v8
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;
}
Comment thread
EMaher marked this conversation as resolved.

// 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(', ')}.`
});
129 changes: 129 additions & 0 deletions .github/workflows/issue-labels-migrate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: Migrate Legacy Labels

on:
workflow_dispatch:

permissions:
issues: write

jobs:
migrate:
runs-on: ubuntu-latest
steps:
- name: "Rename legacy labels to type: equivalents"
uses: actions/github-script@v8
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' }
];

function isAlreadyExistsError(err) {
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) {
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({
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 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}`);
}
}
}

core.info('Label migration complete');
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -85,14 +85,21 @@ jobs:
];

const TYPE_LABELS = [
{ name: 'type:feature', color: 'DDD1F2', description: 'New capability' },
{ 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:feature', color: 'DDD1F2', description: 'New capability' },
{ name: 'type:enhancement', color: 'A2EEEF', description: 'Improvement to existing functionality' },
{ 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 = [
{ 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 = [
Expand All @@ -104,7 +111,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' }
];

Expand Down Expand Up @@ -144,10 +150,11 @@ 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);
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/squad-heartbeat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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');
Expand Down Expand Up @@ -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: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/squad-issue-assign.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading