ferrygl 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.
- ferrygl-0.1.0/.gitignore +12 -0
- ferrygl-0.1.0/AGENTS.md +164 -0
- ferrygl-0.1.0/LICENSE +21 -0
- ferrygl-0.1.0/PKG-INFO +318 -0
- ferrygl-0.1.0/README.md +296 -0
- ferrygl-0.1.0/pyproject.toml +51 -0
- ferrygl-0.1.0/src/ferrygl/__init__.py +3 -0
- ferrygl-0.1.0/src/ferrygl/__main__.py +3 -0
- ferrygl-0.1.0/src/ferrygl/_http.py +81 -0
- ferrygl-0.1.0/src/ferrygl/cli.py +92 -0
- ferrygl-0.1.0/src/ferrygl/config.py +271 -0
- ferrygl-0.1.0/src/ferrygl/github.py +272 -0
- ferrygl-0.1.0/src/ferrygl/linear.py +272 -0
- ferrygl-0.1.0/src/ferrygl/model.py +123 -0
- ferrygl-0.1.0/src/ferrygl/plan.py +412 -0
- ferrygl-0.1.0/src/ferrygl/sync.py +306 -0
- ferrygl-0.1.0/tests/builders.py +132 -0
- ferrygl-0.1.0/tests/test_config.py +118 -0
- ferrygl-0.1.0/tests/test_github.py +278 -0
- ferrygl-0.1.0/tests/test_linear.py +155 -0
- ferrygl-0.1.0/tests/test_plan.py +467 -0
- ferrygl-0.1.0/tests/test_sync.py +216 -0
- ferrygl-0.1.0/uv.lock +188 -0
ferrygl-0.1.0/.gitignore
ADDED
ferrygl-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Working on ferryGL
|
|
2
|
+
|
|
3
|
+
These rules are for anyone changing ferryGL, including coding agents. Read the
|
|
4
|
+
[README](README.md) first: it is the user-facing contract for behavior, configuration and
|
|
5
|
+
limits. This file adds the invariants, layout and workflow that keep changes safe.
|
|
6
|
+
|
|
7
|
+
ferryGL copies every issue and pull request a person's GitHub account is part of (created,
|
|
8
|
+
assigned, reviewed, review requested, or with their commits) one way into their own Linear
|
|
9
|
+
workspace, using only their own credentials. Its users usually work in repositories owned by someone else, often private,
|
|
10
|
+
so the safety rules below matter more than features.
|
|
11
|
+
|
|
12
|
+
## Invariants
|
|
13
|
+
|
|
14
|
+
Don't break these without an explicit decision from the project owner.
|
|
15
|
+
|
|
16
|
+
1. **GitHub is read-only.** `src/ferrygl/github.py` only runs GraphQL queries. Never add a
|
|
17
|
+
mutation, comment, label or status write to GitHub.
|
|
18
|
+
2. **Only the user's Linear issues.** ferryGL changes a Linear issue only if it is assigned
|
|
19
|
+
to the API key's owner (`viewer_id`). It never deletes issues or links, and never edits
|
|
20
|
+
titles or descriptions after creating an issue.
|
|
21
|
+
3. **The user's edits win.** A Linear status moves only when the GitHub state recorded on the
|
|
22
|
+
link changes. A link with no recorded state is a first sync, which may close an issue
|
|
23
|
+
(done or canceled) but never reopens or otherwise moves it.
|
|
24
|
+
4. **One home per item, one status source per Linear issue.** Each GitHub URL maps to at most
|
|
25
|
+
one of the user's Linear issues. A pull request that rides with an issue doesn't set that
|
|
26
|
+
Linear issue's status on its own.
|
|
27
|
+
5. **Reruns are idempotent.** Existing Linear issues are found by their GitHub links
|
|
28
|
+
(`attachmentsForURL`), so a rerun never duplicates. Only idempotent requests are retried
|
|
29
|
+
after network errors or 5xx responses; `issueCreate` is not.
|
|
30
|
+
6. **The planner is pure.** `src/ferrygl/plan.py` does no I/O. All reads happen before
|
|
31
|
+
planning, and all writes happen after it, in `src/ferrygl/sync.py`.
|
|
32
|
+
7. **Copy as little as possible.** By default only titles, numbers, repositories, URLs, states
|
|
33
|
+
and the user's role reach Linear. Descriptions are copied only when `[copy] description`
|
|
34
|
+
is on. Never copy comments or other people's names.
|
|
35
|
+
8. **No surprise writes.** `ferrygl sync` without `--apply` must write nothing. A setup
|
|
36
|
+
problem (missing team, project, state or label) stops the run before GitHub is read or
|
|
37
|
+
Linear is written.
|
|
38
|
+
|
|
39
|
+
## Retained data and public contracts
|
|
40
|
+
|
|
41
|
+
Users' Linear workspaces keep what ferryGL writes, so treat these as compatibility contracts:
|
|
42
|
+
|
|
43
|
+
- **Link metadata** uses flat keys with string or number values, because Linear documents
|
|
44
|
+
attachment metadata that way:
|
|
45
|
+
- `ferrygl_v`: format version, currently `1`
|
|
46
|
+
- `ferrygl_kind`: `issue`, `pr` (one you opened) or `involved` (someone else's)
|
|
47
|
+
- `ferrygl_state`: the GitHub state key ferryGL last acted on
|
|
48
|
+
- `ferrygl_synced_at`: ISO timestamp of that state
|
|
49
|
+
- `ferrygl_linked_to`: on links of pull requests that ride with an issue, such as
|
|
50
|
+
`owner/repo#12`
|
|
51
|
+
|
|
52
|
+
ferryGL replaces only its own `ferrygl_*` keys and keeps any others.
|
|
53
|
+
- **State keys** (`github_state` in `plan.py`): `issue:open`, `issue:open:pr_draft`,
|
|
54
|
+
`issue:open:pr_open`, `issue:open:pr_merged`, `issue:closed:completed`,
|
|
55
|
+
`issue:closed:not_planned`, `pr:draft`, `pr:open`, `pr:merged`, `pr:closed`,
|
|
56
|
+
`involved:requested`, `involved:reviewed`, `involved:draft`, `involved:open`,
|
|
57
|
+
`involved:merged`, `involved:closed`. Renaming one makes every recorded link look changed.
|
|
58
|
+
Add a key, or bump `ferrygl_v` and handle the old value.
|
|
59
|
+
- **Config keys** (`config.py`): unknown keys are rejected, so adding a key is safe but
|
|
60
|
+
renaming or removing one breaks existing files. Keep the old name working, with a clear
|
|
61
|
+
error or warning, for at least one release.
|
|
62
|
+
- **CLI:** commands `check` and `sync [--apply]`, `--config`, `FERRYGL_CONFIG`,
|
|
63
|
+
`LINEAR_API_KEY`, and exit statuses 0 (ok), 1 (a route or write failed) and 2 (setup or
|
|
64
|
+
config error).
|
|
65
|
+
|
|
66
|
+
## Layout
|
|
67
|
+
|
|
68
|
+
| Path | Responsibility |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `src/ferrygl/config.py` | Parse and validate `ferrygl.toml` into frozen dataclasses |
|
|
71
|
+
| `src/ferrygl/github.py` | Resolve tokens, run one GitHub search per role, scan commit authors, parse items |
|
|
72
|
+
| `src/ferrygl/linear.py` | Linear GraphQL reads and writes with a personal API key |
|
|
73
|
+
| `src/ferrygl/plan.py` | Pure planning: GitHub items + existing Linear state -> changes |
|
|
74
|
+
| `src/ferrygl/sync.py` | `check` and `sync`: resolve routes, plan, print, apply |
|
|
75
|
+
| `src/ferrygl/model.py` | Shared types: `Kind`, `Role`, `GitHubItem`, `LinearIssue`, `Attachment`, `Change` |
|
|
76
|
+
| `src/ferrygl/_http.py` | JSON POST with retries for both clients |
|
|
77
|
+
| `tests/builders.py` | Builders for synthetic items, Linear issues and links |
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```console
|
|
82
|
+
$ uv sync
|
|
83
|
+
$ uv run pytest
|
|
84
|
+
$ uv run ruff check && uv run ruff format --check
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- Python 3.11 is the oldest supported version, and CI also runs 3.14. Don't use syntax newer
|
|
88
|
+
than 3.11.
|
|
89
|
+
- The only runtime dependency is `httpx`. Discuss any new dependency with the project owner
|
|
90
|
+
before adding it.
|
|
91
|
+
- Match the existing style: small modules, frozen dataclasses, type hints, comments that
|
|
92
|
+
explain why, and error messages that tell the user what to do next.
|
|
93
|
+
|
|
94
|
+
## Testing
|
|
95
|
+
|
|
96
|
+
- Every change to planning behavior needs a test in `tests/test_plan.py` that states the
|
|
97
|
+
scenario in plain terms: what's on GitHub, what's in Linear, and the expected changes.
|
|
98
|
+
- Client changes are tested with `httpx.MockTransport`; end-to-end behavior uses the fakes in
|
|
99
|
+
`tests/test_sync.py`.
|
|
100
|
+
- **Use synthetic data only** (`acme/api`, `ENG-1`). Never put real repository names, issue
|
|
101
|
+
titles, Linear identifiers, tokens or API responses from a user's workspace in tests,
|
|
102
|
+
fixtures, docs, commits or PR descriptions. Users' repositories are often private and
|
|
103
|
+
belong to their clients.
|
|
104
|
+
- Check new or changed Linear queries against Linear's published schema
|
|
105
|
+
(`packages/sdk/src/schema.graphql` in `linear/linear` on GitHub) for field names, argument
|
|
106
|
+
names and types such as `String!` vs `ID!`.
|
|
107
|
+
- Live checks: run read-only commands (`ferrygl check`, `ferrygl sync` without `--apply`)
|
|
108
|
+
against a real workspace only with its owner's credentials and permission. Never run
|
|
109
|
+
`--apply` against a workspace unless its owner asks for that specific run.
|
|
110
|
+
|
|
111
|
+
## Documentation
|
|
112
|
+
|
|
113
|
+
The README is part of every behavior change. Update it in the same change when you alter the
|
|
114
|
+
status rules, adoption rules, config keys, defaults, exit statuses, data copied or supported
|
|
115
|
+
limits. Describe current behavior only: history belongs in Git, and progress notes belong in
|
|
116
|
+
issues and pull requests. Mark planned features as planned.
|
|
117
|
+
|
|
118
|
+
## Git and GitHub
|
|
119
|
+
|
|
120
|
+
- Work on a branch and open a pull request against `main`. Keep each pull request to one
|
|
121
|
+
concern, and say in its description what changed for users and how it was tested.
|
|
122
|
+
- Never push to `main` directly. GitHub can't enforce this while the repository is private on
|
|
123
|
+
a free plan, so a local `pre-push` hook may block it; don't bypass it with `--no-verify`.
|
|
124
|
+
- This checkout may have a repo-local git identity and credential helper. Don't change them.
|
|
125
|
+
When the machine has several GitHub accounts, run `gh` as the repository owner's account,
|
|
126
|
+
for example `GH_TOKEN=$(gh auth token --user <owner>) gh ...`.
|
|
127
|
+
- Don't add AI or tool attribution to commits, pull requests or comments: no `Co-Authored-By`
|
|
128
|
+
trailers or "generated with" footers. Work is published under the owner's name.
|
|
129
|
+
- These need the project owner's explicit approval each time: merging to `main`, making the
|
|
130
|
+
repository public, publishing a release or a PyPI package, and changing the license.
|
|
131
|
+
|
|
132
|
+
## Releasing
|
|
133
|
+
|
|
134
|
+
Releases need the project owner's approval each time.
|
|
135
|
+
|
|
136
|
+
1. In a pull request, set `__version__` in `src/ferrygl/__init__.py` (the only place the
|
|
137
|
+
version lives) and update the README for anything users will notice. Merge it.
|
|
138
|
+
2. Tag the merge commit on `main` with `v<version>`, such as `v0.2.0`, and push the tag.
|
|
139
|
+
3. `.github/workflows/release.yml` tests and builds the package, checks that the tag matches
|
|
140
|
+
the version, publishes to PyPI through trusted publishing (no token is stored), and
|
|
141
|
+
creates a GitHub release with the built files.
|
|
142
|
+
|
|
143
|
+
PyPI never accepts the same version twice. If a release is broken, publish a new patch
|
|
144
|
+
version rather than trying to replace it.
|
|
145
|
+
|
|
146
|
+
## Working agreement
|
|
147
|
+
|
|
148
|
+
- Settle routine implementation choices yourself, within these rules.
|
|
149
|
+
- Bring consequential decisions (a contract change, a new dependency, anything that changes
|
|
150
|
+
what reaches a user's Linear workspace) to the owner with a recommendation, the tradeoff and
|
|
151
|
+
the evidence.
|
|
152
|
+
- When handing work back, say what changed, how it was tested, what wasn't tested, and the
|
|
153
|
+
next decision needed.
|
|
154
|
+
|
|
155
|
+
## Current state
|
|
156
|
+
|
|
157
|
+
- `check` and `sync` are implemented and tested with fixtures. GitHub reads have been
|
|
158
|
+
exercised against a real repository. Reads and writes against a real Linear workspace
|
|
159
|
+
haven't been exercised yet, so do a read-only preview before the first release.
|
|
160
|
+
- Planned: a scheduled GitHub Actions template.
|
|
161
|
+
- Finding pull requests with the user's commits scans every pull request in scope, because
|
|
162
|
+
GitHub search has no commit-author qualifier for pull requests. Keep that scan bounded by
|
|
163
|
+
`since`, and watch its cost when changing it.
|
|
164
|
+
- Open question: whether comments or mentions should count as roles.
|
ferrygl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oussama Batouche
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ferrygl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ferrygl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Copy your GitHub work into your own Linear workspace, from repositories you can read but don't administer.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ao-batouche/ferryGL
|
|
6
|
+
Project-URL: Repository, https://github.com/ao-batouche/ferryGL
|
|
7
|
+
Project-URL: Issues, https://github.com/ao-batouche/ferryGL/issues
|
|
8
|
+
Author: Oussama Batouche
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: github,issues,linear,pull-requests,sync
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Software Development :: Bug Tracking
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<img src="assets/ferrygl-logo-horizontal.png" alt="ferryGL" width="560">
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
# ferryGL
|
|
28
|
+
|
|
29
|
+
Ferry your GitHub work into your own Linear workspace, from repositories you can read but
|
|
30
|
+
don't administer.
|
|
31
|
+
|
|
32
|
+
> **Status: early version (0.1).** `ferrygl check` and `ferrygl sync` work, and are tested
|
|
33
|
+
> with local fixtures and against a real GitHub repository.
|
|
34
|
+
>
|
|
35
|
+
> ferryGL is an independent project. It is not affiliated with or endorsed by Linear or GitHub.
|
|
36
|
+
|
|
37
|
+
## Who it's for
|
|
38
|
+
|
|
39
|
+
You write code in someone else's GitHub organization (a client, a partner, an upstream
|
|
40
|
+
project), but your team plans and reports work in its own Linear workspace.
|
|
41
|
+
|
|
42
|
+
Linear's GitHub integration doesn't cover this case:
|
|
43
|
+
|
|
44
|
+
- It must be installed by an owner, or a repository admin, of the GitHub organization.
|
|
45
|
+
- A GitHub organization can connect to only one Linear workspace. If the organization uses
|
|
46
|
+
Linear itself, your workspace can't connect to it.
|
|
47
|
+
|
|
48
|
+
So you copy issues and pull requests into Linear by hand and move their status as they
|
|
49
|
+
progress. ferryGL does that for you with your own credentials. It never writes to GitHub.
|
|
50
|
+
|
|
51
|
+
## What it does
|
|
52
|
+
|
|
53
|
+
On each run, ferryGL finds every issue and pull request your GitHub account is part of in
|
|
54
|
+
the repositories you list, and makes sure each one has exactly one Linear issue, assigned to
|
|
55
|
+
you, with the right status.
|
|
56
|
+
|
|
57
|
+
| Your role on GitHub | How ferryGL finds it | Default Linear title |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| You created an issue, or are assigned to it | `is:issue author:<you>`, `is:issue assignee:<you>` | `{title} #{number}` |
|
|
60
|
+
| You opened a pull request | `is:pr author:<you>` | `{title} - #{number}` |
|
|
61
|
+
| You're assigned to someone else's pull request | `is:pr assignee:<you>` | `[PR] {title} - #{number}` |
|
|
62
|
+
| You reviewed it, or your review is requested | `reviewed-by:<you>`, `user-review-requested:<you>` | `[PR] {title} - #{number}` |
|
|
63
|
+
| You wrote one of its commits | the commit authors of each pull request | `[PR] {title} - #{number}` |
|
|
64
|
+
|
|
65
|
+
An item you have several roles on still gets one Linear issue. In the config, someone else's
|
|
66
|
+
pull request you're part of is an `involved` item.
|
|
67
|
+
|
|
68
|
+
GitHub search can't find pull requests by commit author, so ferryGL reads every pull request
|
|
69
|
+
in the route updated since `since` and checks the authors, including co-authors, of its first
|
|
70
|
+
100 commits. That's the slowest part of a run. Set `since`, or turn it off with
|
|
71
|
+
`prs_with_commits = false`.
|
|
72
|
+
|
|
73
|
+
A pull request you opened that refers to one of your synced issues, through a closing keyword
|
|
74
|
+
such as `Fixes #252` or a plain `#252` in its title or description, is linked to that
|
|
75
|
+
issue's Linear issue instead of getting its own. Someone else's pull request always gets its
|
|
76
|
+
own Linear issue.
|
|
77
|
+
|
|
78
|
+
For example, you're assigned `acme/api#252` and open `acme/api#335`, whose description says
|
|
79
|
+
"Part of #252":
|
|
80
|
+
|
|
81
|
+
1. ferryGL creates one Linear issue, "Store shared configuration once #252", links both
|
|
82
|
+
GitHub URLs to it and sets it to In Review.
|
|
83
|
+
2. #335 merges while #252 stays open. The Linear issue stays In Review, because the work
|
|
84
|
+
isn't done until the GitHub issue is closed.
|
|
85
|
+
3. #252 is closed as completed. The Linear issue moves to Done.
|
|
86
|
+
|
|
87
|
+
ferryGL only changes Linear issues assigned to you. If a teammate's Linear issue links the
|
|
88
|
+
same pull request, it's left alone.
|
|
89
|
+
|
|
90
|
+
## Status rules
|
|
91
|
+
|
|
92
|
+
Linear state names are configurable. These are the defaults:
|
|
93
|
+
|
|
94
|
+
| GitHub state | Linear status |
|
|
95
|
+
|---|---|
|
|
96
|
+
| Issue open, no linked pull request open | Todo |
|
|
97
|
+
| Issue open, a linked pull request open | In Review (In Progress if every open one is a draft) |
|
|
98
|
+
| Issue open, linked pull requests merged | Unchanged |
|
|
99
|
+
| Issue closed as completed | Done |
|
|
100
|
+
| Issue closed as not planned or duplicate | Canceled |
|
|
101
|
+
| Your pull request is a draft | In Progress |
|
|
102
|
+
| Your pull request is open | In Review |
|
|
103
|
+
| Pull request merged | Done |
|
|
104
|
+
| Pull request closed without merging | Canceled |
|
|
105
|
+
| Someone else's: your review is requested, including again after you reviewed | Todo |
|
|
106
|
+
| Someone else's: you reviewed, no new request, still open | In Review |
|
|
107
|
+
| Someone else's, assigned to you or with your commits: draft | In Progress |
|
|
108
|
+
| Someone else's, assigned to you or with your commits: open | In Review |
|
|
109
|
+
|
|
110
|
+
**Your edits win.** ferryGL records the GitHub state it last acted on in the link's metadata
|
|
111
|
+
on the Linear issue, and changes the Linear status only when that GitHub state changes. If
|
|
112
|
+
you move an issue by hand, ferryGL leaves it alone until something happens on GitHub.
|
|
113
|
+
|
|
114
|
+
**Nothing is deleted.** If an item leaves your scope, for example you're unassigned or a
|
|
115
|
+
review request is withdrawn, its Linear issue stays as it is.
|
|
116
|
+
|
|
117
|
+
## Linear issues you already made by hand
|
|
118
|
+
|
|
119
|
+
Before creating a Linear issue, ferryGL looks for one of yours in the route's team that
|
|
120
|
+
already mentions the item:
|
|
121
|
+
|
|
122
|
+
- its GitHub URL is in the description or links, or
|
|
123
|
+
- its title ends with `#<number>`, when the route has a single repository and the
|
|
124
|
+
description mentions no other GitHub item.
|
|
125
|
+
|
|
126
|
+
A Linear issue already linked to a different GitHub item is about that item, and never
|
|
127
|
+
matches. Two more rules cover hand-made issues for pull requests:
|
|
128
|
+
|
|
129
|
+
- If nothing mentions a pull request you opened, a Linear issue that mentions an issue it
|
|
130
|
+
refers to counts, unless that Linear issue is linked to a pull request.
|
|
131
|
+
- If an issue assigned to you has no Linear issue but its pull request already has one, the
|
|
132
|
+
issue joins that Linear issue.
|
|
133
|
+
|
|
134
|
+
When exactly one issue matches, ferryGL links it instead of creating a new one (`adopt` in
|
|
135
|
+
the plan). It doesn't change that issue's status unless GitHub says the work is done or
|
|
136
|
+
canceled. When several match, ferryGL skips the item and lists them, so you can link the
|
|
137
|
+
right one by hand.
|
|
138
|
+
|
|
139
|
+
## What data leaves GitHub
|
|
140
|
+
|
|
141
|
+
Private repositories often belong to someone else, and their contents may be covered by an
|
|
142
|
+
agreement. By default ferryGL copies as little as possible:
|
|
143
|
+
|
|
144
|
+
- **Copied:** title, number, repository, URL, state and your role (assignee, author or
|
|
145
|
+
reviewer).
|
|
146
|
+
- **Not copied unless you turn it on:** descriptions. Comments, labels and other people's
|
|
147
|
+
names are never copied.
|
|
148
|
+
- **Stricter:** title templates such as `"{repo}#{number}"` keep titles out of Linear too.
|
|
149
|
+
|
|
150
|
+
Check that copying titles into your Linear workspace is acceptable for each repository you
|
|
151
|
+
configure.
|
|
152
|
+
|
|
153
|
+
## Setup
|
|
154
|
+
|
|
155
|
+
ferryGL needs Python 3.11 or later. Install the `ferrygl` command with any of:
|
|
156
|
+
|
|
157
|
+
```console
|
|
158
|
+
$ pipx install ferrygl
|
|
159
|
+
$ uv tool install ferrygl
|
|
160
|
+
$ pip install ferrygl
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
To try unreleased changes, install from the repository instead, for example
|
|
164
|
+
`uv tool install git+https://github.com/ao-batouche/ferryGL`.
|
|
165
|
+
|
|
166
|
+
**1. A GitHub token that can read the repositories.** ferryGL only reads issues and pull
|
|
167
|
+
requests. For each route it uses, in order:
|
|
168
|
+
|
|
169
|
+
- the GitHub CLI account named by the route's `github_user` (`gh auth token --user <name>`),
|
|
170
|
+
- otherwise `GH_TOKEN` or `GITHUB_TOKEN`,
|
|
171
|
+
- otherwise the GitHub CLI's active account.
|
|
172
|
+
|
|
173
|
+
`github_user` is for people with more than one GitHub account, such as a work account for a
|
|
174
|
+
client's organization and a personal one for open source. A fine-grained token needs read
|
|
175
|
+
access to Issues, Pull requests and Metadata. Some organizations require owner approval for
|
|
176
|
+
fine-grained tokens, or block classic ones.
|
|
177
|
+
|
|
178
|
+
**2. A Linear API key.** Create a personal API key in
|
|
179
|
+
[Linear's API settings](https://linear.app/settings/api) with read and write access. You can
|
|
180
|
+
limit it to the teams ferryGL writes to. Put it in `LINEAR_API_KEY`, or in a file named by
|
|
181
|
+
`[linear] api_key_file`.
|
|
182
|
+
|
|
183
|
+
**3. A config file.** ferryGL reads `~/.config/ferrygl/ferrygl.toml`, or the file given by
|
|
184
|
+
`--config` or `FERRYGL_CONFIG`. Each `[[route]]` sends some repositories to a Linear team and,
|
|
185
|
+
optionally, a project:
|
|
186
|
+
|
|
187
|
+
```toml
|
|
188
|
+
[[route]]
|
|
189
|
+
repos = ["acme/api", "acme/web"]
|
|
190
|
+
team = "ENG" # Linear team key or name
|
|
191
|
+
project = "Acme engagement" # optional
|
|
192
|
+
github_user = "alice-acme" # optional: read GitHub as this gh account
|
|
193
|
+
|
|
194
|
+
[[route]]
|
|
195
|
+
repos = ["upstream-org/*"] # every repository of a user or organization
|
|
196
|
+
team = "ENG"
|
|
197
|
+
project = "Upstream contributions"
|
|
198
|
+
github_user = "alice"
|
|
199
|
+
|
|
200
|
+
[sync]
|
|
201
|
+
since = 2026-09-01 # ignore items not updated since this date
|
|
202
|
+
label = "ferryGL" # optional: an existing label added to every issue ferryGL creates
|
|
203
|
+
# Roles, all true by default: issues_assigned, issues_created, prs_created, prs_assigned,
|
|
204
|
+
# prs_reviewed, review_requests, prs_with_commits
|
|
205
|
+
|
|
206
|
+
[titles] # use "{repo}#{number}" to keep titles out of Linear
|
|
207
|
+
issue = "{title} #{number}"
|
|
208
|
+
pr = "{title} - #{number}"
|
|
209
|
+
involved = "[PR] {title} - #{number}"
|
|
210
|
+
|
|
211
|
+
[copy]
|
|
212
|
+
description = false
|
|
213
|
+
|
|
214
|
+
[status] # your team's Linear workflow state names
|
|
215
|
+
todo = "Todo"
|
|
216
|
+
in_progress = "In Progress"
|
|
217
|
+
in_review = "In Review"
|
|
218
|
+
done = "Done"
|
|
219
|
+
canceled = "Canceled"
|
|
220
|
+
|
|
221
|
+
[linear]
|
|
222
|
+
api_key_file = "~/.config/ferrygl/linear-api-key"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**4. Check, preview, then apply:**
|
|
226
|
+
|
|
227
|
+
```console
|
|
228
|
+
$ ferrygl check
|
|
229
|
+
$ ferrygl sync
|
|
230
|
+
$ ferrygl sync --apply
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`check` confirms that both credentials work, every repository is readable, and the Linear
|
|
234
|
+
teams, projects, states and label exist. `sync` on its own only prints the plan, for
|
|
235
|
+
example:
|
|
236
|
+
|
|
237
|
+
```text
|
|
238
|
+
acme/api#252 issue update ENG-7 Todo -> In Review
|
|
239
|
+
acme/api#335 pr link -> ENG-7
|
|
240
|
+
acme/api#348 involved adopt -> ENG-9
|
|
241
|
+
3 changes (1 adopt, 1 link, 1 update). Run with --apply to make them.
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`sync --apply` makes those changes and prints the identifier of each Linear issue it
|
|
245
|
+
creates. Run the preview first: the first sync is the one most likely to find Linear issues
|
|
246
|
+
you made by hand, and it records the current GitHub state on each of them (`record` in the
|
|
247
|
+
plan).
|
|
248
|
+
|
|
249
|
+
## Running on a schedule
|
|
250
|
+
|
|
251
|
+
- **cron on a machine you use:**
|
|
252
|
+
|
|
253
|
+
```text
|
|
254
|
+
0 9,15 * * 1-5 ferrygl sync --apply >> ~/.cache/ferrygl.log 2>&1
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
With `[linear] api_key_file` and `github_user` set, cron needs no extra environment.
|
|
258
|
+
|
|
259
|
+
- **GitHub Actions in a repository on your own account:** a scheduled workflow with your
|
|
260
|
+
GitHub token and Linear key stored as secrets. The workflow's built-in `GITHUB_TOKEN`
|
|
261
|
+
can't read other organizations' repositories, so it needs your own token. A template
|
|
262
|
+
workflow is planned.
|
|
263
|
+
|
|
264
|
+
## When something goes wrong
|
|
265
|
+
|
|
266
|
+
- If a configured Linear team, project, state or label doesn't exist, `sync` stops before
|
|
267
|
+
reading GitHub or writing anything, and exits with status 2. `ferrygl check` shows what's
|
|
268
|
+
missing.
|
|
269
|
+
- If a route's repositories can't be read, ferryGL reports it, skips that route, continues
|
|
270
|
+
with the others, and exits with status 1.
|
|
271
|
+
- If a write fails, ferryGL reports it and continues. A run that fails halfway is safe to
|
|
272
|
+
repeat: ferryGL finds Linear issues by their GitHub links, so a rerun doesn't create
|
|
273
|
+
duplicates.
|
|
274
|
+
- Creating a Linear issue is never retried after a network error, because Linear may have
|
|
275
|
+
created it anyway. If it did, the next sync adopts it through the GitHub URL in its
|
|
276
|
+
description.
|
|
277
|
+
|
|
278
|
+
## How it works
|
|
279
|
+
|
|
280
|
+
- **No database.** Every Linear issue ferryGL manages has a link with the GitHub URL.
|
|
281
|
+
ferryGL finds it with Linear's `attachmentsForURL` query, and keeps the last GitHub state
|
|
282
|
+
it acted on in the link's `metadata`, as flat `ferrygl_*` keys such as `ferrygl_state`.
|
|
283
|
+
Linear holds all the state.
|
|
284
|
+
- **Few requests.** GitHub reads use the GraphQL search API: up to four searches per route,
|
|
285
|
+
plus one request per 50 results.
|
|
286
|
+
- **Testable core.** Planning (`ferrygl.plan`) is a pure function from GitHub items, existing
|
|
287
|
+
Linear issues and config to a list of changes. It's tested without network access.
|
|
288
|
+
- **Small footprint.** One runtime dependency, `httpx`.
|
|
289
|
+
|
|
290
|
+
## Development
|
|
291
|
+
|
|
292
|
+
```console
|
|
293
|
+
$ uv sync
|
|
294
|
+
$ uv run pytest
|
|
295
|
+
$ uv run ruff check && uv run ruff format --check
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Before changing ferryGL, read [AGENTS.md](AGENTS.md): it lists the invariants, the data
|
|
299
|
+
ferryGL leaves in users' Linear workspaces, and the testing and review rules for
|
|
300
|
+
contributors and coding agents.
|
|
301
|
+
|
|
302
|
+
## Not planned
|
|
303
|
+
|
|
304
|
+
- Writing anything to GitHub: comments, labels or status.
|
|
305
|
+
- Two-way sync, or keeping descriptions and comments in sync.
|
|
306
|
+
- Webhooks or a hosted service. ferryGL polls, so changes arrive on your schedule, not
|
|
307
|
+
instantly.
|
|
308
|
+
- Syncing every issue in a repository for a whole team. Linear's GitHub integration does that
|
|
309
|
+
when you can install it.
|
|
310
|
+
|
|
311
|
+
## Open questions
|
|
312
|
+
|
|
313
|
+
- Should commenting on an item, or being mentioned in it, count as a role? Today they don't,
|
|
314
|
+
because they'd pull in many items you only touched in passing.
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
[MIT](LICENSE)
|