diff --git a/.changeset/metal-birds-share.md b/.changeset/metal-birds-share.md new file mode 100644 index 0000000000..5dde4075a1 --- /dev/null +++ b/.changeset/metal-birds-share.md @@ -0,0 +1,5 @@ +--- +"@rhds/elements": major +--- +DELETE THIS FILE BEFORE MERGING +chore: changeset validating github action diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml new file mode 100644 index 0000000000..3bb682f067 --- /dev/null +++ b/.github/workflows/validate-pr.yml @@ -0,0 +1,25 @@ +name: Validate PRs + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - edited + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + - run: npm i semantic-release @changesets/read --prefer-offline + - uses: actions/github-script@v7 + with: + script: | + const { validate } = await import('${{ github.workspace }}/scripts/validate-prs.js'); + await validate({ context }); diff --git a/scripts/validate-prs.js b/scripts/validate-prs.js new file mode 100644 index 0000000000..2ad2fa5b21 --- /dev/null +++ b/scripts/validate-prs.js @@ -0,0 +1,67 @@ +import { default as read } from '@changesets/read'; +import semanticRelease from 'semantic-release'; + +/** @typedef {'major'|'minor'|'patch'} ReleaseType */ + +/** + * named capture group 1 `commitType`: + * > Either `feat`, `fix`, `chore`, `docs`, or `style` + * **ANY** (_>= 0x_) + * named capture group 2 `bang`: + * > `!` + */ +const COMMIT_TYPE_RE = /(?feat|fix|chore|docs|style).*(?!)/; + +async function getReleaseType(title, mergeType) { + if (mergeType === 'rebase') { + const result = await semanticRelease({ + dryRun: true, + branches: ['main'], + }) ?? {}; + /** @type {ReleaseType} */ + const type = result?.nextRelease?.type; + return type; + } else { + const { commitType, bang } = title.match(COMMIT_TYPE_RE)?.groups ?? {}; + if (bang) { + return 'major'; + } else { + switch (commitType) { + case 'feat': return 'minor'; + case 'fix': return 'patch'; + } + } + } +} + +export async function validate({ context }) { + const { base, title, auto_merge: autoMerge } = context.payload.pull_request; + + if (base.ref.startsWith('staging/')) { + return true; + } + + const type = await getReleaseType(title, autoMerge?.merge_method) ?? null; + const sets = await read(process.cwd()); + + /** @type {ReleaseType} */ + const release = sets.reduce((greatest, type) => { + switch (greatest) { + case null: + case 'major': + return greatest; + case 'minor': + return type === 'major' ? type : greatest; + case 'patch': + return (type === 'major' || type === 'minor') ? type : greatest; + } + }, null); + + if (!release && type?.match(/m(aj|in)or/)) { + throw new Error(`PR conventional commit title has type (${type}) but no changesets were detected.`); + } + + if (type !== release) { + throw new Error(`PR conventional commit title type (${type}) does not match release type (${release}).`); + } +}