Skip to content

feat(labeling): harden canonical seeding with policy-gated cleanup#451

Merged
ashleyshaw merged 2 commits into
developfrom
codex/449-label-governance-hardening
May 27, 2026
Merged

feat(labeling): harden canonical seeding with policy-gated cleanup#451
ashleyshaw merged 2 commits into
developfrom
codex/449-label-governance-hardening

Conversation

@ashleyshaw
Copy link
Copy Markdown
Member

Summary

  • harden canonical label seeding runtime so workflow execution is deterministic
  • add non-destructive governance policy gate for orphan label cleanup pending 🏷️ Orphan Labels Detected (30) #95 decisions
  • extend validation and migration docs to enforce canonical label contract

Changes

  • add .github/label-governance-policy.yml with destructive cleanup gate (enabled: false default)
  • update scripts/agents/includes/label-sync.js:
    • add CLI runtime for workflow execution
    • defer deletes unless policy + explicit allowlist permit them
    • emit deferred-delete reporting
  • update scripts/validation/validate-labeling-configs.cjs:
    • validate governance policy schema
    • validate canonical label family prefixes
  • update .github/workflows/labeling.yml:
    • pass GITHUB_TOKEN to label sync
    • upload label sync report artifact
  • update docs/MIGRATION.md with seeding/cleanup gate rules

Validation

  • node scripts/validation/validate-labeling-configs.cjs
  • node scripts/agents/includes/check-template-labels.js
  • markdownlint-cli2 on updated markdown files

Gate

Closes #66
Refs #95
Refs #449

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

Warning

Review limit reached

@ashleyshaw, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 31 minutes and 8 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 5767a1ab-a3c5-4939-a994-0a68113096a3

📥 Commits

Reviewing files that changed from the base of the PR and between 75667bf and 5588826.

📒 Files selected for processing (9)
  • .github/label-governance-policy.yml
  • .github/projects/active/label-governance-stabilisation-2026-05-27/README.md
  • .github/projects/active/label-governance-stabilisation-2026-05-27/issues/children/batch-01-execution/01-01-task-canonical-labels-and-seeding-issue-66.md
  • .github/reports/audits/issue-95-orphan-labels-audit-2026-05-27.md
  • .github/workflows/labeling.yml
  • CHANGELOG.md
  • docs/MIGRATION.md
  • scripts/agents/includes/label-sync.js
  • scripts/validation/validate-labeling-configs.cjs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/449-label-governance-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Reviewer Summary for PR #451

CI Status:success
Files changed: 8

Recommendations

  • Ready to proceed pending human review

@ashleyshaw ashleyshaw added the meta:no-changelog No changelog needed label May 27, 2026
@github-actions github-actions Bot added area:ci Build and CI pipelines area:labels Label governance and routing area:documentation Docs & guides area:scripts Scripts & tooling lang:js JavaScript/TypeScript lang:md Markdown content/docs status:needs-review Awaiting code review priority:normal Default priority type:chore Chore / small hygiene change type:feature Feature or enhancement meta:needs-changelog Requires a changelog entry before merge labels May 27, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a label governance policy to manage canonical seeding and gate destructive orphan-label cleanup. It adds a new policy configuration file, updates documentation, and hardens the label synchronization script to defer deletions unless explicitly enabled and approved. It also updates the validation script to enforce canonical label prefixes and validate the new policy schema. The review feedback highlights several key improvements: ensuring cross-platform compatibility on Windows when checking the main module, failing fast in GitHub Actions if required environment variables are missing, fixing a validation bypass for string-based labels, and strictly validating that policy arrays contain only strings.

Comment on lines +570 to +575
if (import.meta.url === `file://${process.argv[1]}`) {
runCli().catch((error) => {
console.error(`[label-sync] ${error.message}`);
process.exit(1);
});
}
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.

high

The comparison of import.meta.url with a file URL constructed from process.argv[1] will fail on Windows because process.argv[1] uses backslashes and does not match the standard file URL format. Use fileURLToPath from the url module to perform a robust, cross-platform main module check.

Suggested change
if (import.meta.url === `file://${process.argv[1]}`) {
runCli().catch((error) => {
console.error(`[label-sync] ${error.message}`);
process.exit(1);
});
}
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
runCli().catch((error) => {
console.error("[label-sync] " + error.message);
process.exit(1);
});
}

Comment on lines +16 to +19
import fs from "fs";
import path from "path";
import yaml from "js-yaml";
import github from "@actions/github";
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

Import fileURLToPath from the built-in url module to enable a robust, cross-platform check for the main module execution.

Suggested change
import fs from "fs";
import path from "path";
import yaml from "js-yaml";
import github from "@actions/github";
import fs from "fs";
import path from "path";
import yaml from "js-yaml";
import github from "@actions/github";
import { fileURLToPath } from "url";

Comment on lines +509 to +514
if (!token || !repoSlug) {
console.log(
"[label-sync] Skipping sync: requires GITHUB_TOKEN and GITHUB_REPOSITORY.",
);
return;
}
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

If the script is executed in a GitHub Actions environment (where process.env.GITHUB_ACTIONS is true), missing required environment variables like GITHUB_TOKEN or GITHUB_REPOSITORY should fail the build rather than silently skipping. This prevents misconfigured workflows from passing silently.

Suggested change
if (!token || !repoSlug) {
console.log(
"[label-sync] Skipping sync: requires GITHUB_TOKEN and GITHUB_REPOSITORY.",
);
return;
}
if (!token || !repoSlug) {
if (process.env.GITHUB_ACTIONS === "true") {
throw new Error("Missing GITHUB_TOKEN or GITHUB_REPOSITORY in GitHub Actions environment.");
}
console.log(
"[label-sync] Skipping sync: requires GITHUB_TOKEN and GITHUB_REPOSITORY.",
);
return;
}

Comment on lines +46 to +53
const hasAllowedPrefix = allowedPrefixes.some((prefix) =>
item.name.startsWith(prefix),
);
if (!hasAllowedPrefix) {
fail(
`Label '${item.name}' must use a canonical family prefix (${allowedPrefixes.join(", ")})`,
);
}
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

The prefix validation is bypassed for string-based labels because of the early return on line 42 (which is outside the modified lines). If labels.yml contains simple string entries, they will not be validated against the allowedPrefixes list. Consider refactoring the validation loop to extract the label name first (supporting both string and object formats) and then applying the prefix check to all entries.

Comment on lines +114 to +126
if (
cleanup.approved_orphan_labels !== undefined &&
!Array.isArray(cleanup.approved_orphan_labels)
) {
fail("destructive_cleanup.approved_orphan_labels must be an array");
}

if (
cleanup.never_delete_labels !== undefined &&
!Array.isArray(cleanup.never_delete_labels)
) {
fail("destructive_cleanup.never_delete_labels must be an array");
}
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

The validation for approved_orphan_labels and never_delete_labels only checks if they are arrays, but does not verify that the elements inside the arrays are strings. If non-string values are accidentally added, they will be silently filtered out by asStringSet in the sync script. It is safer to fail the validation if any element is not a string.

  if (
    cleanup.approved_orphan_labels !== undefined &&
    (!Array.isArray(cleanup.approved_orphan_labels) || !cleanup.approved_orphan_labels.every(item => typeof item === "string"))
  ) {
    fail("destructive_cleanup.approved_orphan_labels must be an array of strings");
  }

  if (
    cleanup.never_delete_labels !== undefined &&
    (!Array.isArray(cleanup.never_delete_labels) || !cleanup.never_delete_labels.every(item => typeof item === "string"))
  ) {
    fail("destructive_cleanup.never_delete_labels must be an array of strings");
  }

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5dee4bf970

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +76 to +78
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LABEL_SYNC_REPORT_PATH: .github/reports/labeling/label-sync-${{ github.run_id }}.md
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honour dry_run for label sync

When this workflow is manually dispatched with the default dry_run=true, this step now gives the new label-sync CLI a write token but never maps inputs.dry_run to LABEL_SYNC_DRY_RUN (the script only treats LABEL_SYNC_DRY_RUN === "true" as dry-run). As a result a dry-run dispatch can still create or update repository labels before the later labeling agent runs in dry-run mode; please wire the input through here or make the CLI honour the existing DRY_RUN variable.

Useful? React with 👍 / 👎.

Comment on lines +562 to +567
console.log(
`[label-sync] Created ${syncReport.created.length}, updated ${syncReport.updated.length}, deleted ${syncReport.deleted.length}, deferred ${syncReport.deferredDeletes.length}`,
);
console.log(
`[label-sync] Deletion mode: ${deletionMode} (policy + env gate)`,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fail when label sync records API errors

When any create/update API request fails, syncLabelsWithCanonical catches the exception and stores it in syncReport.errors, but this CLI path still writes the report, logs counts, and exits 0. In contexts such as a read-only pull_request token or a rejected label update, .github/workflows/labeling.yml has continue-on-error: false but the job can still go green without actually seeding the canonical labels; please throw when sync errors are present before exiting.

Useful? React with 👍 / 👎.

@ashleyshaw ashleyshaw force-pushed the codex/449-label-governance-hardening branch from 4ae5e74 to 5588826 Compare May 27, 2026 21:07
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Reviewer Summary for PR #451

CI Status:success
Files changed: 9

Recommendations

  • Ready to proceed pending human review

@github-actions github-actions Bot removed the type:chore Chore / small hygiene change label May 27, 2026
@ashleyshaw ashleyshaw merged commit 13170e2 into develop May 27, 2026
12 checks passed
@ashleyshaw ashleyshaw deleted the codex/449-label-governance-hardening branch May 27, 2026 21:08
@ashleyshaw ashleyshaw removed the meta:no-changelog No changelog needed label May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ci Build and CI pipelines area:documentation Docs & guides area:labels Label governance and routing area:scripts Scripts & tooling lang:js JavaScript/TypeScript lang:md Markdown content/docs meta:needs-changelog Requires a changelog entry before merge priority:normal Default priority status:needs-review Awaiting code review type:feature Feature or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Workflows/Docs] Canonical labels + org seeding workflow

1 participant