From 706be37d5c929fe025ffea97b1e1a00fb198af06 Mon Sep 17 00:00:00 2001 From: Drew Smirnoff Date: Tue, 21 Apr 2026 16:01:55 +0400 Subject: [PATCH] ci: add bot @floatpanebot (#746) --- .github/workflows/bot-approve.yml | 56 +++++++++++++++ .github/workflows/bot-check-ci.yml | 104 ++++++++++++++++++++++++++++ .github/workflows/bot-pr-checks.yml | 84 ++++++++++++++++++++++ .github/workflows/demo.yml | 4 +- .github/workflows/screenshots.yml | 4 +- .github/workflows/update-flake.yml | 6 +- 6 files changed, 251 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/bot-approve.yml create mode 100644 .github/workflows/bot-check-ci.yml create mode 100644 .github/workflows/bot-pr-checks.yml diff --git a/.github/workflows/bot-approve.yml b/.github/workflows/bot-approve.yml new file mode 100644 index 0000000000000000000000000000000000000000..ce121cf5e9a795db67c97d64f5e54a6007c60979 --- /dev/null +++ b/.github/workflows/bot-approve.yml @@ -0,0 +1,56 @@ +name: Bot - PR Approval Command + +on: + issue_comment: + types: [created] + +jobs: + approve: + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/approve') }} + runs-on: ubuntu-latest + steps: + - name: Process Approval Command + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} + script: | + const commenter = context.payload.comment.user.login; + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr_number = context.issue.number; + + try { + // 1. Verify membership in the floatpane organization + await github.rest.orgs.checkMembershipForUser({ + org: 'floatpane', + username: commenter, + }); + + // 2. Add an approving review + await github.rest.pulls.createReview({ + owner, + repo, + pull_number: pr_number, + event: 'APPROVE', + body: `Approved on behalf of @${commenter} via \`/approve\` command.` + }); + + // Optionally add a reaction to the command comment to acknowledge it + await github.rest.reactions.createForIssueComment({ + owner, + repo, + comment_id: context.payload.comment.id, + content: 'rocket' + }); + + } catch (error) { + if (error.status === 404) { + // Not a member + await github.rest.issues.createComment({ + owner, repo, issue_number: pr_number, + body: `Sorry @${commenter}, only members of the \`floatpane\` organization can use the \`/approve\` command.` + }); + } else { + core.setFailed(`Error processing approval: ${error.message}`); + } + } diff --git a/.github/workflows/bot-check-ci.yml b/.github/workflows/bot-check-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..a3c685d9a4ed8f6dd9d292285ae079c29cb7c7d4 --- /dev/null +++ b/.github/workflows/bot-check-ci.yml @@ -0,0 +1,104 @@ +name: Bot - CI Failure Notifier + +on: + workflow_run: + workflows: ["CI"] + types: [completed] + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Process CI Result + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} + script: | + const run = context.payload.workflow_run; + const owner = context.repo.owner; + const repo = context.repo.repo; + + // 1. Robustly find the PR number (works for forks too) + let pr_number; + if (run.pull_requests && run.pull_requests.length > 0) { + pr_number = run.pull_requests[0].number; + } else { + // Fallback: Find PR associated with the commit SHA + const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner, + repo, + commit_sha: run.head_sha + }); + if (prs.length > 0) { + pr_number = prs[0].number; + } + } + + if (!pr_number) { + console.log("No open PR found for this workflow run. Exiting."); + return; + } + + // 2. Handle CI Failure -> Request Changes + if (run.conclusion === 'failure') { + const jobs = await github.rest.actions.listJobsForWorkflowRun({ + owner, repo, run_id: run.id + }); + + const failedJobs = jobs.data.jobs.filter(j => j.conclusion === 'failure').map(j => j.name); + + let message = "The CI workflow failed. Please fix the following issues locally and push again:\n\n"; + let foundSpecificInstruction = false; + + if (failedJobs.some(name => name.includes('lint'))) { + message += "- **Lint Failed**: Run `gofmt -w .` to format files and `go vet ./...` to check for vet errors.\n"; + foundSpecificInstruction = true; + } + if (failedJobs.some(name => name.includes('mod-tidy'))) { + message += "- **Mod-tidy Failed**: Run `go mod tidy` locally and commit the resulting `go.mod` and `go.sum` changes.\n"; + foundSpecificInstruction = true; + } + if (failedJobs.some(name => name.includes('nix'))) { + message += "- **Nix Failed**: Run `nix flake check --no-build` locally to find the issue with the flake.\n"; + foundSpecificInstruction = true; + } + + if (!foundSpecificInstruction) { + message += "Please check the [CI logs](" + run.html_url + ") for more details on the failure."; + } + + // Submit an official "Request Changes" review + await github.rest.pulls.createReview({ + owner, + repo, + pull_number: pr_number, + body: message, + event: 'REQUEST_CHANGES' + }); + + } + // 3. Handle CI Success -> Dismiss Reviews + else if (run.conclusion === 'success') { + const { data: botUser } = await github.rest.users.getAuthenticated(); + + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, repo, pull_number: pr_number + }); + + // Find active blocking reviews left by the bot account + const botReviews = reviews.filter(r => + r.user.login === botUser.login && + r.state === 'CHANGES_REQUESTED' + ); + + // Dismiss them + for (const review of botReviews) { + await github.rest.pulls.dismissReview({ + owner, + repo, + pull_number: pr_number, + review_id: review.id, + message: 'The CI workflow has passed! Dismissing previous review.' + }); + } + } diff --git a/.github/workflows/bot-pr-checks.yml b/.github/workflows/bot-pr-checks.yml new file mode 100644 index 0000000000000000000000000000000000000000..ad46e3fd5473a83bf56f0c27e391690f7efa2ddb --- /dev/null +++ b/.github/workflows/bot-pr-checks.yml @@ -0,0 +1,84 @@ +name: Bot - PR Formatting Checks + +on: + pull_request_target: + types: [opened, edited, synchronize] + +jobs: + check-pr: + runs-on: ubuntu-latest + steps: + - name: Validate PR Title and Body + uses: actions/github-script@v7 + env: + PR_BODY: ${{ github.event.pull_request.body }} + PR_TITLE: ${{ github.event.pull_request.title }} + with: + github-token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} + script: | + const title = process.env.PR_TITLE; + const body = process.env.PR_BODY || ""; + const owner = context.repo.owner; + const repo = context.repo.repo; + const pull_number = context.issue.number; + + let errors = []; + + // 1. Check Conventional Commits + const ccRegex = /^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9\-]+\))?:\s.+$/; + if (!ccRegex.test(title)) { + errors.push("- **Title**: Does not follow conventional commits (e.g., `feat: added something`, `fix(core): resolved crash`)."); + } + + // 2. Check PR Template adherence + if (!body.includes("## What?") || !body.includes("## Why?")) { + errors.push("- **Body**: Missing the `## What?` or `## Why?` headings required by the PR template."); + } + + // Check if they just left the HTML comments from the template empty + const cleanedBody = body.replace(//g, "").trim(); + if (cleanedBody.length < 10) { + errors.push("- **Body**: The PR description is too short or hasn't replaced the template placeholders."); + } + + // 3. Request Changes or Dismiss previous requests + if (errors.length > 0) { + const message = `Hi @${context.actor}! Please fix the following issues with your PR:\n\n${errors.join("\n")}`; + + await github.rest.pulls.createReview({ + owner, + repo, + pull_number, + body: message, + event: 'REQUEST_CHANGES' + }); + + core.setFailed("PR formatting checks failed."); + } else { + // The PR is now valid. Let's find out who the bot is to dismiss its own reviews. + const { data: botUser } = await github.rest.users.getAuthenticated(); + + // Fetch all reviews on this PR + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number + }); + + // Find active blocking reviews left by the bot account + const botReviews = reviews.filter(r => + r.user.login === botUser.login && + r.state === 'CHANGES_REQUESTED' + ); + + // Dismiss them so the PR is unblocked + for (const review of botReviews) { + await github.rest.pulls.dismissReview({ + owner, + repo, + pull_number, + review_id: review.id, + message: 'Formatting issues have been resolved. Thank you!' + }); + } + } diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml index 239eda259aec7b8a45842c786c36869168df793f..1a024e827bf3157fb9205c67854a41eaf311a777 100644 --- a/.github/workflows/demo.yml +++ b/.github/workflows/demo.yml @@ -83,8 +83,8 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@v8 with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "docs: update demo.gif from release [skip ci]" + token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} + commit-message: "docs: update demo.gif from release" title: "docs: update demo.gif" body: | This PR updates the demo GIF based on the latest release version. diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 9e2fc590d7f4d8739be0eb8cc95bc012d5356e5a..02cf81893e8e5e778d495ad088f37ff12977fd03 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -124,8 +124,8 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@v8 with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "docs: update feature screenshots [skip ci]" + token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} + commit-message: "docs: update feature screenshots" title: "docs: update feature screenshots" body: | This PR updates the feature screenshots based on the latest release version. diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index f2b544dd6ffac8d94bb5c9c6e66ca7ac0606c50e..06473719e288983d7733532d9d4fc04db09d994c 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -23,12 +23,12 @@ jobs: - name: Create Pull Request env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} run: | git diff --quiet flake.lock && exit 0 BRANCH="chore/update-flake-lock" - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "Floatpane Bot" + git config user.email "us@floatpane.com" git checkout -b "$BRANCH" git add flake.lock git commit -m "chore: update flake.lock"