python-yarbo 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. python_yarbo-0.1.0/.editorconfig +17 -0
  2. python_yarbo-0.1.0/.github/CODEOWNERS +4 -0
  3. python_yarbo-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
  4. python_yarbo-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +61 -0
  5. python_yarbo-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +48 -0
  6. python_yarbo-0.1.0/.github/SECURITY.md +76 -0
  7. python_yarbo-0.1.0/.github/copilot.yml +4 -0
  8. python_yarbo-0.1.0/.github/dependabot.yml +31 -0
  9. python_yarbo-0.1.0/.github/workflows/auto-create-pr.yml +93 -0
  10. python_yarbo-0.1.0/.github/workflows/ci.yml +103 -0
  11. python_yarbo-0.1.0/.github/workflows/copilot-automerge.yml +112 -0
  12. python_yarbo-0.1.0/.github/workflows/release.yml +122 -0
  13. python_yarbo-0.1.0/.github/workflows/security.yml +56 -0
  14. python_yarbo-0.1.0/.gitignore +62 -0
  15. python_yarbo-0.1.0/.pre-commit-config.yaml +37 -0
  16. python_yarbo-0.1.0/CHANGELOG.md +66 -0
  17. python_yarbo-0.1.0/CONTRIBUTING.md +168 -0
  18. python_yarbo-0.1.0/LICENSE +21 -0
  19. python_yarbo-0.1.0/PKG-INFO +306 -0
  20. python_yarbo-0.1.0/README.md +264 -0
  21. python_yarbo-0.1.0/docs/api.md +154 -0
  22. python_yarbo-0.1.0/docs/index.md +44 -0
  23. python_yarbo-0.1.0/examples/basic_control.py +67 -0
  24. python_yarbo-0.1.0/examples/cloud_login.py +82 -0
  25. python_yarbo-0.1.0/examples/telemetry_stream.py +54 -0
  26. python_yarbo-0.1.0/pyproject.toml +99 -0
  27. python_yarbo-0.1.0/ruff.toml +49 -0
  28. python_yarbo-0.1.0/src/yarbo/__init__.py +122 -0
  29. python_yarbo-0.1.0/src/yarbo/_codec.py +63 -0
  30. python_yarbo-0.1.0/src/yarbo/auth.py +244 -0
  31. python_yarbo-0.1.0/src/yarbo/client.py +695 -0
  32. python_yarbo-0.1.0/src/yarbo/cloud.py +288 -0
  33. python_yarbo-0.1.0/src/yarbo/cloud_mqtt.py +109 -0
  34. python_yarbo-0.1.0/src/yarbo/const.py +197 -0
  35. python_yarbo-0.1.0/src/yarbo/discovery.py +217 -0
  36. python_yarbo-0.1.0/src/yarbo/error_reporting.py +83 -0
  37. python_yarbo-0.1.0/src/yarbo/exceptions.py +112 -0
  38. python_yarbo-0.1.0/src/yarbo/keys/README.md +41 -0
  39. python_yarbo-0.1.0/src/yarbo/local.py +1636 -0
  40. python_yarbo-0.1.0/src/yarbo/models.py +787 -0
  41. python_yarbo-0.1.0/src/yarbo/mqtt.py +487 -0
  42. python_yarbo-0.1.0/tests/conftest.py +152 -0
  43. python_yarbo-0.1.0/tests/test_auth.py +161 -0
  44. python_yarbo-0.1.0/tests/test_client.py +138 -0
  45. python_yarbo-0.1.0/tests/test_cloud.py +114 -0
  46. python_yarbo-0.1.0/tests/test_cloud_mqtt.py +158 -0
  47. python_yarbo-0.1.0/tests/test_codec.py +71 -0
  48. python_yarbo-0.1.0/tests/test_discovery.py +56 -0
  49. python_yarbo-0.1.0/tests/test_error_reporting.py +93 -0
  50. python_yarbo-0.1.0/tests/test_local.py +687 -0
  51. python_yarbo-0.1.0/tests/test_models.py +468 -0
  52. python_yarbo-0.1.0/tests/test_mqtt.py +547 -0
  53. python_yarbo-0.1.0/tests/test_typed_commands.py +685 -0
  54. python_yarbo-0.1.0/uv.lock +1736 -0
@@ -0,0 +1,17 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ trim_trailing_whitespace = true
8
+
9
+ [*.{py,toml,yaml,yml,json,md}]
10
+ indent_style = space
11
+ indent_size = 4
12
+
13
+ [*.{yaml,yml}]
14
+ indent_size = 2
15
+
16
+ [Makefile]
17
+ indent_style = tab
@@ -0,0 +1,4 @@
1
+ # CODEOWNERS — code review assignment
2
+ # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
3
+
4
+ * @markus-lassfolk
@@ -0,0 +1,110 @@
1
+ name: Bug Report
2
+ description: Report a bug or unexpected behaviour in python-yarbo.
3
+ title: "[Bug]: "
4
+ labels: ["bug", "triage"]
5
+ assignees:
6
+ - markus-lassfolk
7
+
8
+ body:
9
+ - type: markdown
10
+ attributes:
11
+ value: |
12
+ Thanks for taking the time to report a bug! Please fill in as much detail as possible.
13
+ **Do not include credentials, IP addresses, or serial numbers in this report.**
14
+
15
+ - type: textarea
16
+ id: description
17
+ attributes:
18
+ label: Describe the bug
19
+ description: A clear and concise description of what the bug is.
20
+ placeholder: Tell us what happened...
21
+ validations:
22
+ required: true
23
+
24
+ - type: textarea
25
+ id: steps
26
+ attributes:
27
+ label: Steps to reproduce
28
+ description: Steps to reproduce the behaviour.
29
+ placeholder: |
30
+ 1. Import the library
31
+ 2. Run `async with YarboClient(...) as client:`
32
+ 3. Call `await client.lights_on()`
33
+ 4. See error
34
+ validations:
35
+ required: true
36
+
37
+ - type: textarea
38
+ id: expected
39
+ attributes:
40
+ label: Expected behaviour
41
+ description: What did you expect to happen?
42
+ validations:
43
+ required: true
44
+
45
+ - type: textarea
46
+ id: actual
47
+ attributes:
48
+ label: Actual behaviour
49
+ description: What actually happened? Include full tracebacks (redact any sensitive data).
50
+ validations:
51
+ required: true
52
+
53
+ - type: input
54
+ id: lib-version
55
+ attributes:
56
+ label: python-yarbo version
57
+ placeholder: "e.g. 0.1.0 — run `pip show python-yarbo`"
58
+ validations:
59
+ required: true
60
+
61
+ - type: input
62
+ id: python-version
63
+ attributes:
64
+ label: Python version
65
+ placeholder: "e.g. 3.12.3 — run `python --version`"
66
+ validations:
67
+ required: true
68
+
69
+ - type: dropdown
70
+ id: os
71
+ attributes:
72
+ label: Operating System
73
+ options:
74
+ - Ubuntu 24.04
75
+ - Ubuntu 22.04
76
+ - Debian 12
77
+ - macOS 14 (Sonoma)
78
+ - macOS 13 (Ventura)
79
+ - Windows 11
80
+ - Windows 10
81
+ - Raspberry Pi OS
82
+ - Other (specify in additional context)
83
+ validations:
84
+ required: true
85
+
86
+ - type: dropdown
87
+ id: transport
88
+ attributes:
89
+ label: Transport used
90
+ options:
91
+ - Local MQTT (YarboLocalClient / same WiFi as robot)
92
+ - Cloud API (YarboCloudClient)
93
+ - Hybrid (YarboClient)
94
+ - Discovery (discover_yarbo)
95
+ validations:
96
+ required: true
97
+
98
+ - type: input
99
+ id: yarbo-model
100
+ attributes:
101
+ label: Yarbo model
102
+ placeholder: "e.g. Yarbo G1, Yarbo S1 (snow blower)"
103
+
104
+ - type: textarea
105
+ id: additional
106
+ attributes:
107
+ label: Additional context
108
+ description: |
109
+ Anything else that might help — logs (redact credentials/IPs/SNs),
110
+ MQTT captures, code snippets, etc.
@@ -0,0 +1,61 @@
1
+ name: Feature Request
2
+ description: Suggest a new feature or enhancement for python-yarbo.
3
+ title: "[Feature]: "
4
+ labels: ["enhancement"]
5
+ assignees:
6
+ - markus-lassfolk
7
+
8
+ body:
9
+ - type: markdown
10
+ attributes:
11
+ value: |
12
+ Got an idea to make python-yarbo better? Great — describe it below!
13
+
14
+ - type: textarea
15
+ id: problem
16
+ attributes:
17
+ label: Problem or motivation
18
+ description: Is your feature request related to a problem? Describe it.
19
+ placeholder: "I find it frustrating when..."
20
+ validations:
21
+ required: true
22
+
23
+ - type: textarea
24
+ id: solution
25
+ attributes:
26
+ label: Proposed solution
27
+ description: A clear description of what you want to happen.
28
+ validations:
29
+ required: true
30
+
31
+ - type: textarea
32
+ id: alternatives
33
+ attributes:
34
+ label: Alternatives considered
35
+ description: Have you considered any alternative solutions or workarounds?
36
+
37
+ - type: textarea
38
+ id: api-sketch
39
+ attributes:
40
+ label: API sketch (optional)
41
+ description: If you have an idea of what the Python API would look like, sketch it here.
42
+ render: python
43
+ placeholder: |
44
+ # Example:
45
+ async with YarboClient(broker="192.168.1.24", sn="...") as client:
46
+ await client.start_plan("morning-route")
47
+ async for event in client.watch_plan_events():
48
+ print(event.status)
49
+
50
+ - type: checkboxes
51
+ id: willing-to-pr
52
+ attributes:
53
+ label: Would you like to implement this?
54
+ options:
55
+ - label: Yes, I'd like to submit a PR for this feature.
56
+
57
+ - type: textarea
58
+ id: additional
59
+ attributes:
60
+ label: Additional context
61
+ description: Any other context, links to protocol docs, or related issues.
@@ -0,0 +1,48 @@
1
+ ## Summary
2
+
3
+ <!-- Describe your changes in a few sentences. Link the related issue(s). -->
4
+
5
+ Closes #<!-- issue number -->
6
+
7
+ ## Type of change
8
+
9
+ - [ ] 🐛 Bug fix (non-breaking change that fixes an issue)
10
+ - [ ] ✨ New feature (non-breaking change that adds functionality)
11
+ - [ ] 💥 Breaking change (fix or feature that would cause existing functionality to change)
12
+ - [ ] 🧹 Refactor / code cleanup (no behaviour change)
13
+ - [ ] 📝 Documentation update
14
+ - [ ] ⚙️ CI/CD / tooling change
15
+
16
+ ## Changes made
17
+
18
+ <!-- Bullet-point summary of what changed and why. -->
19
+
20
+ -
21
+ -
22
+
23
+ ## Testing
24
+
25
+ <!-- Describe how you tested this. Include commands you ran. -->
26
+
27
+ - [ ] Added / updated pytest tests
28
+ - [ ] All tests pass locally (`pytest tests/`)
29
+ - [ ] Tested manually against Yarbo hardware (if applicable — describe setup below)
30
+
31
+ ## Quality checklist
32
+
33
+ - [ ] **ruff lint**: Zero errors (`ruff check src/ tests/`)
34
+ - [ ] **ruff format**: No formatting issues (`ruff format --check src/ tests/`)
35
+ - [ ] **mypy**: Zero new type errors (`mypy src/yarbo/`)
36
+ - [ ] **Tests pass**: `pytest tests/` — all green
37
+ - [ ] **CHANGELOG.md** updated under `[Unreleased]`
38
+ - [ ] **Docs updated** (docstrings, README if API changed)
39
+ - [ ] **No secrets, credentials, IP addresses, or serial numbers** in code, comments, or commits
40
+ - [ ] **Follows coding standards** in [CONTRIBUTING.md](../CONTRIBUTING.md)
41
+
42
+ ## Screenshots / output (optional)
43
+
44
+ <!-- Paste relevant terminal output, before/after if helpful. Redact any credentials/IPs. -->
45
+
46
+ ---
47
+
48
+ > **Reviewer note**: Branch must be up-to-date with `develop` and all CI checks must pass before merge.
@@ -0,0 +1,76 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ python-yarbo is under active development. Security fixes are applied to the **latest release** only.
6
+
7
+ | Version | Supported |
8
+ | ------- | ------------------ |
9
+ | 0.x | ✅ Latest release |
10
+ | < 0.1 | ❌ Not supported |
11
+
12
+ ## Reporting a Vulnerability
13
+
14
+ **Please do not report security vulnerabilities through public GitHub issues.**
15
+
16
+ If you discover a security vulnerability in python-yarbo, please report it privately:
17
+
18
+ 1. **GitHub Private Advisory** *(preferred)*: Go to
19
+ [Security → Advisories](https://github.com/markus-lassfolk/python-yarbo/security/advisories/new)
20
+ and click "Report a vulnerability".
21
+
22
+ 2. **Email**: If you cannot use GitHub Advisories, contact the maintainer directly.
23
+ Include "[python-yarbo Security]" in the subject line.
24
+
25
+ ### What to include
26
+
27
+ - A description of the vulnerability and its potential impact
28
+ - Steps to reproduce or a proof-of-concept (if safe to share)
29
+ - Affected versions
30
+ - Any suggested mitigations
31
+
32
+ ### Response timeline
33
+
34
+ | Milestone | Target SLA |
35
+ | ------------------------------ | -------------------- |
36
+ | Acknowledgement of report | 48 hours |
37
+ | Initial assessment | 5 days |
38
+ | Fix / patch (if confirmed) | 30 days |
39
+ | Public disclosure | After fix is released |
40
+
41
+ ## Security Considerations
42
+
43
+ python-yarbo communicates with Yarbo robot mowers over **local MQTT (plaintext)**.
44
+ Keep the following in mind:
45
+
46
+ - **Credentials**: Never hard-code passwords, API keys, or serial numbers in code.
47
+ Use environment variables or a secrets manager. The RSA public key extracted from
48
+ the APK is not sensitive and can be committed; private keys and passwords must not be.
49
+
50
+ - **Network exposure**: The Yarbo EMQX broker listens on port 1883 (plaintext) on
51
+ the local network. Do **not** expose this port to the internet. Restrict access
52
+ with your router's firewall.
53
+
54
+ - **MQTT authentication**: The local EMQX broker appears to accept anonymous connections.
55
+ Treat the local network as a trust boundary — ensure only authorised devices have WiFi access.
56
+
57
+ - **Serial numbers**: Your Yarbo serial number (SN) is used as an MQTT topic component.
58
+ It is not a secret, but avoid sharing it publicly to reduce exposure.
59
+
60
+ - **Cloud API**: The cloud REST API is migrating to AWS SigV4 auth. JWT tokens
61
+ (30-day lifetime) should be treated as sensitive credentials.
62
+
63
+ ## Scope
64
+
65
+ The following are **in scope** for security reports:
66
+
67
+ - Credential exposure or insecure credential handling in library code
68
+ - Unsafe use of `eval`, `exec`, or similar constructs
69
+ - Insecure defaults that could expose user credentials or device access
70
+
71
+ The following are **out of scope**:
72
+
73
+ - Vulnerabilities in the Yarbo firmware or cloud service (report to Yarbo directly)
74
+ - MQTT broker configuration issues (report to your broker vendor)
75
+ - Issues only reproducible on Python versions below 3.11 (unsupported)
76
+ - Issues in third-party dependencies (report upstream; we will update dependencies)
@@ -0,0 +1,4 @@
1
+ review:
2
+ auto_review: true
3
+ auto_fix: true
4
+ auto_merge: false
@@ -0,0 +1,31 @@
1
+ version: 2
2
+
3
+ updates:
4
+ # Python dependencies
5
+ - package-ecosystem: pip
6
+ directory: /
7
+ schedule:
8
+ interval: weekly
9
+ day: monday
10
+ time: "08:00"
11
+ timezone: Europe/Stockholm
12
+ open-pull-requests-limit: 5
13
+ labels:
14
+ - "dependencies"
15
+ commit-message:
16
+ prefix: "chore(deps)"
17
+
18
+ # GitHub Actions
19
+ - package-ecosystem: github-actions
20
+ directory: /
21
+ schedule:
22
+ interval: weekly
23
+ day: monday
24
+ time: "08:00"
25
+ timezone: Europe/Stockholm
26
+ open-pull-requests-limit: 5
27
+ labels:
28
+ - "dependencies"
29
+ - "ci/cd"
30
+ commit-message:
31
+ prefix: "chore(actions)"
@@ -0,0 +1,93 @@
1
+ name: Auto-create PRs for Bot Branches
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - 'bugfix/**'
7
+ - 'fix/**'
8
+ - 'hotfix/**'
9
+ - 'copilot-fix/**'
10
+ - 'cursor-agent/**'
11
+
12
+ permissions:
13
+ contents: read
14
+ pull-requests: write
15
+
16
+ jobs:
17
+ create-pr:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - name: Create PR if missing
21
+ uses: actions/github-script@v7
22
+ with:
23
+ github-token: ${{ secrets.GITHUB_TOKEN }}
24
+ script: |
25
+ const branch = context.ref.replace('refs/heads/', '');
26
+ const owner = context.repo.owner;
27
+ const repo = context.repo.repo;
28
+ const actor = context.actor;
29
+
30
+ console.log(`Pushed branch: ${branch} by ${actor}`);
31
+
32
+ // Only act if the author is a known bot/agent
33
+ const BOT_AUTHORS = new Set([
34
+ 'copilot',
35
+ 'cursor',
36
+ 'cursor-agent',
37
+ 'github-actions'
38
+ ]);
39
+
40
+ // context.actor for apps is often the app name without the [bot] suffix
41
+ const isBotAuthor = BOT_AUTHORS.has(actor) || actor.endsWith('[bot]');
42
+
43
+ if (!isBotAuthor) {
44
+ console.log('Author is not a bot. Skipping auto-PR creation.');
45
+ return;
46
+ }
47
+
48
+ // Check if a PR already exists for this branch
49
+ const { data: pulls } = await github.rest.pulls.list({
50
+ owner,
51
+ repo,
52
+ head: `${owner}:${branch}`,
53
+ state: 'open'
54
+ });
55
+
56
+ if (pulls.length > 0) {
57
+ console.log(`PR already exists for branch ${branch}: #${pulls[0].number}`);
58
+ return;
59
+ }
60
+
61
+ // Get default branch to target
62
+ const { data: repoInfo } = await github.rest.repos.get({ owner, repo });
63
+ const defaultBranch = repoInfo.default_branch;
64
+
65
+ // Format title from branch name (e.g. bugfix/autofix-test -> Bugfix/autofix test)
66
+ let title = branch.split('/').pop().replace(/-/g, ' ');
67
+ title = title.charAt(0).toUpperCase() + title.slice(1);
68
+
69
+ if (branch.startsWith('bugfix/') || branch.startsWith('fix/')) {
70
+ title = `Fix: ${title}`;
71
+ }
72
+
73
+ console.log(`Creating PR for branch ${branch} targeting develop`);
74
+
75
+ try {
76
+ const { data: newPr } = await github.rest.pulls.create({
77
+ owner,
78
+ repo,
79
+ title: title,
80
+ head: branch,
81
+ base: "develop",
82
+ body: `🤖 **Auto-created PR**\n\nThis branch (\`${branch}\`) was pushed by an AI agent/bot (\`${actor}\`), but no PR was opened. This PR was automatically created so CI can run and the \`copilot-automerge\` workflow can process it.`
83
+ });
84
+
85
+ console.log(`✅ Created PR #${newPr.number}`);
86
+ } catch (error) {
87
+ console.error(`❌ Failed to create PR: ${error.message}`);
88
+ if (error.status === 422) {
89
+ console.log("This usually means there are no commits between the base branch and the head branch.");
90
+ } else {
91
+ throw error;
92
+ }
93
+ }
@@ -0,0 +1,103 @@
1
+ name: CI
2
+
3
+ # Run on every push to main/develop and every pull request (any target branch).
4
+ # workflow_dispatch allows manual run from the Actions tab if triggers don't fire.
5
+ on:
6
+ push:
7
+ branches: [main, develop]
8
+ pull_request: {}
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: read
13
+ checks: write
14
+
15
+ jobs:
16
+ lint:
17
+ name: Lint (Python ${{ matrix.python-version }})
18
+ runs-on: ubuntu-latest
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ python-version: ["3.11", "3.12", "3.13"]
23
+
24
+ steps:
25
+ - uses: actions/checkout@v6
26
+
27
+ - name: Set up Python ${{ matrix.python-version }}
28
+ uses: actions/setup-python@v6
29
+ with:
30
+ python-version: ${{ matrix.python-version }}
31
+ cache: pip
32
+
33
+ - name: Install dev dependencies
34
+ run: pip install -e ".[dev]"
35
+
36
+ - name: Ruff lint
37
+ run: ruff check src/ tests/
38
+
39
+ - name: Ruff format check
40
+ run: ruff format --check src/ tests/
41
+
42
+ - name: mypy type-check
43
+ run: mypy src/yarbo/
44
+
45
+ test:
46
+ name: test (Python ${{ matrix.python-version }})
47
+ runs-on: ubuntu-latest
48
+ needs: lint
49
+ strategy:
50
+ fail-fast: false
51
+ matrix:
52
+ python-version: ["3.11", "3.12", "3.13"]
53
+
54
+ steps:
55
+ - uses: actions/checkout@v6
56
+
57
+ - name: Set up Python ${{ matrix.python-version }}
58
+ uses: actions/setup-python@v6
59
+ with:
60
+ python-version: ${{ matrix.python-version }}
61
+ cache: pip
62
+
63
+ - name: Install dev dependencies
64
+ run: pip install -e ".[dev]"
65
+
66
+ - name: Run pytest with coverage
67
+ run: |
68
+ pytest --cov=yarbo --cov-report=xml --cov-report=term-missing \
69
+ --tb=short -q tests/
70
+
71
+ - name: Upload coverage artifact
72
+ if: matrix.python-version == '3.12'
73
+ uses: actions/upload-artifact@v6
74
+ with:
75
+ name: coverage-report
76
+ path: coverage.xml
77
+
78
+ # Uncomment when Codecov token is configured:
79
+ # - name: Upload to Codecov
80
+ # if: matrix.python-version == '3.12'
81
+ # uses: codecov/codecov-action@v4
82
+ # with:
83
+ # token: ${{ secrets.CODECOV_TOKEN }}
84
+ # files: coverage.xml
85
+ # fail_ci_if_error: false
86
+
87
+ # Gate jobs with stable names for branch protection (required status checks).
88
+ # Configure branch protection to require "Lint" and "Test" if you use 2 checks.
89
+ lint-gate:
90
+ name: Lint
91
+ runs-on: ubuntu-latest
92
+ needs: lint
93
+ if: always() && (needs.lint.result == 'success' || needs.lint.result == 'failure')
94
+ steps:
95
+ - run: test '${{ needs.lint.result }}' = success
96
+
97
+ test-gate:
98
+ name: Test
99
+ runs-on: ubuntu-latest
100
+ needs: test
101
+ if: always() && (needs.test.result == 'success' || needs.test.result == 'failure')
102
+ steps:
103
+ - run: test '${{ needs.test.result }}' = success
@@ -0,0 +1,112 @@
1
+ name: Auto-merge Copilot Fixes
2
+
3
+ on:
4
+ check_suite:
5
+ types: [completed]
6
+ pull_request:
7
+ types: [opened, synchronize]
8
+
9
+ permissions:
10
+ contents: write
11
+ checks: read
12
+ pull-requests: write
13
+
14
+ jobs:
15
+ automerge:
16
+ runs-on: ubuntu-latest
17
+ if: >
18
+ github.event_name == 'check_suite' &&
19
+ github.event.check_suite.conclusion == 'success' ||
20
+ github.event_name == 'pull_request'
21
+ steps:
22
+ - name: Auto-merge eligible bot fix PRs
23
+ uses: actions/github-script@v7
24
+ with:
25
+ github-token: ${{ secrets.GITHUB_TOKEN }}
26
+ script: |
27
+ const owner = context.repo.owner;
28
+ const repo = context.repo.repo;
29
+
30
+ // Bot authors that are allowed to auto-merge
31
+ const BOT_AUTHORS = new Set([
32
+ 'copilot[bot]',
33
+ 'cursor[bot]',
34
+ 'cursor-agent',
35
+ 'github-actions[bot]',
36
+ ]);
37
+
38
+ // Branch prefixes eligible for auto-merge (only when authored by bots)
39
+ const FIX_PREFIXES = [
40
+ 'copilot-fix/', 'cursor-agent/',
41
+ 'bugfix/', 'fix/', 'hotfix/',
42
+ ];
43
+
44
+ const { data: pulls } = await github.rest.pulls.list({
45
+ owner, repo, state: 'open', per_page: 50
46
+ });
47
+
48
+ const eligible = pulls.filter(pr => {
49
+ const branch = pr.head.ref;
50
+ const author = pr.user.login;
51
+ const isBotAuthor = BOT_AUTHORS.has(author);
52
+ const isFixBranch = FIX_PREFIXES.some(p => branch.startsWith(p));
53
+ return isBotAuthor && isFixBranch;
54
+ });
55
+
56
+ if (eligible.length === 0) {
57
+ console.log('No eligible bot fix PRs found');
58
+ return;
59
+ }
60
+
61
+ for (const pr of eligible) {
62
+ console.log(`Checking PR #${pr.number}: ${pr.title} (${pr.head.ref}) by ${pr.user.login}`);
63
+
64
+ const { data: checks } = await github.rest.checks.listForRef({
65
+ owner, repo, ref: pr.head.sha
66
+ });
67
+
68
+ const allChecks = checks.check_runs.filter(
69
+ c => c.name !== 'automerge' && c.name !== 'Auto-merge Copilot Fixes'
70
+ );
71
+
72
+ if (allChecks.length === 0) {
73
+ console.log(` No checks found yet, skipping`);
74
+ continue;
75
+ }
76
+
77
+ const pending = allChecks.filter(c => c.status !== 'completed');
78
+ const failed = allChecks.filter(
79
+ c => c.status === 'completed' &&
80
+ c.conclusion !== 'success' &&
81
+ c.conclusion !== 'neutral' &&
82
+ c.conclusion !== 'skipped'
83
+ );
84
+
85
+ if (pending.length > 0) {
86
+ console.log(` ${pending.length} checks still pending, skipping`);
87
+ continue;
88
+ }
89
+
90
+ if (failed.length > 0) {
91
+ console.log(` ${failed.length} checks failed: ${failed.map(c => c.name).join(', ')}`);
92
+ await github.rest.issues.createComment({
93
+ owner, repo, issue_number: pr.number,
94
+ body: `⚠️ Auto-merge skipped — ${failed.length} check(s) failed: ${failed.map(c => '`' + c.name + '`').join(', ')}.\n\nPlease fix manually or close this PR.`
95
+ });
96
+ continue;
97
+ }
98
+
99
+ console.log(` All ${allChecks.length} checks passed, squash-merging...`);
100
+ try {
101
+ await github.rest.pulls.merge({
102
+ owner, repo, pull_number: pr.number,
103
+ merge_method: 'squash',
104
+ commit_title: `${pr.title} (#${pr.number})`,
105
+ commit_message: `Auto-merged bot fix PR.\n\nCo-authored-by: ${pr.user.login}`
106
+ });
107
+ console.log(` ✅ Merged PR #${pr.number}`);
108
+ } catch (e) {
109
+ console.log(` ❌ Merge failed: ${e.message}`);
110
+ }
111
+ }
112
+ issues: write