Gitea client: conditional issue listing #4
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #1.
Goal
Implement
tireless_core::port::ForgeClientforGiteaClient, read paths only.The struct exists with
#[allow(dead_code)]fields and no implementation.The read path is
list_opted_in_issues, and it must be a polite API citizen:send
If-None-Matchwhen the repo has a storedETag, treat304 Not Modifiedas an empty result rather than an error, and back off with jitter on
429and5xx. Being unattended is not a licence to hammer a forge (design.md §5).The token comes from the environment variable named by
config.forge.gitea.token_env— never from the config file, and never theoperator's own token (design.md §6.4).
Files
crates/tireless-data/src/forge.rs— theForgeClientimpl forGiteaClientcrates/tireless-data/src/lib.rs— exports if neededSteps
GiteaClienta constructor taking base URL and token, reading the tokenfrom the named environment variable at construction and failing loudly if it
is absent.
list_opted_in_issues: query the repo's open issues filtered bythe opt-in label, sending
If-None-Matchwhenrepo.last_etagis set.304, and surface the newETagso the caller canstore it.
DiscoveredIssue, preserving all labels — therouter needs them, and so does reconciliation.
429/5xx, bounded;a forge that is down should not be retried forever inside one poll.
add_label,remove_label,comment,create_issue,open_pull_request) returning a clear "not implemented instage 1" error rather than silently succeeding.
Acceptance
cargo test --workspacecargo clippy --all-targets --all-features -- -D warningsIf-None-Matchis sent when an ETagis stored, and that a
304response yieldsOk(vec![]).429is retried with backoff and eventually gives up ratherthan looping.
Out of scope
protocol has been exercised with a dry-run executor.
GitHubClient. It is for legacy repos and is disabled in the shipped config;leave it as it is.
Config. It is named there, not carried there.Done in
98f193d, together with #3.The spec gap
Step 3 says "surface the new
ETagso the caller can store it" — butlist_opted_in_issuesreturned a bareVec<DiscoveredIssue>, so there wasnowhere to put it. The only copy would have stayed inside the client, and a
poller that looked correct would have re-fetched every repo in full on every
tick, forever, with nothing to show it was happening.
It returns an
IssuePagenow, which also carriesnot_modified. Thatdistinction turned out to matter more than the ETag: a 304 is not an empty
repo. A caller that conflated them would read every quiet poll as "every issue
disappeared" and abandon the jobs behind them. Carrying it in the type means a
caller has to decide rather than assume, and there is a test pinning it.
Beyond the spec
Pull requests are filtered out. Gitea's
type=issuesparameter shouldexclude them, but an older server ignores it and returns both — and a pull
request enqueued as an issue would be planned or implemented as though it were
one. Cheap to guard, expensive to debug.
Retry-Afteris honoured but capped at 60s. A forge asking us to wait anhour would otherwise stall the whole poll tick for every other repo.
Backoff is jittered. N repos throttled at the same moment must not all retry
at the same moment and throttle each other again.
Writes return an error naming the stage they land in, rather than a silent
Ok. A no-op that succeeded would let stage 2 look finished while the forge sawnothing — and there is a test asserting they do not even reach the network.
Verified against a mock forge
Twelve tests, no database needed, so these run in the ordinary
cargo test:If-None-Match, and 304 is reported asnot-modified rather than as empty or as an error;
If-None-Matchcould earn a 304 for a client that has never seen the issues;
unbounded retry would hold the tick open indefinitely;
planner reads the other;
GitHubClientis deliberately still a stub. It is disabled in the shippedconfig, and an unused implementation is one more thing to keep working for
nobody.