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.
- python_yarbo-0.1.0/.editorconfig +17 -0
- python_yarbo-0.1.0/.github/CODEOWNERS +4 -0
- python_yarbo-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
- python_yarbo-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +61 -0
- python_yarbo-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +48 -0
- python_yarbo-0.1.0/.github/SECURITY.md +76 -0
- python_yarbo-0.1.0/.github/copilot.yml +4 -0
- python_yarbo-0.1.0/.github/dependabot.yml +31 -0
- python_yarbo-0.1.0/.github/workflows/auto-create-pr.yml +93 -0
- python_yarbo-0.1.0/.github/workflows/ci.yml +103 -0
- python_yarbo-0.1.0/.github/workflows/copilot-automerge.yml +112 -0
- python_yarbo-0.1.0/.github/workflows/release.yml +122 -0
- python_yarbo-0.1.0/.github/workflows/security.yml +56 -0
- python_yarbo-0.1.0/.gitignore +62 -0
- python_yarbo-0.1.0/.pre-commit-config.yaml +37 -0
- python_yarbo-0.1.0/CHANGELOG.md +66 -0
- python_yarbo-0.1.0/CONTRIBUTING.md +168 -0
- python_yarbo-0.1.0/LICENSE +21 -0
- python_yarbo-0.1.0/PKG-INFO +306 -0
- python_yarbo-0.1.0/README.md +264 -0
- python_yarbo-0.1.0/docs/api.md +154 -0
- python_yarbo-0.1.0/docs/index.md +44 -0
- python_yarbo-0.1.0/examples/basic_control.py +67 -0
- python_yarbo-0.1.0/examples/cloud_login.py +82 -0
- python_yarbo-0.1.0/examples/telemetry_stream.py +54 -0
- python_yarbo-0.1.0/pyproject.toml +99 -0
- python_yarbo-0.1.0/ruff.toml +49 -0
- python_yarbo-0.1.0/src/yarbo/__init__.py +122 -0
- python_yarbo-0.1.0/src/yarbo/_codec.py +63 -0
- python_yarbo-0.1.0/src/yarbo/auth.py +244 -0
- python_yarbo-0.1.0/src/yarbo/client.py +695 -0
- python_yarbo-0.1.0/src/yarbo/cloud.py +288 -0
- python_yarbo-0.1.0/src/yarbo/cloud_mqtt.py +109 -0
- python_yarbo-0.1.0/src/yarbo/const.py +197 -0
- python_yarbo-0.1.0/src/yarbo/discovery.py +217 -0
- python_yarbo-0.1.0/src/yarbo/error_reporting.py +83 -0
- python_yarbo-0.1.0/src/yarbo/exceptions.py +112 -0
- python_yarbo-0.1.0/src/yarbo/keys/README.md +41 -0
- python_yarbo-0.1.0/src/yarbo/local.py +1636 -0
- python_yarbo-0.1.0/src/yarbo/models.py +787 -0
- python_yarbo-0.1.0/src/yarbo/mqtt.py +487 -0
- python_yarbo-0.1.0/tests/conftest.py +152 -0
- python_yarbo-0.1.0/tests/test_auth.py +161 -0
- python_yarbo-0.1.0/tests/test_client.py +138 -0
- python_yarbo-0.1.0/tests/test_cloud.py +114 -0
- python_yarbo-0.1.0/tests/test_cloud_mqtt.py +158 -0
- python_yarbo-0.1.0/tests/test_codec.py +71 -0
- python_yarbo-0.1.0/tests/test_discovery.py +56 -0
- python_yarbo-0.1.0/tests/test_error_reporting.py +93 -0
- python_yarbo-0.1.0/tests/test_local.py +687 -0
- python_yarbo-0.1.0/tests/test_models.py +468 -0
- python_yarbo-0.1.0/tests/test_mqtt.py +547 -0
- python_yarbo-0.1.0/tests/test_typed_commands.py +685 -0
- 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,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,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
|