| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const fs = require('fs') |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const types = new Set([ |
| 'build', |
| |
| |
| |
| 'ci', |
| 'config', |
| 'deps', |
| 'docs', |
| 'feat', |
| 'fix', |
| 'perf', |
| 'refactor', |
| 'revert', |
| 'style', |
| 'telemetry', |
| 'test', |
| 'types', |
| ]) |
|
|
| |
| const scopes = new Set([ |
| 'amazonq', |
| 'core', |
| 'explorer', |
| 'lambda', |
| 'logs', |
| 'redshift', |
| 'q-chat', |
| 'q-featuredev', |
| 'q-inlinechat', |
| 'q-transform', |
| 'sam', |
| 's3', |
| 'telemetry', |
| 'toolkit', |
| 'ui', |
| 'sagemakerunifiedstudio', |
| ]) |
| void scopes |
|
|
| |
| |
| |
| |
| |
| |
| |
| function validateTitle(title) { |
| const parts = title.split(':') |
| const subject = parts.slice(1).join(':').trim() |
|
|
| if (title.startsWith('Merge')) { |
| return undefined |
| } |
|
|
| if (parts.length < 2) { |
| return 'missing colon (:) char' |
| } |
|
|
| const typeScope = parts[0] |
|
|
| const [type, scope] = typeScope.split(/\(([^)]+)\)$/) |
|
|
| if (/\s+/.test(type)) { |
| return `type contains whitespace: "${type}"` |
| } else if (type === 'chore') { |
| return 'Do not use "chore" as a type. If the existing valid types are insufficent, add a new type to the `lintcommit.js` script.' |
| } else if (!types.has(type)) { |
| return `invalid type "${type}"` |
| } else if (!scope && typeScope.includes('(')) { |
| return `must be formatted like type(scope):` |
| } else if (!scope && ['feat', 'fix'].includes(type)) { |
| return `"${type}" type must include a scope (example: "${type}(amazonq)")` |
| } else if (scope && scope.length > 30) { |
| return 'invalid scope (must be <=30 chars)' |
| } else if (scope && /[^- a-z0-9]+/.test(scope)) { |
| return `invalid scope (must be lowercase, ascii only): "${scope}"` |
| } else if (subject.length === 0) { |
| return 'empty subject' |
| } else if (subject.length > 100) { |
| return 'invalid subject (must be <=100 chars)' |
| } |
|
|
| return undefined |
| } |
|
|
| function run() { |
| const eventData = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8')) |
| const pullRequest = eventData.pull_request |
|
|
| |
|
|
| if (!pullRequest) { |
| console.info('No pull request found in the context') |
| return |
| } |
|
|
| const title = pullRequest.title |
|
|
| const failReason = validateTitle(title) |
| const msg = failReason |
| ? ` |
| Invalid pull request title: \`${title}\` |
| |
| * Problem: ${failReason} |
| * Expected format: \`type(scope): subject...\` |
| * type: one of (${Array.from(types).join(', ')}) |
| * scope: lowercase, <30 chars |
| * subject: must be <100 chars |
| * documentation: https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#pull-request-title |
| * Hint: *close and re-open the PR* to re-trigger CI (after fixing the PR title). |
| ` |
| : `Pull request title matches the [expected format](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#pull-request-title).` |
|
|
| if (process.env.GITHUB_STEP_SUMMARY) { |
| fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, msg) |
| } |
|
|
| if (failReason) { |
| console.error(msg) |
| process.exit(1) |
| } else { |
| console.info(msg) |
| } |
| } |
|
|
| function _test() { |
| const tests = { |
| ' foo(scope): bar': 'type contains whitespace: " foo"', |
| 'build: update build process': undefined, |
| 'chore: update dependencies': |
| 'Do not use "chore" as a type. If the existing valid types are insufficent, add a new type to the `lintcommit.js` script.', |
| 'ci: configure CI/CD': undefined, |
| 'config: update configuration files': undefined, |
| 'deps: bump the aws-sdk group across 1 directory with 5 updates': undefined, |
| 'docs: update documentation': undefined, |
| 'feat(foo): add new feature': undefined, |
| 'feat(foo):': 'empty subject', |
| 'feat foo):': 'type contains whitespace: "feat foo)"', |
| 'feat(foo)): sujet': 'invalid type "feat(foo))"', |
| 'feat(foo: sujet': 'invalid type "feat(foo"', |
| 'feat(Q Foo Bar): bar': 'invalid scope (must be lowercase, ascii only): "Q Foo Bar"', |
| 'feat(scope):': 'empty subject', |
| 'feat(q foo bar): bar': undefined, |
| 'feat(foo): x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x ': |
| 'invalid subject (must be <=100 chars)', |
| 'feat: foo': '"feat" type must include a scope (example: "feat(amazonq)")', |
| 'fix: foo': '"fix" type must include a scope (example: "fix(amazonq)")', |
| 'fix(a-b-c): resolve issue': undefined, |
| 'foo (scope): bar': 'type contains whitespace: "foo "', |
| 'invalid title': 'missing colon (:) char', |
| 'perf: optimize performance': undefined, |
| 'refactor: improve code structure': undefined, |
| 'revert: feat: add new feature': undefined, |
| 'style: format code': undefined, |
| 'test: add new tests': undefined, |
| 'types: add type definitions': undefined, |
| 'Merge staging into feature/lambda-get-started': undefined, |
| } |
|
|
| let passed = 0 |
| let failed = 0 |
|
|
| for (const [title, expected] of Object.entries(tests)) { |
| const result = validateTitle(title) |
| if (result === expected) { |
| console.log(`✅ Test passed for "${title}"`) |
| passed++ |
| } else { |
| console.log(`❌ Test failed for "${title}" (expected "${expected}", got "${result}")`) |
| failed++ |
| } |
| } |
|
|
| console.log(`\n${passed} tests passed, ${failed} tests failed`) |
| } |
|
|
| function main() { |
| const mode = process.argv[2] |
|
|
| if (mode === 'test') { |
| _test() |
| } else { |
| run() |
| } |
| } |
|
|
| main() |
|
|