From 86c60f6865a453673a6801de19ca373579e67047 Mon Sep 17 00:00:00 2001
From: Yurii Motov
Date: Wed, 13 May 2026 16:52:40 +0200
Subject: [PATCH 1/6] Add overview and docs for backend
---
README.md | 192 +++++++++++++++++++++++++++++++++++++++++++++-
backend/README.md | 71 +++++++++++++++++
2 files changed, 260 insertions(+), 3 deletions(-)
create mode 100644 backend/README.md
diff --git a/README.md b/README.md
index 2651125..e492bf9 100644
--- a/README.md
+++ b/README.md
@@ -12,8 +12,194 @@
-# covered
+# Covered
-Self-hosted coverage reports storage for GitHub repositories.
+Self-hosted coverage report hosting for GitHub repositories — an alternative to Smokeshow, Codecov, and Coveralls for teams that prefer to keep their coverage data in-house and overcome limitations.
-Docs are coming soon.
+**Covered** has two parts:
+
+- A **FastAPI backend** that stores HTML coverage reports, serves them over HTTP, and exposes an SVG badge.
+- A **CLI** that runs in CI, uploads the report, posts a `covered` commit status to GitHub, and refreshes the badge cache.
+
+## Architecture
+
+Here is a [a bit simplified] diagram of how the components interact:
+
+```mermaid
+flowchart LR
+ S3[("AWS S3
HTML reports")]
+
+ CI["Covered CLI"]
+ Backend["Covered backend"]
+ Redis[("[Optional] Redis cache")]
+ GH["GitHub API"]
+ Reader["Browser"]
+
+ CI -->|"Upload report files"| S3
+
+ CI -->|"Get S3 credentials for upload session"| Backend
+ CI -->|"Set commit status"| GH
+ Reader -->|"Get badge SVG
Redirect to report URL
Serve report files"| Backend
+ Backend -->|"Get latest status"| GH
+ Backend --> |"Get cached badge or report URL"| Redis
+ Backend -->|"Get report files"| S3
+```
+
+CLI requests temporary credentials from the backend to upload the report to S3, uploads the report, then sets a `covered` status on the commit.
+When reader opens the page that contains badge, the badge is loaded from the backend, which looks up the latest `covered` status for the default branch commit, finds the corresponding coverage value, and serves the badge SVG. The badge links to the report URL, which is also served by the backend.
+Optional Redis caching can be used to reduce latency and GitHub API calls for frequently accessed badges.
+After uploading a report on the default branch, the CLI can also trigger a cache purge (if `-purge-cache` is specified) to ensure the badge reflects the new coverage value as soon as possible.
+
+## Getting started
+
+Setting up Covered takes two steps:
+
+1. **Deploy the backend** — provision S3, Redis, and a GitHub token, then deploy to FastAPI Cloud. See [backend/README.md](backend/README.md).
+2. **Wire up CI** — add the `covered` CLI to your test workflow. See the [GitHub Action setup](#github-action-setup) below; full CLI options are in [cli/README.md](cli/README.md).
+
+## GitHub Action setup
+
+In CI, coverage is uploaded by the [`covered` CLI](cli/README.md), which is published to PyPI. After your test job generates an HTML coverage report (typically `htmlcov/`), the CLI uploads it to your backend, posts a `covered` commit status on the commit, and refreshes the badge cache (if configured).
+
+### Workflow setup
+
+Before adding the workflows, make sure:
+
+- **The backend is deployed and reachable** — see [backend/README.md](backend/README.md).
+- **Your project uses `uv` with a committed `uv.lock`.** The workflows install dependencies via `uv sync --locked`.
+
+Covered's CLI runs in a **separate workflow** that triggers after your test workflow finishes. This split is what makes coverage work for pull requests from forks: a fork's workflow can't read your `COVERED_API_KEY` secret and can't post commit statuses, but the second workflow runs in the base repository's context and can do both.
+
+The first workflow runs your tests and uploads the HTML report as an artifact. The second downloads the artifact and invokes the `covered` CLI.
+
+#### 1. Test workflow
+
+```yaml
+# .github/workflows/test.yml
+name: Test
+
+on:
+ push:
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ with:
+ version: "0.11.7"
+ enable-cache: true
+ - run: uv sync --locked
+ - run: uv run coverage run -m pytest
+ - run: uv run coverage html # writes htmlcov/
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: coverage-html
+ path: htmlcov
+```
+
+#### 2. Coverage upload workflow
+
+```yaml
+# .github/workflows/coverage-upload.yml
+name: Coverage upload
+
+on:
+ workflow_run:
+ workflows: [Test]
+ types: [completed]
+
+permissions:
+ contents: read
+ actions: read # download the artifact from the Test run
+ statuses: write # post the `covered` commit status
+
+jobs:
+ upload:
+ runs-on: ubuntu-latest
+ if: github.event.workflow_run.conclusion == 'success'
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: coverage-html
+ path: htmlcov
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ run-id: ${{ github.event.workflow_run.id }}
+ - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ with:
+ version: "0.11.7"
+ enable-cache: true
+ - run: uv sync --locked
+ - name: Upload coverage to covered
+ run: uv run covered htmlcov
+ env:
+ COVERED_API_URL: ${{ secrets.COVERED_API_URL }}
+ COVERED_API_KEY: ${{ secrets.COVERED_API_KEY }}
+ COVERED_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ COVERED_REPO_OWNER: ${{ github.repository_owner }}
+ COVERED_REPO_NAME: ${{ github.event.repository.name }}
+ COVERED_COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
+ COVERED_COVERAGE_THRESHOLD: 99
+ COVERED_PURGE_CACHE: ${{ github.event.workflow_run.head_branch == github.event.repository.default_branch }}
+```
+
+#### 3. Add dev dependencies
+
+The workflows above expect `pytest`, `coverage`, and `covered` in the synced environment. Add them as dev dependencies:
+
+```bash
+uv add --dev pytest coverage covered
+```
+
+#### 4. Configure repository secrets
+
+In your GitHub repository settings, add:
+
+| Secret | Value |
+|---|---|
+| `COVERED_API_URL` | Base URL of your deployed backend, e.g. `https://covered.example.com`. |
+| `COVERED_API_KEY` | The `API_KEY` value you configured on the backend. |
+
+`COVERED_GH_TOKEN` does **not** need to be a custom PAT — the workflow-provided `${{ secrets.GITHUB_TOKEN }}` is sufficient, as long as the job has `statuses: write` permission.
+
+---
+
+A few non-obvious points worth knowing:
+
+- `COVERED_COMMIT_SHA` must be `github.event.workflow_run.head_sha`, **not** `github.sha`. When a workflow is triggered by `workflow_run`, `github.sha` points at the default branch, not the commit that was actually tested.
+- The same applies to `COVERED_PURGE_CACHE`: use `workflow_run.head_branch` to detect the default branch.
+- `actions: read` is needed because the artifact lives on a different workflow run.
+
+For a working pair of workflows, see this repo's [test.yml](.github/workflows/test.yml) and [coverage-upload.yml](.github/workflows/coverage-upload.yml). The full list of CLI options is in [cli/README.md](cli/README.md).
+
+### Adding the badge to your README
+
+Once the upload has run at least once on your default branch, add the badge to your repository's README:
+
+```markdown
+[](https://covered.example.com/badge/redirect/OWNER/REPO/)
+```
+
+Replace `covered.example.com` with your backend URL and `OWNER`/`REPO` with the repository's slug. The image points to the SVG badge endpoint. The link redirects to the latest stored report.
+
+### Troubleshooting
+
+| Symptom | Likely cause |
+|---|---|
+| Upload fails with 403 | `COVERED_API_KEY` doesn't match the backend's `API_KEY` setting. |
+| Upload succeeds but no `covered` status on the commit | The job is missing `statuses: write`, or `COVERED_GH_TOKEN` cannot post statuses on the repository. |
+| Coverage parsing is skipped | The directory passed to `covered` doesn't contain `index.html` — make sure `coverage html` ran successfully. |
+| Badge shows `??` | No `covered` commit status has been posted on the default branch yet — re-run the workflow on `master`/`main`. |
+| Badge doesn't refresh after a push to default branch | `COVERED_PURGE_CACHE` wasn't `true` for that run, or GitHub's image proxy (Camo) hasn't yet expired its cache (~5 minutes). |
+
+## License
+
+MIT. See [LICENSE](LICENSE).
diff --git a/backend/README.md b/backend/README.md
new file mode 100644
index 0000000..906cbef
--- /dev/null
+++ b/backend/README.md
@@ -0,0 +1,71 @@
+
+
+Make it green.
+
+
+# Covered backend
+
+FastAPI app that stores HTML coverage reports, serves them over HTTP, and exposes an SVG badge endpoint reflecting each repository's latest coverage on its default branch.
+
+This document is the operator manual — provisioning the external services, configuring the backend, deploying to [FastAPI Cloud](https://fastapicloud.com/), and verifying the install. For the project overview and the CI side of the picture, see the [top-level README](../README.md).
+
+## Prerequisites
+
+Before deploying the backend, you will need:
+
+- **An S3 bucket** to store the uploaded HTML reports. The backend writes objects under the `sites//...` prefix and reads them back when serving reports. No public access or static website hosting needs to be enabled — the backend serves files itself.
+
+- **An AWS IAM user for the backend**, with a long-lived access key. The user needs:
+ - `s3:PutObject` and `s3:GetObject` on `arn:aws:s3:::/sites/*` — used to create per-upload site directories and to serve report files.
+ - `sts:AssumeRole` on the upload role described below.
+
+- **An AWS IAM role for uploads**, whose ARN is passed to the backend as `AWS_UPLOAD_ROLE_ARN`. The backend assumes this role via STS to mint short-lived credentials that the CLI uses to upload report files directly to S3. The role needs:
+ - A permissions policy granting `s3:PutObject` on `arn:aws:s3:::/sites/*`. The backend further narrows this per upload via a session policy scoped to a single `site_id`, so the CLI never receives credentials that can write outside its own report directory.
+ - A trust policy allowing the backend IAM user to assume it.
+
+- **A Redis instance** reachable from the backend. It is used as a short-lived cache for rendered badge SVGs.
+
+- **A GitHub token** with read access to commit statuses on every repository whose coverage you want to display. A fine-grained PAT with `Commit statuses: Read-only` is sufficient; a classic PAT with `repo` (or `public_repo` for public repositories only) also works.
+
+## Environment variables
+
+The backend is configured via environment variables, loaded by `app.config.Settings` (pydantic-settings).
+
+| Variable | Required | Default | Description |
+|---|---|---|---|
+| `API_KEY` | yes | — | Token the CLI uses to authenticate with `/coverage/create-site/` and `/coverage/invalidate-cache/*`. Treat as a secret. |
+| `AWS_REGION` | no | `us-east-1` | Region of the S3 bucket. |
+| `AWS_BUCKET` | no | `covered` | Name of the S3 bucket reports are stored in. |
+| `AWS_ACCESS_KEY_ID` | yes | — | Access key of the backend's IAM user (the one with `s3:GetObject`/`PutObject` on `/sites/*` and `sts:AssumeRole` on the upload role). |
+| `AWS_SECRET_ACCESS_KEY` | yes | — | Secret access key for the above IAM user. |
+| `AWS_UPLOAD_ROLE_ARN` | yes | — | ARN of the IAM role the backend assumes via STS to mint short-lived upload credentials for the CLI. |
+| `REDIS_URL` | yes | — | Connection URL for Redis. Used to cache rendered badge SVGs (60 s TTL). |
+| `GITHUB_TOKEN` | yes | — | GitHub token used to read commit statuses. See [Prerequisites](#prerequisites) for the required scopes. |
+
+## Deploying to FastAPI Cloud
+
+Covered is designed to run on [FastAPI Cloud](https://fastapicloud.com/). The deploy flow:
+
+1. **Sign up at [fastapicloud.com](https://fastapicloud.com/).** Access is waitlist-only at the moment — getting in typically takes a couple of days.
+
+2. **Create an app** in the FastAPI Cloud dashboard.
+
+3. **Configure the environment variables** on the app (the [table above](#environment-variables) lists all of them). FastAPI Cloud also offers a Redis integration that can set `REDIS_URL` for you — use it if you want, otherwise paste in the URL from your Redis provider.
+
+4. **Clone (or fork) this repository.** Fork if you plan to customize the backend.
+
+5. **Install dependencies and deploy.** From the repo root:
+
+ ```bash
+ uv sync --project backend
+ cd backend
+ fastapi deploy
+ ```
+
+6. **Authorize the CLI.** On the first deploy, the CLI opens your browser to authorize itself against your FastAPI Cloud account — grant the requested permissions.
+
+7. **Pick your team and app** when prompted. Typically your personal team and the app you created in step 2; leave the rest at their defaults.
+
+8. **Confirm and wait** for the deploy to report success.
+
+9. **Verify** by triggering the [`Coverage upload` workflow](../README.md#workflow-setup) in a repository configured to use this backend. If anything fails, the FastAPI Cloud dashboard logs are the first place to look.
\ No newline at end of file
From d382b3fd2b16c6e0a497e534bc79881c533cecce Mon Sep 17 00:00:00 2001
From: Yurii Motov
Date: Wed, 13 May 2026 16:53:14 +0200
Subject: [PATCH 2/6] Fix command in `cli/README.md`
---
cli/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cli/README.md b/cli/README.md
index d34900e..6936272 100644
--- a/cli/README.md
+++ b/cli/README.md
@@ -33,7 +33,7 @@ covered ./htmlcov \
Every option also reads from a matching environment variable (`COVERED_API_URL`, `COVERED_API_KEY`, `COVERED_REPO_OWNER`, `COVERED_REPO_NAME`, `COVERED_COMMIT_SHA`, `COVERED_GH_TOKEN`, `COVERED_COVERAGE_THRESHOLD`, `COVERED_PURGE_CACHE`), which is the typical way to use it from CI.
-`covered upload --help` lists all options.
+`covered --help` lists all options.
## What it does
From 90e4d204ed245f8b3a79475b7b0b6ec4856126aa Mon Sep 17 00:00:00 2001
From: Yurii Motov
Date: Wed, 13 May 2026 16:56:49 +0200
Subject: [PATCH 3/6] Add empty lines for readability
---
README.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/README.md b/README.md
index e492bf9..ab9a064 100644
--- a/README.md
+++ b/README.md
@@ -46,8 +46,11 @@ flowchart LR
```
CLI requests temporary credentials from the backend to upload the report to S3, uploads the report, then sets a `covered` status on the commit.
+
When reader opens the page that contains badge, the badge is loaded from the backend, which looks up the latest `covered` status for the default branch commit, finds the corresponding coverage value, and serves the badge SVG. The badge links to the report URL, which is also served by the backend.
+
Optional Redis caching can be used to reduce latency and GitHub API calls for frequently accessed badges.
+
After uploading a report on the default branch, the CLI can also trigger a cache purge (if `-purge-cache` is specified) to ensure the badge reflects the new coverage value as soon as possible.
## Getting started
From 10cddf28a6f27335da2191911b33db3c9ad19771 Mon Sep 17 00:00:00 2001
From: Yurii Motov
Date: Thu, 14 May 2026 17:34:37 +0200
Subject: [PATCH 4/6] Update backend docs
---
backend/README.md | 19 +++---
backend/aws.md | 145 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 152 insertions(+), 12 deletions(-)
create mode 100644 backend/aws.md
diff --git a/backend/README.md b/backend/README.md
index 906cbef..dd0be81 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -13,17 +13,12 @@ This document is the operator manual — provisioning the external services, con
Before deploying the backend, you will need:
-- **An S3 bucket** to store the uploaded HTML reports. The backend writes objects under the `sites//...` prefix and reads them back when serving reports. No public access or static website hosting needs to be enabled — the backend serves files itself.
+- **AWS infrastructure** — an S3 bucket, an IAM user, and an IAM role. See [aws.md](aws.md) for step-by-step setup with the exact policy JSON. The short version of what you'll end up with:
+ - An **S3 bucket** for HTML reports. The backend writes objects under the `sites//...` prefix and reads them back when serving reports. No public access or static website hosting — the backend serves files itself.
+ - An **IAM user** for the backend, with a long-lived access key. It needs `s3:PutObject` + `s3:GetObject` on `/sites/*`, and `sts:AssumeRole` on the upload role.
+ - An **IAM role for uploads**, with a permissions policy granting `s3:PutObject` on `/sites/*` and a trust policy allowing the IAM user to assume it. The backend assumes this role via STS to mint short-lived credentials for the CLI, further narrowed per upload by a session policy scoped to a single `site_id` — so the CLI never receives credentials that can write outside its own report directory.
-- **An AWS IAM user for the backend**, with a long-lived access key. The user needs:
- - `s3:PutObject` and `s3:GetObject` on `arn:aws:s3:::/sites/*` — used to create per-upload site directories and to serve report files.
- - `sts:AssumeRole` on the upload role described below.
-
-- **An AWS IAM role for uploads**, whose ARN is passed to the backend as `AWS_UPLOAD_ROLE_ARN`. The backend assumes this role via STS to mint short-lived credentials that the CLI uses to upload report files directly to S3. The role needs:
- - A permissions policy granting `s3:PutObject` on `arn:aws:s3:::/sites/*`. The backend further narrows this per upload via a session policy scoped to a single `site_id`, so the CLI never receives credentials that can write outside its own report directory.
- - A trust policy allowing the backend IAM user to assume it.
-
-- **A Redis instance** reachable from the backend. It is used as a short-lived cache for rendered badge SVGs.
+- **A Redis instance** (optional) reachable from the backend. It is used as a short-lived cache for rendered badge SVGs.
- **A GitHub token** with read access to commit statuses on every repository whose coverage you want to display. A fine-grained PAT with `Commit statuses: Read-only` is sufficient; a classic PAT with `repo` (or `public_repo` for public repositories only) also works.
@@ -39,7 +34,7 @@ The backend is configured via environment variables, loaded by `app.config.Setti
| `AWS_ACCESS_KEY_ID` | yes | — | Access key of the backend's IAM user (the one with `s3:GetObject`/`PutObject` on `/sites/*` and `sts:AssumeRole` on the upload role). |
| `AWS_SECRET_ACCESS_KEY` | yes | — | Secret access key for the above IAM user. |
| `AWS_UPLOAD_ROLE_ARN` | yes | — | ARN of the IAM role the backend assumes via STS to mint short-lived upload credentials for the CLI. |
-| `REDIS_URL` | yes | — | Connection URL for Redis. Used to cache rendered badge SVGs (60 s TTL). |
+| `REDIS_URL` | no | — | Connection URL for Redis. Used to cache rendered badge SVGs (60 s TTL). |
| `GITHUB_TOKEN` | yes | — | GitHub token used to read commit statuses. See [Prerequisites](#prerequisites) for the required scopes. |
## Deploying to FastAPI Cloud
@@ -50,7 +45,7 @@ Covered is designed to run on [FastAPI Cloud](https://fastapicloud.com/). The de
2. **Create an app** in the FastAPI Cloud dashboard.
-3. **Configure the environment variables** on the app (the [table above](#environment-variables) lists all of them). FastAPI Cloud also offers a Redis integration that can set `REDIS_URL` for you — use it if you want, otherwise paste in the URL from your Redis provider.
+3. **Configure the environment variables** on the app (the [table above](#environment-variables) lists all of them).
4. **Clone (or fork) this repository.** Fork if you plan to customize the backend.
diff --git a/backend/aws.md b/backend/aws.md
new file mode 100644
index 0000000..f42a17f
--- /dev/null
+++ b/backend/aws.md
@@ -0,0 +1,145 @@
+# AWS setup
+
+This guide walks through creating the S3 bucket, IAM user, and IAM role the [Covered backend](README.md) needs. If you already have AWS infrastructure to reuse, you can adapt the policies described here.
+
+## What you'll create
+
+Three resources:
+
+1. **An S3 bucket** — stores the uploaded HTML coverage reports.
+2. **An IAM user** for the backend — provides the long-lived credentials the backend uses to read reports back from S3 and to assume the upload role.
+3. **An IAM role** (the *upload role*) — assumed by the backend via STS to mint short-lived credentials. The backend hands those temporary credentials to the CLI, scoped via a session policy to a single report's directory, so the CLI can never write outside its own report.
+
+This two-credential design (long-lived backend user + short-lived assumed role) is what isolates each CI upload.
+
+## Placeholders used below
+
+| Placeholder | Meaning | Example |
+|---|---|---|
+| `covered-reports` | The S3 bucket name | `acme-covered-reports` |
+| `` | AWS region | `us-east-1` |
+| `` | Your 12-digit AWS account ID | `123456789012` |
+
+Replace them with your own values in the policy JSON.
+
+## 1. Create the S3 bucket
+
+In the [S3 Console](https://console.aws.amazon.com/s3/):
+
+1. Click **Create bucket**.
+2. **Bucket name**: `covered-reports` (or whatever you prefer).
+3. **Region**: ``. Pick one close to where the backend will run; the backend's `AWS_REGION` env var must match this.
+4. Leave all other settings at their defaults. In particular, **keep "Block all public access" enabled** — the backend serves files itself; the bucket does not need to be public.
+5. Click **Create bucket**.
+
+## 2. Create the IAM user
+
+We create the user before the role so the role's trust policy can reference the user's ARN.
+
+In the [IAM Console](https://console.aws.amazon.com/iam/), under **Users**:
+
+1. Click **Create user**.
+2. **User name**: `covered-backend` (any name).
+3. Do **not** check "Provide user access to the AWS Management Console" — the backend uses programmatic access only.
+4. On the **Set permissions** step, choose **Attach policies directly** and don't select anything. Click **Next**, then **Create user**.
+
+Open the created user and copy its **ARN** from the **Summary** panel — it looks like `arn:aws:iam:::user/covered-backend`. You'll paste it into the role's trust policy in the next step.
+
+## 3. Create the upload role
+
+In the IAM Console, under **Roles**:
+
+1. Click **Create role**.
+2. **Trusted entity type**: **Custom trust policy**.
+3. Paste this trust policy, substituting `` and the user name if you changed it:
+
+ ```json
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Principal": { "AWS": "arn:aws:iam:::user/covered-backend" },
+ "Action": "sts:AssumeRole"
+ }
+ ]
+ }
+ ```
+
+
+
+4. Click **Next**. Skip attaching any managed policy on this screen — we'll add an inline policy after creation.
+5. **Role name**: `covered-upload-role` (any name).
+6. Click **Create role**.
+
+Now open the role and add its permissions policy. On the **Permissions** tab, click **Add permissions → Create inline policy**, switch to the **JSON** editor, and paste:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": "s3:PutObject",
+ "Resource": "arn:aws:s3:::covered-reports/sites/*"
+ }
+ ]
+}
+```
+
+This is the outer bound — when the backend assumes the role, it applies a session policy that narrows access to a single `sites//*` prefix for that specific upload.
+
+Save the policy (name it `covered-upload-policy`).
+
+Finally, copy the role's **ARN** from the role's **Summary** panel — it looks like `arn:aws:iam:::role/covered-upload-role`. This is what you'll set as `AWS_UPLOAD_ROLE_ARN` on the backend.
+
+## 4. Attach the user's permissions policy
+
+Go back to the user from step 2. On the **Permissions** tab, click **Add permissions → Create inline policy**, switch to **JSON**, and paste:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "S3Access",
+ "Effect": "Allow",
+ "Action": ["s3:PutObject", "s3:GetObject"],
+ "Resource": "arn:aws:s3:::covered-reports/sites/*"
+ },
+ {
+ "Sid": "AssumeUploadRole",
+ "Effect": "Allow",
+ "Action": "sts:AssumeRole",
+ "Resource": "arn:aws:iam:::role/covered-upload-role"
+ }
+ ]
+}
+```
+
+Save the policy (name it `covered-backend-policy`).
+
+The backend needs `s3:PutObject` (to create the per-upload `site_id` directory) and `s3:GetObject` (to serve report files back to readers), plus `sts:AssumeRole` to mint upload credentials for the CLI.
+
+## 5. Create the access key
+
+On the user's **Security credentials** tab:
+
+1. Click **Create access key**.
+2. **Use case**: **Other**, then click **Next**.
+3. Click **Create access key**.
+4. **Copy the access key ID and the secret access key now** — the secret is shown only once and can never be retrieved again. These become `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` on the backend.
+
+## Backend env vars, recapped
+
+After all five steps you have:
+
+| Backend env var | Value |
+|---|---|
+| `AWS_REGION` | The region of the bucket (e.g. `us-east-1`) |
+| `AWS_BUCKET` | `covered-reports` |
+| `AWS_ACCESS_KEY_ID` | From step 5 |
+| `AWS_SECRET_ACCESS_KEY` | From step 5 |
+| `AWS_UPLOAD_ROLE_ARN` | From step 3 |
+
+Set these on FastAPI Cloud following [Deploying to FastAPI Cloud](README.md#deploying-to-fastapi-cloud).
From 27a81e643c890ee29e2354554d6d18e828a6a676 Mon Sep 17 00:00:00 2001
From: Yurii Motov
Date: Thu, 14 May 2026 17:34:52 +0200
Subject: [PATCH 5/6] Clarify that Redis is optional
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index ab9a064..a6a2af9 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ After uploading a report on the default branch, the CLI can also trigger a cache
Setting up Covered takes two steps:
-1. **Deploy the backend** — provision S3, Redis, and a GitHub token, then deploy to FastAPI Cloud. See [backend/README.md](backend/README.md).
+1. **Deploy the backend** — provision S3, Redis (optionally), and a GitHub token, then deploy to FastAPI Cloud. See [backend/README.md](backend/README.md).
2. **Wire up CI** — add the `covered` CLI to your test workflow. See the [GitHub Action setup](#github-action-setup) below; full CLI options are in [cli/README.md](cli/README.md).
## GitHub Action setup
From 4d1a48a19725add7110cd233315e2e5726d54430 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Thu, 14 May 2026 19:38:14 +0000
Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/README.md b/backend/README.md
index dd0be81..0d7a4e2 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -63,4 +63,4 @@ Covered is designed to run on [FastAPI Cloud](https://fastapicloud.com/). The de
8. **Confirm and wait** for the deploy to report success.
-9. **Verify** by triggering the [`Coverage upload` workflow](../README.md#workflow-setup) in a repository configured to use this backend. If anything fails, the FastAPI Cloud dashboard logs are the first place to look.
\ No newline at end of file
+9. **Verify** by triggering the [`Coverage upload` workflow](../README.md#workflow-setup) in a repository configured to use this backend. If anything fails, the FastAPI Cloud dashboard logs are the first place to look.