From af0f002e14a08cdbfd879c1183bfe7eb2604bce9 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Fri, 21 Aug 2026 12:42:28 -0400 Subject: [PATCH] ci: expand labeler globs and skip dependabot pr policy (PLAT-107) (#124) * ci: expand labeler globs and skip dependabot pr policy .NET product paths never matched app, so backend PRs stayed unlabeled. Dependabot PRs still ran commit-subject and pin checks on generated titles. Skip those PRs in the reusable policy job. * fix(labeler): match nested elastic beanstalk config paths Root-only .ebextensions and .platform globs miss api/.ebextensions in monorepos. Mirror the Dockerfile nested form. --- .github/PULL_REQUEST_TEMPLATE.md | 4 +- .github/workflows/callable-labeler.yaml | 18 ++++---- .github/workflows/callable-pr-policy.yaml | 50 +++++++++++++++-------- README.md | 6 +-- test/pr-policy.test.mjs | 30 +++++--------- 5 files changed, 61 insertions(+), 47 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4d3359a..c4a2d58 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,8 +4,8 @@ PR conventions - type ∈ feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, release - Maximum 120 characters, including the Jira suffix. - Active Jira projects: DEV (product), PLAT (platform), SEC (security). INFRA is a closed archive. - - The Jira key is required at the end of the title in parentheses. - - Jira-exempt only: Dependabot PRs and permission-controlled emergency reverts. + - Put the Jira key at the end of the title in parentheses. A missing key is a warning, not a failure. + - Dependabot-authored PRs skip the policy gate. Authorized emergency reverts suppress the missing-key warning. - Branch: feature/, fix/, hotfix/, chore/, docs/, refactor/, release/ + kebab-case description. Branch names do not contain Jira keys. - Scope: one logical change per PR. If the title needs "and", split it. diff --git a/.github/workflows/callable-labeler.yaml b/.github/workflows/callable-labeler.yaml index 014c016..e0d0f21 100644 --- a/.github/workflows/callable-labeler.yaml +++ b/.github/workflows/callable-labeler.yaml @@ -53,6 +53,12 @@ jobs: - '**/template.yaml' - 'samconfig.toml' - 'infra/**' + - 'Dockerfile' + - '**/Dockerfile' + - '.ebextensions/**' + - '**/.ebextensions/**' + - '.platform/**' + - '**/.platform/**' app: - changed-files: - any-glob-to-any-file: @@ -64,6 +70,9 @@ jobs: - 'web/**' - 'mobile/**' - 'shared/**' + - '**/*.cs' + - '**/*.cshtml' + - '**/*.razor' content: - changed-files: - any-glob-to-any-file: @@ -98,12 +107,12 @@ jobs: tests: - changed-files: - any-glob-to-any-file: - # directory conventions (covers Java src/test, Ruby test/spec, etc.) - '**/tests/**' - '**/test/**' - '**/spec/**' - '**/__tests__/**' - # JS / TS + - 'e2e/**' + - '**/e2e/**' - '**/*.test.js' - '**/*.test.jsx' - '**/*.test.ts' @@ -112,21 +121,16 @@ jobs: - '**/*.spec.jsx' - '**/*.spec.ts' - '**/*.spec.tsx' - # Python - '**/*_test.py' - '**/test_*.py' - '**/conftest.py' - # .NET - '**/*Tests.cs' - '**/*Test.cs' - '**/*.Tests/**' - # Java / JVM - '**/*Test.java' - '**/*Tests.java' - '**/*IT.java' - # Go - '**/*_test.go' - # Ruby - '**/*_spec.rb' - '**/*_test.rb' EOF diff --git a/.github/workflows/callable-pr-policy.yaml b/.github/workflows/callable-pr-policy.yaml index dbd9537..69413f5 100644 --- a/.github/workflows/callable-pr-policy.yaml +++ b/.github/workflows/callable-pr-policy.yaml @@ -14,17 +14,20 @@ name: PR Policy # # Secrets are optional at the declaration level. For human PRs that include a # Jira key, all three must be configured or the check fails closed (POLICY-INFRA). -# Dependabot skips Jira/branch/body checks but still runs commit-subject and -# workflow supply-chain checks. +# Dependabot-authored PRs (pull_request.user.login == dependabot[bot]) exit +# successfully with no metadata or supply-chain checks. +# +# A missing Jira key on a human PR is a warning, not a failure. A present key +# is still verified against Jira and fails closed on lookup or credential errors. # # Emergency-revert exemption: when the title type is `revert`, the PR has no # Jira key in the title, and the `emergency-revert` label is present on the PR, -# a candidate exemption is computed before title validation so the Jira key is -# not required in the title. The exemption is confirmed by verifying that the -# label was applied by a collaborator with maintain or admin permission. Any -# pagination truncation of the event timeline is POLICY-INFRA — partial history -# is never trusted. Unauthorized/null-actor/bot results add a violation. -# Branch, body, and commit checks remain regardless. +# a candidate exemption is computed so the missing-key warning is suppressed. +# The exemption is confirmed by verifying that the label was applied by a +# collaborator with maintain or admin permission. Any pagination truncation of +# the event timeline is POLICY-INFRA — partial history is never trusted. +# Unauthorized/null-actor/bot results add a violation. Branch, body, and commit +# checks remain regardless. # # Known platform limitation: metadata edits (labels, title changes) made via # GITHUB_TOKEN do not reliably emit a new pull_request event. Org automation @@ -90,12 +93,8 @@ jobs: return errs; } const desc = m[4]; - const jiraSuffix = m[5]; if (desc.endsWith('.')) errs.push('Description must not end with a period'); if (!/^[a-z]/.test(desc)) errs.push('Description must start with a lowercase letter'); - if (!isDependabot && !jiraSuffix && !jiraMaybeExempt) { - errs.push('Missing Jira key — expected (DEV-NNN), (PLAT-NNN), (SEC-NNN), or (AP-NNN) at end of title'); - } return errs; } @@ -651,11 +650,13 @@ jobs: // ── Main ───────────────────────────────────────────────────────────────── const violations = []; + const warnings = []; const infraCodes = []; let infraFailed = false; const MAX_ANNOTATIONS = 50; function addViolation(msg) { violations.push(msg); } + function addWarning(msg) { warnings.push(msg); } function addInfra(msg) { infraCodes.push(msg); infraFailed = true; } const repoOwner = context.repo.owner; @@ -663,6 +664,10 @@ jobs: const pr = context.payload.pull_request; const prNum = pr.number; const isDep = pr.user.login === 'dependabot[bot]'; + if (isDep) { + await core.summary.addRaw('## PR Policy: skipped (Dependabot)').write(); + return; + } const titleTypeMatch = pr.title.match(/^([a-z]+)/); const titleType = titleTypeMatch ? titleTypeMatch[1] : ''; @@ -774,10 +779,14 @@ jobs: } } - // 7 — Jira existence (Dependabot exempt; emergency-revert may be exempt) - // Skip entirely when emergency auth already produced an infra error to - // avoid a redundant credential error on a PR that has no Jira key. - const jiraKey = isDep ? null : getJiraKey(pr.title); + // 7 — Jira existence. A present key is verified fail-closed. A missing + // key is a warning, except authorized emergency-reverts which stay silent. + // Skip the existence lookup when emergency auth already produced an infra + // error to avoid a redundant credential error on a PR that has no key. + const jiraKey = getJiraKey(pr.title); + if (!jiraKey && !jiraExempt) { + addWarning('Missing Jira key. Expected (DEV-NNN), (PLAT-NNN), (SEC-NNN), or (AP-NNN) at end of title'); + } if (!jiraExempt && jiraKey && !emergencyAuthFailed) { const cloudId = process.env.JIRA_CLOUD_ID || ''; const jiraEmail = process.env.JIRA_SERVICE_ACCOUNT_EMAIL || ''; @@ -877,6 +886,7 @@ jobs: const annotated = violations.slice(0, MAX_ANNOTATIONS); for (const msg of annotated) core.error(stripHash(msg)); for (const msg of infraCodes.slice(0, MAX_ANNOTATIONS)) core.error(msg); + for (const msg of warnings.slice(0, MAX_ANNOTATIONS)) core.warning(msg); if (violations.length > MAX_ANNOTATIONS) { core.warning((violations.length - MAX_ANNOTATIONS) + ' additional violation(s) suppressed (max ' + MAX_ANNOTATIONS + ' annotations)'); } @@ -891,6 +901,14 @@ jobs: summaryParts.push('- _...and ' + (violations.length - MAX_ANNOTATIONS) + ' more violation(s) not shown_'); } } + if (warnings.length > 0) { + summaryParts.push('', '### Warnings'); + const shownW = warnings.slice(0, MAX_ANNOTATIONS); + for (const msg of shownW) summaryParts.push('- ' + msg); + if (warnings.length > MAX_ANNOTATIONS) { + summaryParts.push('- _...and ' + (warnings.length - MAX_ANNOTATIONS) + ' more warning(s) not shown_'); + } + } if (infraCodes.length > 0) { summaryParts.push('', '### Infrastructure failures'); const shownI = infraCodes.slice(0, MAX_ANNOTATIONS); diff --git a/README.md b/README.md index a892786..4c9e4e3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Organization-level GitHub configuration for Sea Haven Industries. ### PR title -`type(scope): description (DEV-123)` — maximum 120 characters, including the Jira suffix. The Jira key is required at the end in parentheses. Active projects: **DEV** (product), **PLAT** (platform), **SEC** (security). INFRA is a closed archive. Jira-exempt only: Dependabot PRs and permission-controlled emergency reverts. +`type(scope): description (DEV-123)` — maximum 120 characters, including the Jira suffix. Put the Jira key at the end in parentheses. A missing key is a warning, not a failure. Active projects: **DEV** (product), **PLAT** (platform), **SEC** (security). INFRA is a closed archive. Dependabot-authored PRs skip the policy gate. Authorized emergency reverts suppress the missing-key warning. ### PR body @@ -50,7 +50,7 @@ The two sanctioned deploy paths are merge to `main` triggering the pipeline and **`.github/workflows/cd-dotnet-eb.yaml`** — Reusable CD for .NET apps on AWS Elastic Beanstalk. Publishes the project, packages a bundle, uploads it, creates an application version, and updates an **existing** environment with OIDC credentials — it never creates an environment. Serialised per environment via a `concurrency` group, and the post-deploy check fails the job if EB rolls the deploy back. The caller owns branch-to-environment mapping. -**`.github/workflows/callable-pr-policy.yaml`** — Reusable PR metadata gate. Validates PR title convention (type/scope/Jira key), branch naming, four-section body, commit subjects, AI attribution footers, and workflow file pin compliance — all via GitHub API, no checkout. Emits `policy / pr` when the caller job is named `policy`. Optional secrets `JIRA_CLOUD_ID`, `JIRA_SERVICE_ACCOUNT_EMAIL`, and `JIRA_API_TOKEN` must all be set for human PRs; Dependabot skips Jira/branch/body but still runs commit and workflow supply-chain checks. Emergency `revert` PRs may skip Jira with the `emergency-revert` label applied by a human collaborator with `maintain` or `admin` permission. +**`.github/workflows/callable-pr-policy.yaml`** — Reusable PR metadata gate. Validates PR title convention (type/scope), branch naming, four-section body, commit subjects, AI attribution footers, and workflow file pin compliance — all via GitHub API, no checkout. Emits `policy / pr` when the caller job is named `policy`. Optional secrets `JIRA_CLOUD_ID`, `JIRA_SERVICE_ACCOUNT_EMAIL`, and `JIRA_API_TOKEN` must all be set when a human PR title includes a Jira key. A missing key is a warning; a present key is verified fail-closed. Dependabot-authored PRs (`pull_request.user.login == dependabot[bot]`) exit successfully with no checks. Emergency `revert` PRs suppress the missing-key warning when the `emergency-revert` label was applied by a human collaborator with `maintain` or `admin` permission. The supply-chain check operates in **diff mode**: for modified or renamed workflow files, the gate fetches the base-branch version at `pr.base.sha` and reports only violations whose normalized fingerprint is absent from the base. Added files must be fully compliant. Historical drift already present in the base branch is handled by the drift audit/remediation backlog, not by this gate. A failure to fetch the base version is a `POLICY-INFRA` error and the file is not silently grandfathered. @@ -64,7 +64,7 @@ The supply-chain check operates in **diff mode**: for modified or renamed workfl **`.github/workflows/release-on-merge.yaml`** — Repo automation (not callable): cuts a tag and GitHub Release for **this** repo whenever a merge to `main` changes a reusable workflow, so Dependabot has a release to advance consumer SHA pins to (see the pinning policy below). -**`.github/workflows/policy.yaml`** — This repo's own thin caller of `callable-pr-policy.yaml`, so the PR policy gate runs on `.github`'s own PRs. Pinned to the remote SHA at v1.0.5; a local `./` path reference is rejected by the supply-chain gate. The Jira org secrets (`JIRA_CLOUD_ID`, `JIRA_SERVICE_ACCOUNT_EMAIL`, `JIRA_API_TOKEN`) must be granted to this repo before human PR checks can pass (Dependabot and supply-chain checks still run without them). +**`.github/workflows/policy.yaml`** — This repo's own thin caller of `callable-pr-policy.yaml`, so the PR policy gate runs on `.github`'s own PRs. Pinned to the remote SHA at v1.0.5; a local `./` path reference is rejected by the supply-chain gate. The Jira org secrets (`JIRA_CLOUD_ID`, `JIRA_SERVICE_ACCOUNT_EMAIL`, `JIRA_API_TOKEN`) must be granted to this repo before human PRs that include a Jira key can pass the existence check. Dependabot-authored PRs skip the gate. **`.github/workflows/labeler.yaml`** — This repo's own thin caller of `callable-labeler.yaml`, so the labeler runs on `.github`'s own PRs. diff --git a/test/pr-policy.test.mjs b/test/pr-policy.test.mjs index c6e0978..fb925bc 100644 --- a/test/pr-policy.test.mjs +++ b/test/pr-policy.test.mjs @@ -120,10 +120,8 @@ describe('validateTitle', () => { assert.deepEqual(v.validateTitle('chore(deps): bump lodash from 4.17.20 to 4.17.21', true), []); }); - it('rejects missing Jira key for non-Dependabot', () => { - const errs = v.validateTitle('fix: resolve null pointer', false); - assert.ok(errs.length > 0, 'should have errors'); - assert.ok(errs.some(e => e.includes('Jira')), 'should mention Jira: ' + errs.join('; ')); + it('accepts a title without Jira key', () => { + assert.deepEqual(v.validateTitle('fix: resolve null pointer', false), []); }); it('rejects unknown type', () => { @@ -183,9 +181,11 @@ describe('validateTitle', () => { assert.deepEqual(v.validateTitle('feat(frontend): scaffold vite spa and ci (AP-4)', false), []); }); - it('rejects inactive Jira projects (INFRA)', () => { - const errs = v.validateTitle('fix: patch (INFRA-1)', false); - assert.ok(errs.length > 0, 'INFRA is inactive and should fail'); + it('does not treat INFRA as a valid Jira suffix', () => { + // INFRA is a closed archive, so (INFRA-1) is not a recognized key. + // validateTitle no longer fails on a missing key; Main warns instead. + assert.deepEqual(v.validateTitle('fix: patch (INFRA-1)', false), []); + assert.equal(v.getJiraKey('fix: patch (INFRA-1)'), null); }); it('accepts all valid types', () => { @@ -196,19 +196,11 @@ describe('validateTitle', () => { } }); - // Emergency-revert candidate: jiraMaybeExempt skips the Jira key requirement. - it('accepts revert title without Jira when jiraMaybeExempt is true', () => { + // Missing Jira is a warning in Main, not a validateTitle error. + it('accepts revert title without Jira regardless of jiraMaybeExempt', () => { assert.deepEqual(v.validateTitle('revert: emergency rollback of payment service', false, true), []); - }); - - it('rejects revert title without Jira when jiraMaybeExempt is false', () => { - const errs = v.validateTitle('revert: emergency rollback of payment service', false, false); - assert.ok(errs.some(e => e.includes('Jira')), 'missing Jira should fail without exemption: ' + errs.join('; ')); - }); - - it('rejects revert title without Jira when jiraMaybeExempt is omitted (default false)', () => { - const errs = v.validateTitle('revert: emergency rollback of payment service', false); - assert.ok(errs.some(e => e.includes('Jira')), 'default should behave like false: ' + errs.join('; ')); + assert.deepEqual(v.validateTitle('revert: emergency rollback of payment service', false, false), []); + assert.deepEqual(v.validateTitle('revert: emergency rollback of payment service', false), []); }); });