You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: MIGRATION.md
+52Lines changed: 52 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -211,3 +211,55 @@ After swapping, before merging:
211
211
-`go test ./...` and `go vet ./...` in your repo: tests that mocked the in-tree etag transport will need to mock against ghkit's `etag.NewTransport` or against a higher-level `*http.Client`.
212
212
- Confirm `errors.As(err, &primaryRateLimit)` still unwraps the `*github_primary_ratelimit.RateLimitReachedError` your controller relies on (it does, ghkit re-uses gofri unchanged).
213
213
- If you keep your own metric emitter, wrap it as a RoundTripper above the `*http.Client` returned by `ghkit.HTTPClient(...)` rather than relying on ghkit's slog events for per-route labels.
For tests, `ghtest.LinkHeader(baseURL, page, perPage, lastPage)` builds RFC 8288 Link header values. See [`examples/list-all-repos/`](examples/list-all-repos/main.go) for a runnable end-to-end demo.
167
168
</details>
168
169
170
+
<details>
171
+
<summary><b>Polling a long-running operation (workflow run, check run, deployment)</b></summary>
172
+
173
+
The `polling` sub-package iterates an HTTP endpoint on a caller-tunable interval, reusing the configured `*http.Client` so retry, etag, ratelimit, throttle, and oauth2 apply per attempt. `polling.As[T]` decodes each response into `T`; `WithDoneT` stops on the decoded value; `WithMaxWallClock` caps total time and wraps `context.DeadlineExceeded`.
174
+
175
+
```go
176
+
import (
177
+
"context"
178
+
"errors"
179
+
"log"
180
+
"net/http"
181
+
"os"
182
+
"time"
183
+
184
+
"github.com/google/go-github/v85/github"
185
+
ghkit "github.com/pcanilho/go-github-kit"
186
+
"github.com/pcanilho/go-github-kit/polling"
187
+
"github.com/pcanilho/go-github-kit/retry"
188
+
)
189
+
190
+
hc, err:= ghkit.HTTPClient(
191
+
ghkit.WithToken(os.Getenv("GITHUB_TOKEN")),
192
+
ghkit.WithRetry(retry.WithMaxAttempts(1)), // polling owns the loop
if errors.Is(err, polling.ErrMaxWallClockExceeded) { log.Fatal("budget exceeded") }
214
+
log.Fatal(err)
215
+
}
216
+
run = r
217
+
}
218
+
log.Printf("conclusion=%s", run.GetConclusion())
219
+
```
220
+
221
+
Sharp edges: each `c.Do` may itself loop through `retry.Transport` (pass `retry.WithMaxAttempts(1)` when polling owns the outer loop); `throttle.WithRequestsPerSecond` below `1/interval` dominates cadence; with `WithETagCache` an unchanged resource yields identical decoded bytes per tick (pair with `polling.WithChangeOnly` to skip those silently). Pages-shape body ownership: `Poll` yields `*http.Response` and the caller closes; `As[T]` owns and closes via defer.
222
+
223
+
See [`examples/poll-workflow-run/`](examples/poll-workflow-run/main.go) for a runnable demo.
224
+
</details>
225
+
226
+
<details>
227
+
<summary><b>Searching with envelope pagination (1000-cap aware)</b></summary>
228
+
229
+
GitHub's `/search/*` endpoints return an envelope (`{total_count, incomplete_results, items[]}`), so `pages.As[T]` cannot serve them directly. The `search` sub-package wraps the four common endpoints (`Issues`, `Code`, `Repos`, `Users`) and surfaces both `IncompleteResults` (timed-out queries) and the post-page-10 1000-result hard cap as `ErrResultCapHit`.
Implementation reuses `pages.Pages` for Link-header walking, so per-page retry/etag/ratelimit/throttle composition is automatic. Search has its own `X-RateLimit-Resource` budget; gofri routes requests transparently. See [`examples/search-issues/`](examples/search-issues/main.go).
The `etag` layer transparently converts 304 responses into synthesised 200s with the cached body. The `cond` sub-package surfaces the change-vs-unchanged signal that is otherwise erased, letting consumers skip JSON parse, structural diff, DB writes, and webhook fan-out when nothing changed.
case cond.Updated: // wire 200 (or no etag layer in chain)
292
+
case cond.Unchanged: // synth 200 from cache hit; resource hasn't changed
293
+
}
294
+
```
295
+
296
+
Mechanics: the etag layer sets `cond.HeaderCacheStatus` (`"X-Ghkit-Cache"`) on synth-200 (`"hit"`) and wire-200-store (`"miss"`) paths. `cond.StatusOf(resp)` reads the header. Absent header → `Updated` (no etag layer in chain → caller behaves as if every response is fresh). Pair with `polling.WithChangeOnly` to silently skip yields when polling an unchanged resource. See [`examples/conditional-fetch/`](examples/conditional-fetch/main.go).
0 commit comments