-
Notifications
You must be signed in to change notification settings - Fork 274
👷 ci: add PR quality gates, commit message enforcement, and QA ICU fix #4515
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+6,125
−58
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
00ed5d6
🐛 fix(qa): validate ICU patterns on locked segments at project creation
Ostico 49ce566
refactor(qa): share ICU source detection logic across LQA flows
Copilot d283bed
docs(qa): add full phpdoc for shared ICU detector
Copilot ef79382
🔧 chore(phpstan): regenerate baseline for new @throws rules
Ostico 8fe8839
📝 docs: add PR readiness checklist template and GitHub Actions validator
Ostico d513920
📝 docs(pr-template): simplify AI usage disclosure instructions
Ostico f9823a1
🔧 chore(jest): exclude .github/ from Jest test discovery
Ostico 36d6e87
🔧 chore(phpstan): exclude APIDoc build artifacts from analysis
Ostico 41e46fc
🔧 chore(phpstan): mark APIDoc.php exclude path as optional
Ostico 78c6b89
Update .github/workflows/pr-readiness-check.yml
Ostico cd03503
👷 ci: add conventional-commit message enforcement workflow
Ostico b67d105
🐛 fix(ci): relax commit message check for Copilot and GitHub web edits
Ostico 54ea153
Merge branch 'fix-icu-check-not-performed-on-blocked-segments' into c…
Ostico 37931e4
Merge branch 'develop' into ci/pr-quality-gates
Ostico 99a2be3
Merge remote-tracking branch 'origin/develop' into ci/pr-quality-gates
Ostico 8c40af6
🔧 chore(phpstan): add @throws \Throwable to FastAnalysis transaction …
Ostico 9f8cde0
👷 ci(test-guard): add test adequacy gate workflow
Ostico aaef9ff
👷 ci(test-guard): integrate test-guard into CI/CD pipeline
Ostico cb738f3
👷 ci(test-guard): bump ostico/test-guard to v1.0.3
Ostico 1025eac
👷 ci(test-guard): bump ostico/test-guard to v1.0.5
Ostico 7e832f5
👷 ci: add models:read permission to _ci-cd.yml
Ostico d749bd8
🐛 fix(ci): add models:read permission for test-guard AI analysis
Ostico fc04829
👷 ci(test-guard): switch to floating tag ostico/test-guard@v1
Ostico e24019a
👷 ci(permissions): replace statuses:write with checks:write in workflows
Ostico 82e553d
Merge branch 'develop' into ci/pr-quality-gates
Ostico 99c9db4
👷 ci(coverage): add JS coverage report to test-guard pipeline
Ostico File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| ## Summary | ||
|
|
||
| <!-- What does this PR do and why? Keep it brief. --> | ||
|
|
||
| ## Type | ||
|
|
||
| <!-- Check one. --> | ||
|
|
||
| - [ ] `feat` — new user-facing feature | ||
| - [ ] `fix` — bug fix | ||
| - [ ] `refactor` — restructure without behavior change | ||
| - [ ] `chore` — build, deps, config, docs | ||
| - [ ] `perf` — performance improvement | ||
| - [ ] `test` — test coverage | ||
|
|
||
| ## Changes | ||
|
|
||
| <!-- Key changes — what was modified and how. | ||
| One line per file or logical group. --> | ||
|
|
||
| | File | Change | | ||
| |------|--------| | ||
| | | | | ||
|
|
||
| ## Migration Notes | ||
|
|
||
| <!-- Fill this section only when database migrations are involved. | ||
| Delete this section entirely if no migrations are needed. | ||
|
|
||
| All migrations MUST be placed in the migrations/ directory following | ||
| the naming convention: YYYYMMDDHHMMSS_description.php | ||
| extending AbstractMatecatMigration. --> | ||
|
|
||
| - [ ] Migration file added in `migrations/` directory | ||
| - [ ] Backward-compatible with current production schema | ||
| - [ ] NOT backward-compatible — breaking changes documented in Notes section | ||
| - [ ] Tested on a fresh database and on an existing one | ||
|
|
||
| ## Testing | ||
|
|
||
| <!-- How did you verify this works? Check all that apply. --> | ||
|
|
||
| - [ ] `vendor/bin/phpunit --exclude-group=ExternalServices --no-coverage` passes | ||
| - [ ] `./vendor/bin/phpstan` passes (0 errors, with baseline) | ||
| - [ ] Manual testing performed (describe below) | ||
| - [ ] New tests added for changed behavior | ||
| - [ ] Regression tests added for bug fixes | ||
|
|
||
| <!-- Paste relevant test output or describe manual testing steps. --> | ||
|
|
||
| ## AI Disclosure | ||
|
|
||
| <!-- If AI tools were used to write or review code in this PR, disclose it. | ||
| AI-assisted code is welcome, but you must understand everything you ship. | ||
| Unreviewed AI output will not be merged. --> | ||
|
|
||
| - [ ] No AI tools were used in this PR | ||
| - [ ] AI tools were used — details below | ||
|
|
||
| <!-- If AI was used, just name the agent/tool. | ||
| Example: "Claude Code (claude-opus-4-6)" --> | ||
|
|
||
| ## Notes | ||
|
|
||
| <!-- Anything reviewers should know — breaking changes, follow-up work, | ||
| deployment steps, or open questions. Leave blank if none. --> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| 'use strict'; | ||
|
|
||
| // ── Canonical emoji ↔ type map (from conventional-commit.prompt.md) ── | ||
|
|
||
| const EMOJI_TYPE_MAP = { | ||
| '\u{1F3D7}\uFE0F': 'build', // 🏗️ | ||
| '\u{1F527}': 'chore', // 🔧 | ||
| '\u{1F477}': 'ci', // 👷 | ||
| '\u{1F4DD}': 'docs', // 📝 | ||
| '\u2728': 'feat', // ✨ | ||
| '\u{1F41B}': 'fix', // 🐛 | ||
| '\u26A1\uFE0F': 'perf', // ⚡️ | ||
| '\u267B\uFE0F': 'refactor', // ♻️ | ||
| '\u23EA\uFE0F': 'revert', // ⏪️ | ||
| '\u{1F484}': 'style', // 💄 | ||
| '\u2705': 'test', // ✅ | ||
| '\u{1F310}': 'i18n', // 🌐 | ||
| }; | ||
|
|
||
| const TYPE_EMOJI_MAP = Object.fromEntries( | ||
| Object.entries(EMOJI_TYPE_MAP).map(([emoji, type]) => [type, emoji]), | ||
| ); | ||
|
|
||
| const VALID_TYPES = Object.values(EMOJI_TYPE_MAP); | ||
|
|
||
| const MAX_SUBJECT_LENGTH = 100; | ||
|
|
||
| // ── Helpers ────────────────────────────────────────────────────────── | ||
|
|
||
| /** | ||
| * Strip Unicode variation selector 16 (U+FE0F) for comparison. | ||
| * Some editors/terminals include it, others don't. | ||
| */ | ||
| function stripVS16(str) { | ||
| return str.replace(/\uFE0F/g, ''); | ||
| } | ||
|
|
||
| /** | ||
| * Find the conventional type for a given emoji. | ||
| * Matches with or without the trailing VS16 character. | ||
| */ | ||
| function findTypeForEmoji(emoji) { | ||
| const normalized = stripVS16(emoji); | ||
| for (const [key, type] of Object.entries(EMOJI_TYPE_MAP)) { | ||
| if (stripVS16(key) === normalized) { | ||
| return type; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| function isAutoGenerated(subject) { | ||
| return /^Merge /.test(subject); | ||
| } | ||
|
|
||
| function isGitHubWebEdit(subject) { | ||
| return /^(Update|Create|Delete|Rename) .+/.test(subject); | ||
| } | ||
|
|
||
| // ── Core validation ────────────────────────────────────────────────── | ||
|
|
||
| function validateCommitMessage(message, {requireEmoji = true} = {}) { | ||
| const errors = []; | ||
|
|
||
| if (!message || typeof message !== 'string') { | ||
| return {valid: false, errors: ['Empty or invalid commit message'], skipped: false}; | ||
| } | ||
|
|
||
| const subject = message.split('\n')[0].trim(); | ||
|
|
||
| if (!subject) { | ||
| return {valid: false, errors: ['Empty subject line'], skipped: false}; | ||
| } | ||
|
|
||
| if (isAutoGenerated(subject)) { | ||
| return {valid: true, errors: [], skipped: true}; | ||
| } | ||
|
|
||
| if (!requireEmoji && isGitHubWebEdit(subject)) { | ||
| return {valid: true, errors: [], skipped: true}; | ||
| } | ||
|
Ostico marked this conversation as resolved.
|
||
|
|
||
| if (subject.length > MAX_SUBJECT_LENGTH) { | ||
| errors.push( | ||
| `Subject exceeds ${MAX_SUBJECT_LENGTH} characters (${subject.length})`, | ||
| ); | ||
| } | ||
|
|
||
| const firstSpace = subject.indexOf(' '); | ||
| if (firstSpace === -1) { | ||
| errors.push( | ||
| requireEmoji | ||
| ? 'Subject must start with an emoji followed by a space' | ||
| : 'Subject must contain a space', | ||
| ); | ||
| return {valid: false, errors, skipped: false}; | ||
| } | ||
|
|
||
| const firstToken = subject.slice(0, firstSpace); | ||
| const rest = subject.slice(firstSpace + 1); | ||
|
|
||
| const emojiType = findTypeForEmoji(firstToken); | ||
| const hasEmoji = emojiType !== null; | ||
|
|
||
| if (requireEmoji && !hasEmoji) { | ||
| const validList = Object.entries(EMOJI_TYPE_MAP) | ||
| .map(([e, t]) => `${e} ${t}`) | ||
| .join(', '); | ||
| errors.push(`Unknown emoji "${firstToken}". Valid: ${validList}`); | ||
| } | ||
|
|
||
| const textToParse = hasEmoji ? rest : subject; | ||
| const formatMatch = textToParse.match(/^(\w+)(?:\(([^)]+)\))?(!)?: (.+)$/); | ||
| if (!formatMatch) { | ||
| errors.push( | ||
| hasEmoji | ||
| ? 'Format after emoji must be: <type>[(<scope>)][!]: <description>' | ||
| : 'Format must be: <type>[(<scope>)][!]: <description>', | ||
| ); | ||
| return {valid: false, errors, skipped: false}; | ||
| } | ||
|
|
||
| const [, type, , , description] = formatMatch; | ||
|
|
||
| if (!VALID_TYPES.includes(type)) { | ||
| errors.push( | ||
| `Invalid type "${type}". Valid: ${VALID_TYPES.join(', ')}`, | ||
| ); | ||
| } | ||
|
|
||
| if (hasEmoji && emojiType && VALID_TYPES.includes(type) && emojiType !== type) { | ||
| const correctEmoji = TYPE_EMOJI_MAP[type] || '?'; | ||
| errors.push( | ||
| `Emoji ${firstToken} is for "${emojiType}", not "${type}". ` + | ||
| `Use ${correctEmoji} for "${type}"`, | ||
| ); | ||
| } | ||
|
|
||
| if (description && /^[A-Z]/.test(description)) { | ||
| errors.push('Description must start with a lowercase letter'); | ||
| } | ||
|
|
||
| if (description && description.endsWith('.')) { | ||
| errors.push('Description must not end with a period'); | ||
| } | ||
|
|
||
| return {valid: errors.length === 0, errors, skipped: false}; | ||
| } | ||
|
|
||
| module.exports = { | ||
| EMOJI_TYPE_MAP, | ||
| TYPE_EMOJI_MAP, | ||
| VALID_TYPES, | ||
| MAX_SUBJECT_LENGTH, | ||
| validateCommitMessage, | ||
| findTypeForEmoji, | ||
| isAutoGenerated, | ||
| isGitHubWebEdit, | ||
| stripVS16, | ||
| }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isGitHubWebEdit()is used to skip validation in relaxed mode, but the current pattern (/^(Update|Create|Delete|Rename) .+/) will also skip non-auto-generated subjects that happen to start with those verbs (e.g. "Update docs"), weakening the hygiene gate. Tighten the detection to match GitHub’s default web editor messages more precisely (e.g. require a file/path-like token, or match the exact templates GitHub uses).