{"id":51116394,"url":"https://github.com/peczenyj/go-claimcheck","last_synced_at":"2026-06-24T22:30:29.619Z","repository":{"id":359160449,"uuid":"1244750089","full_name":"peczenyj/go-claimcheck","owner":"peczenyj","description":"Cloud-agnostic Claim Check pattern for Go. Transparently offload large messages to blob storage (S3/GCS) while sending lightweight pointers via Pub/Sub   (Kafka/RabbitMQ). Powered by Go CDK for total provider portability. Features pluggable serialization, compression/encryption, and rich metadata (checksums,   file size, message counts).","archived":false,"fork":false,"pushed_at":"2026-05-20T17:54:18.000Z","size":63,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"devel","last_synced_at":"2026-05-20T21:48:34.321Z","etag":null,"topics":["blob-storage","claim-check-pattern","cloud-native","distributed-systems","gcs","go","gocloud","golang","kafka","messaging","middleware","offloading","pubsub","rabbitmq","s3"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/peczenyj.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-20T15:01:10.000Z","updated_at":"2026-05-20T17:52:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/peczenyj/go-claimcheck","commit_stats":null,"previous_names":["peczenyj/go-claimcheck"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/peczenyj/go-claimcheck","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peczenyj%2Fgo-claimcheck","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peczenyj%2Fgo-claimcheck/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peczenyj%2Fgo-claimcheck/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peczenyj%2Fgo-claimcheck/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/peczenyj","download_url":"https://codeload.github.com/peczenyj/go-claimcheck/tar.gz/refs/heads/devel","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peczenyj%2Fgo-claimcheck/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34752465,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-24T02:00:07.484Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blob-storage","claim-check-pattern","cloud-native","distributed-systems","gcs","go","gocloud","golang","kafka","messaging","middleware","offloading","pubsub","rabbitmq","s3"],"created_at":"2026-06-24T22:30:26.256Z","updated_at":"2026-06-24T22:30:29.613Z","avatar_url":"https://github.com/peczenyj.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# go-claimcheck\n\n[![tag](https://img.shields.io/github/tag/peczenyj/go-claimcheck.svg)](https://github.com/peczenyj/go-claimcheck/releases)\n![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.25-%23007d9c)\n[![GoDoc](https://pkg.go.dev/badge/github.com/peczenyj/go-claimcheck)](http://pkg.go.dev/github.com/peczenyj/go-claimcheck)\n[![CI](https://github.com/peczenyj/go-claimcheck/actions/workflows/ci.yml/badge.svg)](https://github.com/peczenyj/go-claimcheck/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/gh/peczenyj/go-claimcheck/graph/badge.svg?token=9y6f3vGgpr)](https://codecov.io/gh/peczenyj/go-claimcheck)\n[![Report card](https://goreportcard.com/badge/github.com/peczenyj/go-claimcheck)](https://goreportcard.com/report/github.com/peczenyj/go-claimcheck)\n[![CodeQL](https://github.com/peczenyj/go-claimcheck/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/peczenyj/go-claimcheck/actions/workflows/github-code-scanning/codeql)\n[![Dependency Review](https://github.com/peczenyj/go-claimcheck/actions/workflows/dependency-review.yml/badge.svg)](https://github.com/peczenyj/go-claimcheck/actions/workflows/dependency-review.yml)\n[![License](https://img.shields.io/github/license/peczenyj/go-claimcheck)](./LICENSE)\n[![Latest release](https://img.shields.io/github/release/peczenyj/go-claimcheck.svg)](https://github.com/peczenyj/go-claimcheck/releases/latest)\n[![GitHub Release Date](https://img.shields.io/github/release-date/peczenyj/go-claimcheck.svg)](https://github.com/peczenyj/go-claimcheck/releases/latest)\n[![Last commit](https://img.shields.io/github/last-commit/peczenyj/go-claimcheck.svg)](https://github.com/peczenyj/go-claimcheck/commit/HEAD)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/peczenyj/go-claimcheck/blob/main/CONTRIBUTING.md#pull-request-process)\n[![SLSA Level 2](https://img.shields.io/badge/SLSA-Level_2-green)](https://github.com/peczenyj/go-claimcheck/attestations)\n\nCloud-agnostic Claim Check pattern for Go. Transparently offload large messages to blob storage (S3/GCS/Azure) while sending lightweight pointers via Pub/Sub (Kafka/RabbitMQ/SNS/SQS). \n\nPowered by [Go CDK](https://gocloud.dev/) for total provider portability.\n\n## Features\n\n- **Two API layers:** a low-level **core** (`claimcheck`) for full control over offload and read, and a **Pub/Sub integration layer** (`ccpubsub`) that handles buffering, offloading, and unrolling automatically.\n- **Blob-level delivery:** the consumer `Batch` acknowledges or negatively-acknowledges a whole offloaded blob — the natural unit of delivery for this pattern.\n- **Pluggable Serialization:** built-in NDJSON (JSON Lines) and length-prefixed binary, both streaming for bounded-memory reads.\n- **Data Transformation:** built-in Gzip and Zstd compression middleware.\n- **Rich Metadata:** tracks file size, content type/encoding, and message count on every control message, plus a best-effort MD5 checksum. The checksum is backend-dependent — some stores do not expose an object MD5 (e.g. S3 multipart uploads, GCS composite objects, Azure without Content-MD5); when it is absent, `VerifyChecksum` fails closed with `ErrChecksumUnavailable` rather than reading unverified.\n- **Provider Agnostic:** works with any Pub/Sub and Blob storage supported by [Go CDK](https://gocloud.dev/).\n\n## Installation\n\n```bash\ngo get github.com/peczenyj/go-claimcheck\n```\n\n## Architecture\n\nThe library is organized in two layers. Choose the one that matches the level of\ncontrol you need; both share the same on-blob format, so a producer using one\nlayer interoperates with a consumer using the other.\n\n| Layer | Package | Send | Receive |\n| :--- | :--- | :--- | :--- |\n| **Core (low-level)** | `claimcheck` | `Offload` a batch to a blob, attach `ControlMessage` metadata to your own pubsub message | `ParseControlMessage`, then `Read` / `Open` the blob |\n| **Pub/Sub integration** | `ccpubsub` | `WrapTopic` buffers and offloads automatically | `WrapSubscription` returns a `Batch` to `Read` and `Ack` |\n\nThe core never imports `gocloud.dev/pubsub`: it deals only in blobs and a\nmetadata map, so the control message can ride any transport. The `ccpubsub`\nwrappers adapt any gocloud `*pubsub.Topic` / `*pubsub.Subscription`.\n\n## Quick Start\n\nThe examples below build up from the simplest possible use to full manual\ncontrol. Every snippet is mirrored by a runnable test in the package, so they\ncompile and pass as written.\n\n### 1. Zero configuration\n\nWrap a topic and subscription and start sending. With empty options you get the\ndefaults — JSON Lines serialization, no compression, and the `claimcheck_`\nmetadata prefix. The producer buffers messages and offloads them to one blob,\npublishing a single control message; the consumer receives that as a `Batch`,\nreads the blob back, and acknowledges the whole blob.\n\n```go\nimport (\n    \"context\"\n    \"log\"\n    \"time\"\n\n    claimcheck \"github.com/peczenyj/go-claimcheck\"\n    \"github.com/peczenyj/go-claimcheck/ccpubsub\"\n    \"gocloud.dev/blob/memblob\"\n    \"gocloud.dev/pubsub/mempubsub\"\n)\n\nctx := context.Background()\nbucket := memblob.OpenBucket(nil)\nbaseTopic := mempubsub.NewTopic()\nbaseSub := mempubsub.NewSubscription(baseTopic, time.Second)\n\ntopic := ccpubsub.WrapTopic(baseTopic, bucket, ccpubsub.TopicOptions{})\nsub := ccpubsub.WrapSubscription(baseSub, bucket, ccpubsub.SubscriptionOptions{})\n\nif err := topic.Send(ctx, \u0026claimcheck.Message{Body: []byte(\"hello\")}); err != nil {\n    log.Fatal(err)\n}\nif err := topic.Shutdown(ctx); err != nil { // flushes the buffered batch\n    log.Fatal(err)\n}\n\nbatch, err := sub.Receive(ctx)\nif err != nil {\n    log.Fatal(err)\n}\nmsgs, err := batch.Read(ctx) // or batch.Open(ctx) to stream with bounded memory\nif err != nil {\n    log.Fatal(err)\n}\nbatch.Ack() // acknowledges the whole blob\n```\n\nA `Batch` may also be *inline* (`batch.Offloaded() == false`) when a received\nmessage carries no control-message metadata — `Read` still returns the message,\n`Ack` acknowledges it, and `Delete` is a no-op (there is no blob to remove).\n\n### 2. Customizing the integration layer\n\nThe same wrappers take options. Here we compress blobs with Zstd, namespace the\nblob keys, verify checksums on read, and control batching: flush after 100\nbuffered messages or every two seconds, whichever comes first. `WrapTopic`\n**always** offloads the buffered batch — the number of messages per blob is\ncaller-controlled via `MaxMessages` / `MaxBytes` / `FlushInterval`. If you set\n**none** of them the batch is only published on an explicit `Flush`/`Shutdown`,\nso the buffer is bounded by `DefaultMaxBytes` (1 MiB) to prevent unbounded\ngrowth; set at least one trigger for predictable flushing. `FlushTimeout`\nbounds how long a single periodic flush may take (0 = run to completion, and\nindependent of `FlushInterval`).\n\n```go\n// Producer and consumer share the same options\n// (see \"The bucket-binding contract\").\nopts := claimcheck.Options{\n    Transformer:    claimcheck.NewZstdTransformer(),\n    KeyPrefix:      \"claimcheck/\",\n    VerifyChecksum: true,\n}\n\ntopic := ccpubsub.WrapTopic(baseTopic, bucket, ccpubsub.TopicOptions{\n    Options:       opts,\n    MaxMessages:   100,\n    FlushInterval: 2 * time.Second,\n})\nsub := ccpubsub.WrapSubscription(baseSub, bucket, ccpubsub.SubscriptionOptions{Options: opts})\n```\n\n`Options` also supports `KeyFunc` (custom blob naming), `Serializer` (JSON Lines\nor length-prefixed), and `MaxMessageSize` / `MaxBatchSize` (decode safety caps).\n\n### 3. Bring your own codec\n\nCompression and serialization are pluggable interfaces. To add a codec, satisfy\nthe three-method `Transformer` interface. This example wraps the standard\nlibrary's DEFLATE codec; the same shape works for any codec — for instance\nBrotli via [`github.com/andybalholm/brotli`](https://github.com/andybalholm/brotli)\n(a third-party package, not a dependency of this library).\n\nThe same interface is also the right place for **encryption** — an AES-GCM or\nNaCl secretbox `Transformer` plugs in exactly like a compression codec.\nEncryption is not built in; it is bring-your-own via this interface.\n\n```go\nimport (\n    \"compress/flate\"\n    \"io\"\n\n    claimcheck \"github.com/peczenyj/go-claimcheck\"\n)\n\ntype flateTransformer struct{}\n\nfunc (flateTransformer) ContentEncoding() string { return \"deflate\" }\n\nfunc (flateTransformer) WrapWriter(w io.Writer) (io.WriteCloser, error) {\n    return flate.NewWriter(w, flate.DefaultCompression)\n}\n\nfunc (flateTransformer) WrapReader(r io.Reader) (io.ReadCloser, error) {\n    return flate.NewReader(r), nil\n}\n\n// Use it like any built-in transformer:\nopts := claimcheck.Options{Transformer: flateTransformer{}}\n```\n\n### 4. Low-level core API (`claimcheck`)\n\nFor full manual control, use the core directly: you manage the topic,\nsubscription, and bucket yourself. `Offload` writes a batch to a blob and returns\na `ControlMessage`; you send its metadata over any transport, then parse it and\n`Read` the blob back on the other side. The core never imports\n`gocloud.dev/pubsub`.\n\n```go\nimport (\n    \"context\"\n    \"log\"\n\n    claimcheck \"github.com/peczenyj/go-claimcheck\"\n    \"gocloud.dev/blob/memblob\"\n)\n\nctx := context.Background()\nbucket := memblob.OpenBucket(nil)\nopts := claimcheck.Options{KeyPrefix: \"claimcheck/\"}\n\n// Producer: write the batch to a blob, get the control message.\ncm, err := claimcheck.Offload(ctx, bucket, opts,\n    []*claimcheck.Message{{Body: []byte(\"alpha\")}, {Body: []byte(\"beta\")}})\nif err != nil {\n    log.Fatal(err)\n}\nmetadata := cm.ToMetadata(opts.MetadataPrefix) // attach to your pubsub message\n\n// Consumer: parse the metadata you received, then read the blob back.\nparsed, ok := claimcheck.ParseControlMessage(metadata, opts.MetadataPrefix)\nif !ok {\n    log.Fatal(\"not a claim-check message\")\n}\nmsgs, err := claimcheck.Read(ctx, bucket, parsed, opts)\nif err != nil {\n    log.Fatal(err)\n}\n_ = claimcheck.Delete(ctx, bucket, parsed) // optional cleanup\n```\n\nFor bounded-memory reads, use `claimcheck.Open` to stream messages in chunks\ninstead of `claimcheck.Read`.\n\n## Conditional offload\n\nBy default every message produced through `WrapTopic` is offloaded to a blob.\nSet `Options.MinSize` to skip the blob for small payloads: a message whose\n`Body` is smaller than `MinSize` is published **inline** (a plain Pub/Sub\nmessage carrying the body and metadata), while messages at or above `MinSize`\nare buffered and offloaded as usual.\n\n```go\nopts := claimcheck.Options{MinSize: 64 * 1024} // offload only payloads \u003e= 64 KiB\ntopic := ccpubsub.WrapTopic(baseTopic, bucket, ccpubsub.TopicOptions{Options: opts})\n```\n\nThe consumer needs no special handling — `WrapSubscription` already returns\ninline messages as a `Batch` whose `Offloaded()` is `false` and whose `Read`\nyields the original message. `MinSize` is `0` by default, preserving the\nalways-offload behavior.\n\nInline messages are published immediately and do not pass through the offload\nbuffer, so a small message may be delivered before previously buffered larger\nones; `WrapTopic` does not guarantee ordering across the inline/offload boundary.\n\n## Observability\n\nSet `Options.Observer` to record offload/read metrics. The `Observer` interface\nlives in the core package and pulls in no observability dependency:\n\n```go\ntype Observer interface {\n    OffloadDone(ctx context.Context, info claimcheck.OffloadInfo)\n    ReadDone(ctx context.Context, info claimcheck.ReadInfo)\n}\n```\n\nEmbed `claimcheck.NopObserver` so future methods stay non-breaking, and override\nthe hooks you care about. Each `Info` carries `MsgCount`, `Bytes`, `Duration`,\n`Err`, and (for reads) an `Inline` flag distinguishing offloaded blobs from\ninline messages — enough for offload/unroll latency histograms, byte counters,\nand offloaded-vs-inline ratios. The observer set on `Options` flows through the\n`ccpubsub` layer automatically. See `Example_observer` in the godoc.\n\n### Writing an OpenTelemetry adapter\n\n`claimcheck` keeps `go.opentelemetry.io/otel` out of its dependency graph, so an\nOTel adapter lives in your own code (or a separate module). Because each `Info`\ncarries `StartTime` and `Duration`, you can record a correctly-timed span\nretroactively:\n\n```go\n// Illustrative — not part of the module; you supply meter and tracer.\ntype otelObserver struct {\n    claimcheck.NopObserver\n    tracer       trace.Tracer\n    offloadBytes metric.Int64Counter\n    offloadDur   metric.Int64Histogram\n}\n\nfunc (o otelObserver) OffloadDone(ctx context.Context, info claimcheck.OffloadInfo) {\n    o.offloadBytes.Add(ctx, info.Bytes)\n    o.offloadDur.Record(ctx, info.Duration.Microseconds())\n\n    _, span := o.tracer.Start(ctx, \"claimcheck.offload\",\n        trace.WithTimestamp(info.StartTime))\n    if info.Err != nil {\n        span.RecordError(info.Err)\n    }\n    span.End(trace.WithTimestamp(info.StartTime.Add(info.Duration)))\n}\n```\n\n## The bucket-binding contract\n\nThe claim check only works when the producer and consumer **independently agree**\non where the blob lives and how it is encoded. There is no negotiation: the\ncontrol message carries the blob key and content-type/encoding, but the consumer\nmust already be configured to reach the same storage and decode the same way.\n\nBoth sides must share:\n\n- **the same blob bucket** — the consumer must be able to open the exact bucket\n  the producer wrote to (same provider, region, and bucket name/prefix);\n- **a compatible `Serializer`** — the consumer must decode what the producer\n  encoded (JSON Lines vs. length-prefixed);\n- **a compatible `Transformer`** — matching compression (`\"\"`, `gzip`, `zstd`);\n- **the same `MetadataPrefix`** — or the consumer will not recognise the control\n  message and will treat the delivery as an inline (non-offloaded) batch.\n\nIf these drift, the failure mode is silent or late: a mismatched `MetadataPrefix`\nmakes the consumer ignore the pointer and hand back the raw control message as an\ninline `Batch`; a wrong bucket makes `Read` fail to open the blob; a mismatched\nserializer or transformer surfaces as a decode error (or, with\n`VerifyChecksum`, an `ErrChecksumMismatch`). Treat the bucket + options as a\nshared contract you deploy to both sides together. *(Tip: If `InjectBlobMetadata` is used, the consumer can optionally use `bucket.Attributes()` to validate the content-type and encoding before reading, though this incurs an extra network round-trip.)*\n\n## Delivery semantics\n\nThis library is **at-least-once**, and the unit of delivery is the **whole blob**:\n\n- `WrapSubscription` Acks/Nacks the underlying pubsub message, which points at one\n  offloaded blob. `batch.Ack()` acknowledges every message in that blob at once;\n  `batch.Nack()` requests redelivery of the entire blob.\n- If your consumer crashes after reading a blob but before `Ack`, the broker\n  redelivers the same control message and you process the **whole blob again**.\n  Make consumers **idempotent** (e.g. dedupe on a message key) — there is no\n  partial-blob acknowledgement.\n- Offloaded blobs are **not** deleted automatically. Call `batch.Delete(ctx)`\n  (or `claimcheck.Delete`) once you have durably processed the batch, or run a\n  lifecycle/TTL policy on the bucket to reclaim storage.\n\n## Blob retention and cleanup\n\nDelivery is at-least-once and deleting the offloaded blob is the **consumer's**\nresponsibility. Two things prevent blobs from accumulating:\n\n**1. Ack-deletes (recommended).** Create the subscription with `AckDeletes` so\nacking a batch also removes its blob (ack first, then a best-effort delete):\n\n```go\nsub := ccpubsub.WrapSubscription(baseSub, bucket, ccpubsub.SubscriptionOptions{\n    Options:    claimcheck.Options{MetadataPrefix: \"cc_\"},\n    AckDeletes: true,\n})\n// batch.Ack() now also deletes the blob.\n```\n\nFor explicit error handling, call `batch.AckAndDelete(ctx)` instead of `Ack()`.\nThe ack-then-delete ordering is deliberate: a crash after ack but before delete\nleaves an orphan (swept by the lifecycle policy below); a crash before ack means\nneither ran, so redelivery still finds the blob present.\n\n**2. A bucket lifecycle policy (backstop).** Set an object-expiration rule on the\nbucket, scoped to your `KeyPrefix`, with an age longer than (max broker\nretention + worst-case processing time). It acts on the object's native creation\ntime — no library cooperation needed — and mops up anything ack-deletes misses\n(consumers that never delete, or the post-ack crash window).\n\nS3:\n\n```json\n{\n  \"Rules\": [{\n    \"ID\": \"expire-claimcheck-blobs\",\n    \"Filter\": { \"Prefix\": \"claimcheck/\" },\n    \"Status\": \"Enabled\",\n    \"Expiration\": { \"Days\": 7 }\n  }]\n}\n```\n\nGCS:\n\n```json\n{\n  \"rule\": [{\n    \"action\": { \"type\": \"Delete\" },\n    \"condition\": { \"age\": 7, \"matchesPrefix\": [\"claimcheck/\"] }\n  }]\n}\n```\n\nThe producer also self-cleans: if publishing the control message fails after the\nblob is written, the buffering `WrapTopic` deletes the orphaned blob before\nreturning the error.\n\n## Known limitations\n\n- **Producer I/O is serialized.** `WrapTopic` performs the blob write and the\n  control-message publish while holding its internal lock, so concurrent `Send`\n  calls block for the duration of a flush. This bounds producer throughput under\n  heavy concurrency; see\n  [#53](https://github.com/peczenyj/go-claimcheck/issues/53). If you need higher\n  concurrency today, run multiple `WrapTopic` instances or shard producers.\n\n## Supply chain security\n\nEvery tagged release meets [SLSA](https://slsa.dev) **Build Level 2**: the release\nworkflow runs on a GitHub-hosted runner and builds a source archive\n(`go-claimcheck-\u003cversion\u003e.tar.gz`) and a `SHA256SUMS` file, then generates a\nSigstore-signed build-provenance attestation over them using GitHub's\n[artifact attestations](https://docs.github.com/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds).\nBecause the provenance is produced and signed by the hosted build platform, it is\nauthentic and tamper-evident — meeting SLSA Build Level 2 by default.\n\nVerify a downloaded release artifact against its provenance with the GitHub CLI:\n\n```bash\ngh attestation verify go-claimcheck-0.6.0.tar.gz --repo peczenyj/go-claimcheck\n```\n\n## Development\n\nThis project uses [Task](https://taskfile.dev/) to manage the development workflow.\n\n### Prerequisites\n\n- Go 1.25+\n- [golangci-lint](https://golangci-lint.run/)\n- [gotestsum](https://github.com/gotestyourself/gotestsum) (for formatted test output)\n\n### Common Tasks\n\n- **Run Tests:** `task test`\n- **Run Linter:** `task lint`\n- **Format Code:** `task format`\n- **Tidy Modules:** `task tidy`\n\n## Integration Testing\n\nIntegration tests live behind the `integration` build tag and exercise the full\nclaim-check round-trip against real infrastructure.\n\n```bash\ntask test:integration\n```\n\nThere are three tests:\n\n- **`TestIntegrationKafka`** — publishes and consumes through a Redpanda (Kafka)\n  container, with MinIO as the blob store. Requires Docker.\n- **`TestIntegrationRabbitMQ`** — publishes and consumes through a RabbitMQ\n  container, with MinIO as the blob store. Requires Docker.\n- **`TestIntegrationExternal`** — runs against real backends you provide via\n  environment variables. **Skipped unless `CLAIMCHECK_IT_PUBSUB_URL` is set.**\n\nThe external test reads:\n\n| Variable | Purpose | Default |\n| :--- | :--- | :--- |\n| `CLAIMCHECK_IT_PUBSUB_URL` | pubsub URL used for **both** publish and consume (`rabbit://my-queue`, `kafka://my-topic`, `mem://t`, …) | _(required; test skipped if empty)_ |\n| `CLAIMCHECK_IT_BLOB_URL` | blob bucket URL (`s3://bucket?region=...\u0026endpoint=...\u0026use_path_style=true`, `file:///tmp/cc`, …) | `mem://` |\n| `CLAIMCHECK_IT_MESSAGE_COUNT` | number of messages to push | `1024` |\n\n`CLAIMCHECK_IT_PUBSUB_URL` is passed to both `pubsub.OpenTopic` and\n`pubsub.OpenSubscription`, so it must be valid as both for the chosen driver\n(e.g. a RabbitMQ exchange/queue with a binding). The Go CDK URL openers read\ntheir own variables, which you set for the real backends:\n\n- Kafka: `KAFKA_BROKERS` (comma-separated)\n- RabbitMQ: `RABBIT_SERVER_URL` (`amqp://...`)\n- S3: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, optionally `AWS_ENDPOINT_URL_S3`\n\nExample against a real RabbitMQ + real S3 bucket:\n\n```bash\nexport CLAIMCHECK_IT_PUBSUB_URL='rabbit://claimcheck'\nexport RABBIT_SERVER_URL='amqp://guest:guest@localhost:5672/'\nexport CLAIMCHECK_IT_BLOB_URL='s3://my-bucket?region=eu-west-1'\nexport AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=...\ntask test:integration\n```\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeczenyj%2Fgo-claimcheck","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpeczenyj%2Fgo-claimcheck","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeczenyj%2Fgo-claimcheck/lists"}