From 8d0f50dcf1dcefd325d671bc579e98a02bea4c2a Mon Sep 17 00:00:00 2001 From: Petar Stoev Date: Fri, 22 May 2026 10:56:51 +0300 Subject: [PATCH] chore: add shared PR quality-gate workflows - conventional-commit-title: enforce semantic PR titles - pr-issue-link: require issue ref in PR title or body - dependabot-auto-merge: auto-merge safe dep updates - stale: 60/90-day issue lifecycle - issue-validator: require type:* label or auto-triage - dependabot.yml: weekly updates --- .github/dependabot.yml | 17 ++++++ .../workflows/conventional-commit-title.yml | 34 +++++++++++ .github/workflows/dependabot-auto-merge.yml | 61 +++++++++++++++++++ .github/workflows/issue-validator.yml | 52 ++++++++++++++++ .github/workflows/pr-issue-link.yml | 41 +++++++++++++ .github/workflows/stale.yml | 38 ++++++++++++ 6 files changed, 243 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/conventional-commit-title.yml create mode 100644 .github/workflows/dependabot-auto-merge.yml create mode 100644 .github/workflows/issue-validator.yml create mode 100644 .github/workflows/pr-issue-link.yml create mode 100644 .github/workflows/stale.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..75ec3fc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + groups: + dev-dependencies: + dependency-type: "development" + update-types: + - "minor" + - "patch" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/conventional-commit-title.yml b/.github/workflows/conventional-commit-title.yml new file mode 100644 index 0000000..81c8b80 --- /dev/null +++ b/.github/workflows/conventional-commit-title.yml @@ -0,0 +1,34 @@ +name: Conventional Commit Title + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: read + +jobs: + check-title: + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + chore + docs + refactor + test + perf + style + ci + build + revert + requireScope: false + subjectPattern: ^[A-Za-z].+$ + subjectPatternError: | + The PR subject (after the colon) must start with a letter and be non-empty. + Example: "feat(#2345): add bulk export for tenders" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..463b379 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,61 @@ +name: Dependabot auto-merge + +# Auto-approves and enables GitHub's auto-merge for Dependabot PRs when they are safe: +# - patch-level updates (any dependency) +# - minor-level updates for devDependencies only +# Major updates and minor updates on runtime deps are left for human review. +# +# Auto-merge waits for branch-protection required checks to pass before merging. +# Without branch protection, auto-merge falls through to immediate merge. + +on: + pull_request: + branches: [main] + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v3 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Approve + enable auto-merge (patch) + if: steps.metadata.outputs.update-type == 'version-update:semver-patch' + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr review --approve "$PR_URL" --body "Auto-approving patch-level dependency update." + gh pr merge --auto --squash "$PR_URL" + + - name: Approve + enable auto-merge (minor devDependency) + if: > + steps.metadata.outputs.update-type == 'version-update:semver-minor' + && steps.metadata.outputs.dependency-type == 'direct:development' + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr review --approve "$PR_URL" --body "Auto-approving minor dev-dependency update." + gh pr merge --auto --squash "$PR_URL" + + - name: Flag for human review (otherwise) + if: > + steps.metadata.outputs.update-type == 'version-update:semver-major' + || (steps.metadata.outputs.update-type == 'version-update:semver-minor' + && steps.metadata.outputs.dependency-type != 'direct:development') + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }} + DEP_TYPE: ${{ steps.metadata.outputs.dependency-type }} + run: | + gh pr comment "$PR_URL" --body "⚠️ Not auto-merging: \`$UPDATE_TYPE\` on \`$DEP_TYPE\`. Requires human review." diff --git a/.github/workflows/issue-validator.yml b/.github/workflows/issue-validator.yml new file mode 100644 index 0000000..1e013a8 --- /dev/null +++ b/.github/workflows/issue-validator.yml @@ -0,0 +1,52 @@ +name: Issue Validator + +on: + issues: + types: [opened, reopened, edited] + +permissions: + issues: write + +jobs: + validate: + runs-on: ubuntu-latest + if: github.event.action == 'opened' || github.event.action == 'reopened' + steps: + - name: Ensure type label or request one + uses: actions/github-script@v7 + with: + script: | + const issue = context.payload.issue; + const labels = issue.labels.map(l => l.name); + const hasTypeLabel = labels.some(name => name.startsWith('type:')); + + if (hasTypeLabel) { + core.info(`Has type label - OK`); + return; + } + + // No type label: user created via API or stripped it. Add needs-triage, comment. + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ['needs-triage'] + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: [ + '👋 This issue is missing a `type:*` label.', + '', + 'Please use one of the issue templates when opening new issues:', + '- 🐛 [Bug](../../issues/new?template=bug.yml)', + '- ✨ [Feature](../../issues/new?template=feature.yml)', + '- 🔧 [Chore](../../issues/new?template=chore.yml)', + '', + 'Or add the appropriate `type:bug` / `type:feature` / `type:chore` / `type:spike` label manually.', + '', + '_Auto-labeled `needs-triage`; @PetarStoev02 will review._' + ].join('\n') + }); diff --git a/.github/workflows/pr-issue-link.yml b/.github/workflows/pr-issue-link.yml new file mode 100644 index 0000000..67bade4 --- /dev/null +++ b/.github/workflows/pr-issue-link.yml @@ -0,0 +1,41 @@ +name: PR Issue Link + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +jobs: + check-issue-link: + runs-on: ubuntu-latest + steps: + - name: Check for issue reference + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const title = pr.title || ''; + const body = pr.body || ''; + + // Exempt chore(*) and docs(*) scopes (legitimate no-issue PRs) + const exemptScopes = /^(chore|docs)(\([^)]+\))?!?: /; + if (exemptScopes.test(title)) { + core.info(`Exempt scope detected in title: "${title}"`); + return; + } + + // Accept any of: "Closes #N", "Fixes #N", "Refs #N" in body, or "(#N)" scope in title + const closesPattern = /(?:closes|fixes|resolves|refs|references)\s+#\d+/i; + const scopePattern = /\(#\d+\)/; + + const hasBodyRef = closesPattern.test(body); + const hasTitleRef = scopePattern.test(title); + + if (!hasBodyRef && !hasTitleRef) { + core.setFailed( + 'PR must reference an issue. Add "Closes #N" / "Fixes #N" / "Refs #N" to the PR body, ' + + 'OR include an issue scope like "fix(#1234): ..." in the PR title. ' + + 'Exception: chore(*) and docs(*) PRs are exempt.' + ); + } else { + core.info(`Issue link found: body=${hasBodyRef}, title=${hasTitleRef}`); + } diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..1e10207 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,38 @@ +name: Stale Issues + +on: + schedule: + - cron: '0 2 * * *' # Daily 02:00 UTC + workflow_dispatch: + +permissions: + issues: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v10 + with: + days-before-issue-stale: 60 + days-before-issue-close: 30 + stale-issue-label: 'stale' + stale-issue-message: | + This issue has had no activity in 60 days. Flagging `stale`. + If it's still relevant, comment or add it to a due-dated milestone within 30 days, otherwise it will be auto-closed. + close-issue-message: | + Auto-closing after 90 days of inactivity. Reopen with fresh details if still relevant. + close-issue-reason: 'not_planned' + + # Exempt: any issue with a due-dated milestone, blocked label, or assignee + exempt-issue-labels: 'blocked,non-actionable' + exempt-all-milestones: false + exempt-milestones: '' + exempt-assignees: '' # Issues with an assignee are not exempt; stale still applies if no activity + + # Never touch PRs + days-before-pr-stale: -1 + days-before-pr-close: -1 + + operations-per-run: 100 + ascending: true