Skip to main content

Crate onetaskgraph_github_projects

Crate onetaskgraph_github_projects 

Source
Expand description

A stateless onetaskgraph source over one GitHub Projects v2 board.

A board is a container of projects, not a project. Its own title, shortDescription and readme are never read as an item’s fields and are never written: nothing in this source can rename the board a user configured.

A project is an issue and its tasks are that issue’s sub-issues. GitHub’s schema decides that: Issue exposes parent, subIssues and subIssuesSummary, and DraftIssue exposes none of them. Creating an issue needs a repositoryId, and a board has none, so GitHubProjectsConfig::repository names the one repository this source creates its project and task issues in; a write without it is refused naming the field.

A document is an ordinary issue whose title begins DESIGN_TITLE_PREFIX. A board has no document type and nothing but issues to hold one in, so the title is the discriminator and it is the whole of it. The title this source reports is the one a person wrote, with the prefix taken off — the same way the metadata slot is taken off the body so content is what the person wrote — and writing a document puts the prefix back, so a round trip returns the title that went in.

Telling a document from a project from a task. The design prefix is read first: a document is never a project and never a task, whatever sub-issues it has or does not have. Only then does the rest apply — a board issue is a project when either it has sub-issues or it carries ItemKind::METADATA_KEY; otherwise it is a task. A sub-issue is always a task, whatever it carries. The marker is sufficient and never necessary: it is what makes an empty project — the state a project copy passes through between creating the project and filing its first task — readable as a project, while the sub-issue arm lets a person author a project on the board by hand with no knowledge of this product’s metadata at all. Reading the prefix later than the sub-issue rule would make a design issue with no sub-issues an empty project, which is exactly the state that rule exists to catch. Pull requests are neither a project nor a task nor a document and are ignored.

Where an entity is, is a link. Every project, task and document this source reports carries a Location::Url naming the issue’s own web address — the same address the url field already reports, in the shape that says a reader can open it. That is the contrast the location contract exists for: a reader holding an entity from this source is handed something to link to and one holding an entity from a folder of Markdown is handed a path, and neither has to know which plugin answered. It does not replace or derive from url; that field goes on reporting what it always reported.

Where metadata lives. Short typed things go to typed fields and native relations: status to the board’s Status single-select and the issue’s own state, the copy origin to a source-owned onetaskgraph.origin text field, and dependencies to blockedBy and to sub-issue links. Unbounded caller JSON goes in a trailing <!-- onetaskgraph.metadata ... --> comment at the end of the issue body — the same encoding docs/metadata.md settles for Linear, not a second one. A ProjectV2 text field is length-bounded and shortDescription is capped at 300 characters, which is why neither can hold a caller’s own prose.

Status. status_mapping is per-instance configuration from a status category to null, a board Status option name, or a closed state of completed or not-planned. Nothing here ever calls updateProjectV2Field: that mutation’s singleSelectOptions overwrites a field’s option set, so no addition is additive and a mistake destroys every item’s status. A status this board cannot represent is a refusal naming the status and the instance instead.

done closes the issue by default because GitHub derives subIssuesSummary.completed and the board’s own Sub-issues progress field from closed sub-issues: a plan whose finished tasks were only moved to a “Done” column would read 0% complete forever.

§What this source declares, field by field

One verdict per field of Capabilities, and what Native means when this source says it. Proven means a shared journey drives it against the real binary over this source’s own row in crates/onetaskgraph/tests/e2e/fixtures.rs, and every_row_declares_exactly_what_its_plugin_reports is what keeps this list and capabilities from parting.

FieldVerdict
projectsSupported and proven, and the one predicate here that is pushed down rather than applied in process: a task’s project is the issue it is a sub-issue of, so a listing scoped to one asks that issue for its own sub-issues. This is the field that was declared and then not applied, which silently returned another project’s tasks.
documentsSupported and proven. A board holds issues, so a document is one: the issue whose title begins DESIGN_TITLE_PREFIX. Reads, filters and paging answer on exactly the terms a task read does, and a write puts the prefix back.
orphan_tasksSupported and proven. A task issue with no parent is in no project.
filter_by_labelSupported and proven, over the issue’s own labels.
filter_by_statusSupported and proven, over the board’s Status option and the issue’s open or closed state, through this instance’s own status_mapping.
search_titleSupported and proven, over Issue.title.
search_contentSupported and proven, over the visible body — the trailing metadata comment is not part of it.
task_dependenciesSupported and proven, in both directions: blockedBy and blocking.
project_dependenciesSupported and proven, in both directions, over the same two connections, because a project here is an issue.
max_page_sizeSupported and proven. MAX_PAGE_SIZE, GitHub’s own connection maximum.

Nothing here is unsupported. documents is not a predicate — it says this source has documents, which it does — and the three facts behind the uniform Native on the predicates beside it are recorded below rather than re-derived, because a reader who takes Native to mean the remote service filters will read that uniformity as a lie.

First, the plugin contract defines Support::Native as the source applies this predicate itself, and says nothing about where it applies it. What the declaration promises the engine is capability rule 1 — a predicate declared Native is applied — so that the engine may push it down and apply nothing of its own.

Second, this source can keep that promise for every predicate at no additional API cost, because whichever of the three reads below answers a query has already read every item that query could keep before it filters anything. Filtering those items is in-process work over data already in hand.

Third, no predicate but projects could be pushed into the API even if that were wanted, and projects is pushed down: ProjectV2.items takes first and after and offers no filter argument of any kind, GitHub’s issue search offers no qualifier for a label set, a status column or a substring of a body, and its title qualifier matches tokens where this source — and the local Markdown source beside it — match substrings, so pushing a search down would silently narrow the answer. What a project filter has instead is a relationship: a project’s tasks are that issue’s sub-issues, and asking the issue for them is both cheaper and exact. So there is one predicate this source applies by asking a narrower question, six it applies in process, and none it is unable to apply. Declaring one Unsupported would make the engine compensate for work this source has already done, and declaring projects native while ignoring the filter (which this source once did) silently returns another project’s tasks, because the engine trusts the declaration and applies nothing locally.

§The three ways this source reaches an item, and what each costs

A board read is charged for what its nested connections could return rather than for what was asked, so one whole-board read costs the same whether the question was about one project or about all of them. That is why a question about one project is never answered by reading the board:

The questionWhat is sentWhat it costs
one item, by its own idgraphql::ISSUEnode(id:)the item
one project’s tasks or documentsgraphql::SUB_ISSUES — that issue’s own subIssuesthat project
which projects this board holdsgraphql::SEARCH_ISSUES — an issue search scoped to the boardthe board’s issues, without their board items
every task, every document, every labelgraphql::BOARD — the board’s own itemsthe board
which board item one issue is, past the page that came with itgraphql::ISSUE_BOARD_ITEMS — that issue’s own projectItemsone issue’s memberships

The board half of an issue — its board item’s id, its Status option and this source’s origin text field — rides along on Issue.projectItems in the first three, so an item reached any of those ways resolves through the same GitHubProjectsSource::resolve the board walk uses and reports the same title, the same status, the same labels and the same qualified id. That connection comes back a page at a time, at BOARD_ITEMS_PAGE_SIZE, so the entry for this board is looked for on the page in hand and — only if that page reports more of the connection — in the last row’s read of that one issue’s memberships, resumed from the page’s own cursor and walked to exhaustion. An issue with no entry for this board is not this source’s to report, which is what keeps an id naming another repository’s issue from being answered as an item of this board; and because the page is where the search starts rather than where it ends, that answer is one about a connection read to exhaustion and never about an unread page. Nothing costs the extra read but an issue on more boards than a page holds: an issue this board really does not hold reports no next page, so its memberships are already exhausted where they arrived.

No document here selects the board’s own Labels field, and nothing is lost by that. An item’s labels are read from its content alone, wherever that content is reached: the three documents above select Issue.labels on the fragment, and graphql::BOARD selects the same connection on the ... on Issue arm of its content. A board’s Labels field is not one anybody fills in: it is a built-in ProjectV2FieldType, it is absent from ProjectV2CustomFieldType so no project can create one, and ProjectV2FieldValue — the whole of what updateProjectV2ItemFieldValue accepts — offers no way to write one. So GitHub derives it from the content, for every content type it exists on, and there is nothing it can hold that the content does not already say: for an Issue it is that issue’s own labels, so selecting it beside them unions a set with itself.

A draft loses nothing by that either, which is the reasoning this paragraph once had backwards. DraftIssue exposes no labels field, and by the three schema facts above it cannot carry a board Labels value to be derived from one — so a draft has nothing to select and nothing to lose, and reports no labels at all. A PullRequest item is discarded by GitHubProjectsSource::resolve before labels are read. Both halves are held to that by tests in tests/plugin.rs: the four ways an item is reached report one label set, and that set is the fixture issue’s own, by an_item_reports_the_same_labels_title_status_and_id_however_it_is_reached; and a board item whose content is a draft reports an empty set, by a_board_item_whose_content_is_a_draft_reports_no_labels_at_all. The absence of the selection is held over graphql::DOCUMENTS by no_document_selects_the_boards_own_labels_field.

The last row is still the board’s own item connection, and deliberately: a draft board item is not an issue, so no search and no node read can reach one, and the reads that have to answer for the whole board are the ones whose cost is the board’s size anyway.

What a read may return is capped too, and that cap is on the document rather than on the board. GitHub limits the number of nodes one query may return to NODE_COUNT_LIMIT and refuses a query above that before executing it: the answer is an error naming the connection the count crossed at, not a slow or a partial result. Every board this source reads is refused the same way, so no board is too big for these documents and none is small enough to save one that is over.

The count is arithmetic over the document’s own text: each connection contributes the first: it asks for, counts multiply down a nested path and sum across sibling paths. Those are GitHub’s published rules and this workspace does not restate them — github-graphql-node-count implements them, and worst_case_node_count under largest_page_sizes is where every node count here comes from. every_document_this_source_sends_stays_under_githubs_node_limit, in tests/node_count.rs, recomputes every document in graphql::DOCUMENTS from that same text on every run and fails naming any that reaches the limit — so a connection added to a shared fragment is caught there rather than by GitHub.

What decides those counts is the page sizes: MAX_PAGE_SIZE on the outer page, NESTED_PAGE_SIZE on the connections hanging off one item, and BOARD_ITEMS_PAGE_SIZE on the page of an issue’s board memberships a read carries. $nestedFirst is spent twice down one path of a board read, so that constant is effectively squared there, which is why it is the one the limit is most sensitive to. BOARD_ITEMS_PAGE_SIZE is small for a reason of its own, recorded beside it: what a page of memberships misses is recovered by one further read rather than refused, so it buys a bound every read pays for at the price of a request only a multi-board issue pays.

nodeCount and cost are two numbers against two limits, and none of this is about the second. nodeCount is the one above: the most nodes one query may return, checked per query. cost is rate-limit points, metered per hour across everything one credential does; it is what the two limiters [Limiter] tells apart meter, and a document under NODE_COUNT_LIMIT says nothing about it.

Where a read-after-write guarantee comes from, since a search index cannot supply one. GitHub’s issue search is eventually consistent and answers a write made moments ago with the value from before it. Resolving a node id is not, so a read by id and a project’s own sub-issues are already current. What closes the gap for the search is GitHubProjectsSource::created: every read this source answers is completed with what this process itself wrote, so an item created seconds ago is reported whether or not GitHub’s index has caught up. Nothing else is remembered, nothing is written down, and the record dies with the process.

Filtering happens before paging, so a page of a filtered result is a page of the survivors rather than the survivors of a page. Label and text matching answer the same question the same way the local Markdown source’s do, so one cross-source expectation holds for both.

The fixture-server tests above run wherever this crate is selected; the credentialed lane runs in the same required check, beside them, and can fail it — it verifies the current schema, then drives every field of the table above against the real board. It builds its own fixture there — two projects, one task filed under each, one filed under neither, a label on one of the three and a closed status on another — because that shape is what tells an honoured predicate from an ignored one: a board holding a single project answers a project filter the same way whether or not this source applies it, which is exactly how the defect above went unseen.

That lane writes only to the board GH_PROJECTS_OWNER and GH_PROJECTS_NUMBER name, and only into the repository GH_PROJECTS_REPOSITORY names, and skips — as it does without GH_PROJECTS_TOKEN — when any of them is absent. Requiring both to be nominated is what keeps a credentialed write lane off a board and a repository nobody nominated; it never asks GitHub which project was updated most recently. Before it starts, the lane also clears any item titled — and any repository label named — the way it titles and names its own artifacts, which is self-healing after an interrupted run: a process killed between its writes and its cleanup leaves artifacts the next run removes.

§What a session of requests costs, and where the report is

This source records every request it sends into accounting::Accounting, at send_once — the one place a request leaves this crate, which is why a read path added later is counted without anybody remembering to count it. That is the whole of what this crate adds to the arrangement; accounting is where what a record carries, how a session’s spend is arrived at, and what it deliberately does not know are set out.

What one whole session of the live journey costs, counted that way against this crate’s loopback fixture board, is written down in session-cost.md beside this crate — with the reduction it came out of, and with what it does and does not say about rate-limit points.

GitHubProjectsSource::accounting is the read: a snapshot to hold and compare, which accounting::Session::report renders the session report from. It is on the ordinary code path — no environment variable, no feature, no build configuration — because an instrument nobody switches on measures nothing, and Plugin::build_recording_into is how a caller making its own calls beside this source’s counts the whole session rather than this source’s share. The credentialed lane in tests/live.rs does exactly that, and prints the report at the end of every run, passed or failed.

A live session refuses to start unless the account can afford it. Before it does any of the work it exists to do, the journey makes one request — GET /rate_limit, which GitHub documents as not counting against the REST rate limit and which answers both of its budgets at once — and starts only if, for each of them, what remains minus this session’s estimated cost is still at least onetaskgraph_live::RETAINED_BUFFER — twenty per cent — of that budget’s whole allowance. A session that cannot declines: it did not run, so it is neither a pass nor a failing assertion, and it says which budget was short, that budget’s limit, what remained, the estimate, the buffer and when it resets — then stops, without waiting for the budget to come back. The estimate is derived offline from tests/fixtures/session-cost.txt and a cost model stated in tests/journey/budget.rs, which is also where the published rule that model rests on is cited; the accounting above records the gate’s own read like any other request, and accounting::Session::report prints the estimate beside what the session really spent.

GitHub is the authority on node count, and the credentialed lane goes and asks it. Everything above computes nodeCount offline from a document’s own text, which is what lets it run on every platform and on a pull request from a fork with no credential — and that is what actually stops a regression merging. But an offline arithmetic can only ever agree with itself: if GitHub changes its rules, this workspace goes on computing the old answer and nothing notices. So tests/live.rs reconciles the two. GitHub’s schema exposes rateLimit(dryRun: true), whose nodeCount is “the maximum number of nodes this query may return” for a document without executing it, and the lane asks it for every query document this source sends, under the largest bindings this source sends, and fails when GitHub’s number and worst_case_node_count disagree. It records what those calls reported about the account’s own allowance, because whether asking is free is a thing to observe rather than to assume. Two quantities, not one: NODE_COUNT_LIMIT bounds nodeCount, and the accounting above measures cost.

GitHub has two rate limiters and this source is refused by both, so nothing here treats them as one thing. The primary budget is the hourly allowance gh api rate_limit reports; the secondary limiter is a burst limiter over content-generating requests, and nothing reports it. Which one refused decides the operator’s next step, so [Limiter] is a type rather than a detail, and it is what MIN_MUTATION_INTERVAL_MS, GitHubProjectsSource::board_cache and GitHubProjectsSource::graphql each answer one part of.

Modules§

accounting
What a session of requests to GitHub cost, counted rather than argued about.
graphql
Exact GraphQL query documents issued by this plugin.

Structs§

ColumnName
The name of a Status single-select option on the board.
GitHubProjectsConfig
Configuration for one GitHub Projects v2 board.
GitHubProjectsSource
A source which reads GitHub afresh for every operation.
PacingConfig
How fast this source writes, and how long it waits out a rate-limit refusal.
Plugin
Factory for GitHubProjectsSource.

Enums§

ClosedState
The two closed states this product can mean.
NodeCountError
Why a document’s node count could not be computed.
StatusTargetConfig
Where one status category lands on this board.

Constants§

CATEGORIES
Every status category, in the order the vocabulary declares them.
CONTENT_CREATION_PER_HOUR
The same ceiling as GitHub publishes it per hour, which this source does not pace at. See MIN_MUTATION_INTERVAL_MS for why the per-minute bound is the one that governs; it is pinned beside its sibling so the gate would notice either one moving.
CONTENT_CREATION_PER_MINUTE
GitHub’s published ceiling on content-generating requests, per minute.
DESIGN_TITLE_PREFIX
The issue-title prefix that makes a board issue a document.
KIND
The registry name for this plugin.
MAX_PACING_MS
The largest any pacing setting may be, in milliseconds.
MAX_PAGE_SIZE
GitHub’s maximum connection page size.
MIN_MUTATION_INTERVAL_MS
Shortest interval between two content-creating mutations, in milliseconds.
NODE_COUNT_LIMIT
The most nodes any one document this source sends may be asked to return.
PRIMARY_WORDINGS
The wordings GitHub answers an exhausted primary budget with.
RETRY_BACKOFF_MS
First wait when a rate-limit refusal carries no hint; each further wait doubles it.
RETRY_BUDGET_MS
Total time one call may spend waiting out rate limits before it reports a failure.
SECONDARY_WORDINGS
The wordings GitHub answers a secondary rate limit with.

Functions§

category_position
Where one category sits in CATEGORIES; see that list for what this pins.
largest_page_sizes
The largest value this source can bind to each page-size variable its documents name.
node_count
The most nodes document could be asked to return under variables.
worst_case_node_count
The most nodes document could be asked to return, by GitHub’s published rules.

Type Aliases§

Variables
The integer bound each page-size variable a document names is given, keyed by variable name without the leading $.