Detailed changes
@@ -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}`);
+ }
+ }
@@ -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.'
+ });
+ }
+ }
@@ -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!'
+ });
+ }
+ }
@@ -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.
@@ -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.
@@ -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"