diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml deleted file mode 100644 index 6d8cd6c3..00000000 --- a/.github/workflows/auto-merge.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Auto Merge - -# Merge PRs that have the lgtm label and pass all required checks. -# Runs on a cron schedule so branch-protection status checks have time to finish. - -on: - schedule: - # Every 30 minutes (avoids :00/:30). - - cron: '7,37 * * * *' - workflow_dispatch: {} - -permissions: - contents: write - pull-requests: write - -jobs: - merge: - runs-on: ubuntu-latest - steps: - - uses: jpmcb/prow-github-actions@c44ac3a57d67639e39e4a4988b52049ef45b80dd # v2.0.0 - with: - jobs: lgtm - github-token: '${{ secrets.GITHUB_TOKEN }}' - merge-method: squash diff --git a/.github/workflows/dco.yml b/.github/workflows/dco.yml deleted file mode 100644 index 31e41375..00000000 --- a/.github/workflows/dco.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: DCO - -# Verify every commit in a PR carries a `Signed-off-by` trailer that matches its -# author. Runs on pull_request_target so it also covers fork PRs without waiting -# on the first-time-contributor approval gate (see #121). -# -# We fetch the PR's commit list straight from the API: the upstream -# tim-actions/get-pr-commits action only understands the `pull_request` event and -# aborts on pull_request_target with "Invalid event", which then left the DCO -# step parsing an empty commit list ("Unexpected end of JSON input"). - -on: - workflow_dispatch: - inputs: - pr: - description: "PR number to check" - required: true - pull_request_target: - branches: - - main - -permissions: - contents: read - -jobs: - dco_check: - permissions: - pull-requests: read - runs-on: ubuntu-latest - name: DCO Check - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr }} - steps: - - name: DCO Check - run: | - set -euo pipefail - - # One line per commit: "\t\t". - # A commit passes when any line of its message is exactly the - # "Signed-off-by: " trailer for its author. - results="$(gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/commits" --jq ' - .[] - | ("Signed-off-by: " + .commit.author.name + " <" + .commit.author.email + ">") as $exp - | "\(.sha[0:8])\t\((.commit.message | split("\n") | index($exp)) != null)\t\($exp)" - ')" - - echo "$results" | awk -F'\t' '{ - if ($2 == "true") printf " PASS %s %s\n", $1, $3 - else printf " FAIL %s missing: %s\n", $1, $3 - }' - - if echo "$results" | awk -F'\t' '$2 == "false" { found = 1 } END { exit !found }'; then - echo "" - echo "DCO check failed: one or more commits lack a Signed-off-by trailer" - echo "matching their author. Sign off every commit, then force-push:" - echo " git rebase --signoff origin/main && git push --force-with-lease" - exit 1 - fi - - echo "All commits are signed off." diff --git a/.github/workflows/pr-commands.yml b/.github/workflows/pr-commands.yml deleted file mode 100644 index 85f6de98..00000000 --- a/.github/workflows/pr-commands.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: PR Commands - -# Prow-style chat-ops for GitHub Actions, powered by jpmcb/prow-github-actions. -# Commands: /cc /uncc /assign /unassign /approve /lgtm /hold -# /retitle /area /kind /priority /remove /close /reopen /lock /milestone - -on: - issue_comment: - types: [created] - -permissions: - contents: read - pull-requests: write - issues: write - -jobs: - execute: - # Only run on PR comments (not plain issues). - if: github.event.issue.pull_request != null - runs-on: ubuntu-latest - steps: - # https://github.com/jpmcb/prow-github-actions - - uses: jpmcb/prow-github-actions@c44ac3a57d67639e39e4a4988b52049ef45b80dd # v2.0.0 - with: - prow-commands: '/assign /unassign /approve /retitle /area /kind /priority /remove /lgtm /close /reopen /lock /milestone /hold /cc /uncc' - github-token: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.github/workflows/pr-freshness.yml b/.github/workflows/pr-freshness.yml deleted file mode 100644 index 11ff69e7..00000000 --- a/.github/workflows/pr-freshness.yml +++ /dev/null @@ -1,194 +0,0 @@ -name: PR freshness - -# When something lands on main, re-check every open PR against the new main and -# leave a high-signal note only when an action is needed: -# - absorbed : rebasing onto main drops all commits (change already merged, e.g. via another PR) -# - conflicting: rebasing onto main fails with conflicts -# Anything else (clean / merely behind) stays quiet. This workflow only comments -# and labels — it never pushes to or modifies any PR branch. -# -# NOTE ON SCOPE: the primary, recommended mechanism for "PRs must be current -# before merge" is native GitHub — enable branch protection -# "Require branches to be up to date before merging", or a merge queue. This -# workflow exists only to cover the gap those do NOT handle: detecting a PR whose -# change is *already on main* (superseded), which is what bit us in #81. -# -# The push:main trigger runs in the base-repo context with a writable token, -# which is what lets it comment on fork-based PRs (a pull_request trigger from a -# fork gets a read-only token and could not). All API writes go through -# actions/github-script; git is used only to compute the rebase verdict. - -on: - push: - branches: [main] - workflow_dispatch: - inputs: - pr: - description: "Single PR number to check (blank = all open PRs)" - required: false - default: "" - -concurrency: - group: pr-freshness - cancel-in-progress: true - -permissions: - contents: read - pull-requests: write - issues: write - -jobs: - check: - runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ github.token }} - INPUT_PR: ${{ github.event.inputs.pr }} - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Compute rebase verdict per open PR - run: | - set +e - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - export GIT_EDITOR=true GIT_SEQUENCE_EDITOR=true - git fetch -q origin main - - if [ -n "$INPUT_PR" ]; then - PRS=( "$INPUT_PR" ) - else - mapfile -t PRS < <(gh pr list --state open --base main --limit 200 \ - --json number,isDraft --jq '.[] | select(.isDraft | not) | .number') - fi - - { - echo "## PR freshness" - echo - echo "| PR | Verdict |" - echo "|----|---------|" - } >> "$GITHUB_STEP_SUMMARY" - - results="[]" - for PR in "${PRS[@]}"; do - [ -z "$PR" ] && continue - if ! git fetch -q origin "pull/$PR/head"; then - echo "| #$PR | fetch-failed |" >> "$GITHUB_STEP_SUMMARY" - continue - fi - git checkout -q --detach FETCH_HEAD - - # Source of truth: simulate the rebase. Git's own "patch contents already - # upstream" logic is the most reliable way to detect an absorbed PR, - # including the case where main got the change via a squash merge. - verdict="ok" - if ! git rebase origin/main >/tmp/rebase.log 2>&1; then - git rebase --abort >/dev/null 2>&1 - verdict="conflicting" - elif [ "$(git rev-list --count origin/main..HEAD)" = "0" ]; then - verdict="absorbed" - fi - git checkout -q --detach origin/main - - results="$(jq -c --argjson pr "$PR" --arg v "$verdict" '. + [{pr: $pr, verdict: $v}]' <<<"$results")" - echo "| #$PR | $verdict |" >> "$GITHUB_STEP_SUMMARY" - done - - printf '%s\n' "$results" > "$RUNNER_TEMP/verdicts.json" - - - name: Comment & label PRs - uses: actions/github-script@v9 - env: - VERDICTS_FILE: ${{ runner.temp }}/verdicts.json - with: - script: | - const fs = require('fs'); - const { owner, repo } = context.repo; - const results = JSON.parse(fs.readFileSync(process.env.VERDICTS_FILE, 'utf8')); - - const MARKER = ''; - const LABELS = { - 'superseded': { color: '0E8A16', description: 'Changes already on main; PR can be closed' }, - 'needs-rebase': { color: 'D93F0B', description: 'Conflicts with main; rebase required' }, - }; - const BODY = { - absorbed: [ - MARKER, '', - '🟢 **These changes are already on `main`.**', '', - 'Rebasing this branch onto the latest `main` drops all of its commits (`patch contents already upstream`), so there is nothing left to merge. The same change has most likely landed through another PR.', '', - 'If that is expected, this PR can be closed. If you believe something is still missing, rebase and push — this note clears automatically.', - ].join('\n'), - conflicting: [ - MARKER, '', - '🟠 **This PR conflicts with the latest `main`.**', '', - 'A rebase onto `main` currently fails with conflicts. Please rebase and resolve them:', '', - '```bash', - 'git fetch origin', - 'git rebase origin/main', - '# resolve conflicts, then:', - 'git push --force-with-lease', - '```', '', - 'This note clears automatically once the conflicts are resolved.', - ].join('\n'), - }; - - // Ensure our labels exist (idempotent). - for (const [name, def] of Object.entries(LABELS)) { - try { - await github.rest.issues.getLabel({ owner, repo, name }); - } catch (e) { - if (e.status !== 404) throw e; - await github.rest.issues.createLabel({ owner, repo, name, ...def }); - } - } - - async function findMarkerComment(issue_number) { - const comments = await github.paginate(github.rest.issues.listComments, { - owner, repo, issue_number, per_page: 100, - }); - return comments.find((c) => c.body && c.body.startsWith(MARKER)); - } - async function upsertComment(issue_number, body) { - const existing = await findMarkerComment(issue_number); - if (existing) { - if (existing.body !== body) { - await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); - } - } else { - await github.rest.issues.createComment({ owner, repo, issue_number, body }); - } - } - async function clearComment(issue_number) { - const existing = await findMarkerComment(issue_number); - if (existing) { - await github.rest.issues.deleteComment({ owner, repo, comment_id: existing.id }); - } - } - async function addLabel(issue_number, name) { - await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [name] }); - } - async function removeLabel(issue_number, name) { - try { - await github.rest.issues.removeLabel({ owner, repo, issue_number, name }); - } catch (e) { - if (e.status !== 404) throw e; // label simply wasn't set - } - } - - for (const { pr, verdict } of results) { - if (verdict === 'absorbed') { - await upsertComment(pr, BODY.absorbed); - await addLabel(pr, 'superseded'); - await removeLabel(pr, 'needs-rebase'); - } else if (verdict === 'conflicting') { - await upsertComment(pr, BODY.conflicting); - await addLabel(pr, 'needs-rebase'); - await removeLabel(pr, 'superseded'); - } else { - await clearComment(pr); - await removeLabel(pr, 'superseded'); - await removeLabel(pr, 'needs-rebase'); - } - core.info(`#${pr} => ${verdict}`); - } diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml deleted file mode 100644 index ac175878..00000000 --- a/.github/workflows/pr-title-lint.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: PR Title Lint - -# Enforce Conventional Commits format on PR titles. -# See https://www.conventionalcommits.org/ - -on: - pull_request_target: - types: [opened, edited, reopened, synchronize] - branches: [main] - -permissions: - contents: read - pull-requests: write - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6 - env: - GITHUB_TOKEN: ${{ github.token }} - with: - # Allowed types (align with CONTRIBUTING.md). - types: | - feat - fix - docs - style - refactor - perf - test - build - ci - chore - revert - # Scope is optional (e.g. "feat(api): …" or just "feat: …"). - requireScope: false - # Reject these scopes (e.g. block "feat(WIP): …"). - disallowScopes: | - WIP - wip - # Lint the PR title only; don't also require a lone commit's message to match. - validateSingleCommit: false - # Leave a comment on the PR explaining why the title failed lint. - commentOnFailure: true