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
3 changes: 3 additions & 0 deletions .github/workflows/labeling.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ jobs:
run: node scripts/validation/validate-issue-fields.cjs

- name: Sync labels with canonical set
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ github.event_name == 'pull_request' && 'true' || inputs.dry_run || 'false' }}
run: |
node scripts/agents/includes/label-sync.js
shell: bash
Expand Down
3 changes: 3 additions & 0 deletions docs/ISSUE_LABELS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ Colors are assigned by family and purpose; see `../.github/labels.yml` for mappi
## Automation

- **Labeling, status, type, and standardization** are all handled by the **unified agent and workflow** ([labeling.agent.js](../scripts/agents/labeling.agent.js), [labeling.yml](../.github/workflows/labeling.yml)).
- The label sync step (`scripts/agents/includes/label-sync.js`) now runs as an executable CLI in CI.
- On `pull_request` events it runs in dry-run mode.
- On other labeling workflow events it enforces canonical sync against `.github/labels.yml`.
- **Default labels** are applied and enforced on all issues.
- **Label conflicts and non-canonical labels** are removed or migrated automatically.

Expand Down
98 changes: 98 additions & 0 deletions scripts/agents/includes/label-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
// TODO: Align this helper with the latest automation spec updates.

import { findStandardLabel } from "./label-lookup.js";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import yaml from "js-yaml";
import github from "@actions/github";

/**
* Sync repository labels with canonical set.
Expand Down Expand Up @@ -435,3 +440,96 @@ export {
standardizeLabelsOnRepo,
generateSyncReport,
};

/**
* Load canonical label definitions from configured labels.yml file.
* @param {string} labelsPath - Path to labels.yml
* @returns {Promise<Array>} Canonical labels array
*/
async function loadCanonicalLabels(labelsPath) {
const raw = await fs.readFile(labelsPath, "utf8");
const parsed = yaml.load(raw);
if (!Array.isArray(parsed)) {
throw new Error(`${labelsPath} must be an array of labels`);
}
return parsed;
}

/**
* Parse a string env value to boolean.
* @param {string|undefined} value - Env value
* @returns {boolean} Parsed boolean
*/
function asBoolean(value) {
if (!value) return false;
return ["1", "true", "yes", "on"].includes(String(value).toLowerCase());
}

/**
* CLI runner for repository label sync.
* Reads context from GitHub Actions env and syncs labels against canonical config.
* Exits non-zero when sync/validation fails.
*/
async function runCli() {
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error("GITHUB_TOKEN is required to run label sync");
}

const labelsPath = process.env.LABELS_CONFIG || ".github/labels.yml";
const dryRun = asBoolean(process.env.DRY_RUN);

const owner =
process.env.GITHUB_REPOSITORY_OWNER || github.context.repo.owner;
const repo =
process.env.GITHUB_REPOSITORY?.split("/")[1] || github.context.repo.repo;
Comment on lines +482 to +485
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Accessing github.context.repo directly can throw an unhandled exception if the GITHUB_REPOSITORY environment variable is missing or malformed (for example, when running or testing the script locally). Since github.context.repo simply parses process.env.GITHUB_REPOSITORY under the hood, we can safely extract the owner and repository directly from the environment variables. This avoids potential runtime crashes and allows our custom error handling on line 487 to gracefully report the missing configuration.

Suggested change
const owner =
process.env.GITHUB_REPOSITORY_OWNER || github.context.repo.owner;
const repo =
process.env.GITHUB_REPOSITORY?.split("/")[1] || github.context.repo.repo;
const [repoOwner, repoName] = (process.env.GITHUB_REPOSITORY || "").split("/");
const owner = process.env.GITHUB_REPOSITORY_OWNER || repoOwner;
const repo = repoName;


if (!owner || !repo) {
throw new Error("Unable to resolve repository owner/name from environment");
}

const canonicalLabels = await loadCanonicalLabels(labelsPath);
const octokit = github.getOctokit(token);

const syncReport = await syncLabelsWithCanonical(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Paginate labels before enabling the CLI sync

The new CLI path now executes syncLabelsWithCanonical()/validateRepoLabels() in CI, but those helpers only request a single listLabelsForRepo page with per_page: 100; Octokit’s pagination docs note per_page tops out at 100. This repository’s .github/labels.yml already contains 149 canonical labels, so once a repo has the full canonical set the CLI only sees the first 100 existing labels, treats the rest as missing, and the validation step fails every labeling run even though the labels are present. Please fetch all pages via octokit.paginate(...) before validating or syncing.

Useful? React with 👍 / 👎.

octokit,
owner,
repo,
canonicalLabels,
dryRun,
);
const validationReport = await validateRepoLabels(
octokit,
owner,
repo,
canonicalLabels,
);

const markdown = generateSyncReport(syncReport, validationReport, null);
process.stdout.write(`${markdown}\n`);

if (!validationReport.valid && dryRun) {
console.warn(
`[label-sync] Dry-run detected label drift (missing=${validationReport.summary.missingCount}, extra=${validationReport.summary.extraCount}, nonCompliant=${validationReport.summary.nonCompliantCount})`,
);
return;
}

if (!validationReport.valid) {
throw new Error(
`Label validation failed (missing=${validationReport.summary.missingCount}, extra=${validationReport.summary.extraCount}, nonCompliant=${validationReport.summary.nonCompliantCount})`,
);
}
}

const isDirectRun =
process.argv[1] &&
path.resolve(fileURLToPath(import.meta.url)) ===
path.resolve(process.argv[1]);

if (isDirectRun) {
runCli().catch((error) => {
console.error(`[label-sync] ${error.message}`);
process.exit(1);
});
}
Loading