Ctrl K
ComunidadGuías…

CONTRIBUTING.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Contributing to Artifact Keeper

Thanks for your interest in contributing! Here's how to get started.

## Getting Started

1. Fork the repository
2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/artifact-keeper.git`
3. Enable the git hooks: `./scripts/setup-hooks.sh` (one time, see [Git hooks](#git-hooks))
4. Create a feature branch: `git checkout -b feat/your-feature` (use `fix/`, `chore/`, or `docs/` as appropriate)
5. Make your changes
6. Run the same checks CI runs (see [What CI checks](#what-ci-checks))
7. If the change is user-facing, add a CHANGELOG fragment (see [Changelog entries](#changelog-entries))
8. Commit and push to your fork
9. Open a Pull Request against `main`, referencing an issue (`Closes #N`)

## Git hooks

Run `./scripts/setup-hooks.sh` once after cloning (or `./scripts/dev.sh setup`). It points
git at the version-controlled hooks in `.githooks/`, which require only git and cargo, no
extra tooling to install:

- **pre-commit** runs `cargo fmt --all -- --check` when Rust files are staged (instant).
- **pre-push** runs `cargo check --workspace --all-targets` with `SQLX_OFFLINE=true`, so
  compile errors are caught before they reach a red PR. It needs no database, and it does not
  run the tests. CI runs them on every PR.

To run the unit tests before a push as well, set `AK_PRE_PUSH_TESTS=1`. The hook then runs
`cargo nextest run --workspace --lib --test-threads 8`, the runner CI uses, and stops with a
message if either of these is missing:

- [cargo-nextest](https://nexte.st): `cargo install cargo-nextest --locked`. Many tests rely on
  its one process per test, so plain `cargo test` fails on a clean tree (#3479).
- A migrated throwaway database in `DATABASE_URL`. The tests create and delete rows across the
  whole schema, so never point this at a live instance. `./scripts/dev.sh start` needs
  `sqlx-cli` (`cargo install sqlx-cli --locked`); `./scripts/dev.sh reset` wipes it afterwards:

```bash
./scripts/dev.sh start
set -a; . ./.env.local-dev; set +a
AK_PRE_PUSH_TESTS=1 git push
```

The npm scan-on-proxy regression tests in `handlers-1` skip locally when npm
is unavailable. CI uses Node.js 22 with npm 10.9.9 and sets
`AK_TESTS_REQUIRE_NPM=1` so missing npm fails instead of silently skipping.
Local runs do not enforce a particular npm version; use
`npm install --global npm@10.9.9` to match CI.

A full test build peaks around 20 GB of rustc memory, which is why this is not the default.

The hooks are fast local feedback, not a substitute for CI. Bypass once with
`git commit --no-verify` / `git push --no-verify` only when you genuinely need to.

## The offline query cache (`.sqlx/`)

CI and the git hooks compile with `SQLX_OFFLINE=true` against the committed `.sqlx/` cache.
After adding or changing a `sqlx::query!` macro, regenerate it against a migrated throwaway
database and commit the result:

```bash
export DATABASE_URL=postgresql://registry:registry@localhost:5432/artifact_registry
sqlx migrate run --source backend/migrations
cargo sqlx prepare --workspace -- --all-targets
cargo sqlx prepare --workspace --check -- --all-targets   # the cache is reproducible
```

Keep the `-- --all-targets`. Without it the inline test modules are not compiled, so prepare
deletes the entry for every query that only a test uses, and the next offline test build fails
(#4092). Do not hand-edit `.sqlx/*.json`; a hand-written entry goes stale the next time the
schema changes and makes `--check` fail for everyone.

## What CI checks

Every PR must be fully green before merge. The pipeline (`.github/workflows/ci.yml` and
related workflows) enforces, in order of how fast you can reproduce each locally:

| Gate | What it runs | Reproduce locally |
| --- | --- | --- |
| Formatting | `cargo fmt --all -- --check` | `cargo fmt --all -- --check` (pre-commit hook) |
| Lint | `cargo clippy --workspace --all-targets -- -D warnings` | same command |
| Migration slots | migration files must have unique numeric prefixes | `ls backend/migrations/ \| tail` |
| Unit tests | `cargo nextest run --workspace --lib --bins`, split into test shards (one instrumented build per shard, GitHub-hosted matrix; see TESTING.md "Test shards") | `cargo nextest run --workspace --lib --test-threads 8` (see CLAUDE.md); one shard: add `--features test-shard-<name>` |
| Shell tests | `scripts/ci/test-*.sh`, the CHANGELOG/workflow gates, dtrack-init | `for t in scripts/ci/test-*.sh; do bash "$t"; done` |
| Coverage floor | 50% of lines, read from the shards' merged `lcov.info` in the hosted `coverage-gates` job (PRs only, advisory) | `cargo llvm-cov --workspace --lib --summary-only` |
| New-code coverage | new/changed lines must be >= 70% covered (skipped under 10 new lines) | add tests for the lines you changed |
| Duplication | jscpd over changed `.rs` files, <= 3% | `jscpd --min-lines 10 --threshold 3 --format rust <files>` |
| Integration tests | `#[ignore]`d `backend/tests/*` suites, run in Backend Integration Tests on pushes and backend-touching PRs | `./scripts/dev.sh start` then `cargo nextest run --workspace --run-ignored ignored-only --test <name>` |
| Smoke E2E | docker compose up + smoke profile | `./scripts/run-e2e-tests.sh` |
| Security audit | `cargo audit` on the dependency tree | `cargo audit` |
| Linked issue | PR body must reference an issue (`Closes #N`) | n/a (PR body) |
| CHANGELOG fragments | every `changes/unreleased/*.md` is well-formed | `python3 scripts/ci/changelog-fragments.py validate` |
| CodeQL | static analysis | n/a (runs in CI) |

The hooks cover the fmt gate and compile errors, and with `AK_PRE_PUSH_TESTS=1` the unit tests.
Run clippy yourself. The coverage, duplication, and linked-issue gates run only in CI, so check
those before pushing a large change. Do not use
"push and see if CI passes" as a workflow, and do not bypass a failing gate with `--admin`.

## Changelog entries

Do not edit `CHANGELOG.md`. Each PR with a user-facing change adds **one new file**
`changes/unreleased/<issue-or-pr-number>-<slug>.md`, so no two PRs touch the same file and
merging one PR does not put every other open PR into conflict:

```markdown
---
section: Fixed
issues: [#1234]
---
- **What changed, in one bold sentence** (#1234). Why it was wrong and what the fix does.
```

`section` is one of `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`. The
body is exactly the bullet as it would read in `CHANGELOG.md`, and it must cite the issues
listed in `issues`. Full rules: [changes/README.md](changes/README.md). The maintainers
assemble the fragments into `CHANGELOG.md` when a release is cut. CI-only changes need no
fragment.

## Development Setup

### Prerequisites

- Rust 1.75+
- PostgreSQL 16
- Docker & Docker Compose (for integration tests)

### Running Locally

```bash
# Start dependencies
docker compose up -d postgres opensearch

# Run the backend
cargo run

# Run tests
cargo nextest run --workspace --lib --test-threads 8
```

## What to Contribute

- **Bug reports** — File an issue with steps to reproduce
- **Bug fixes** — Open a PR referencing the issue
- **New package format handlers** — See the WASM plugin system and [example plugin](https://github.com/artifact-keeper/artifact-keeper-example-plugin)
- **Documentation improvements** — Docs live in `site/src/content/docs/`
- **Feature requests** — Open a discussion in [GitHub Discussions](https://github.com/artifact-keeper/artifact-keeper/discussions)

## Guidelines

- Keep PRs focused on a single change
- Follow existing code style (`cargo fmt` enforces this)
- Add tests for new functionality
- Update documentation if your change affects user-facing behavior

### Offloading CPU-bound work with `spawn_blocking`

Request handlers run on the shared Tokio runtime workers. A long
synchronous computation on a worker stalls every other task scheduled on
that thread, so CPU-bound work over caller-controlled input must be moved
off the async path with `tokio::task::spawn_blocking`.

Wrap a parse/compute step in `spawn_blocking` when **both** are true:

- it is synchronous and CPU-bound (regex scans, XML/JSON deserialization,
  archive extraction, hashing, key generation), and
- the input size is attacker- or client-controlled and can exceed ~1 MiB
  (proxied upstream index/metadata bodies, uploaded manifests, SBOMs).

Pair the offload with an explicit size cap and reject oversized bodies
before parsing, as the PEP 503 simple-index proxy does
(`api/handlers/pypi.rs`: `MAX_SIMPLE_ROOT_BODY_BYTES` + `spawn_blocking`).
Small, bounded inputs (a single coordinate string, a short header) do not
need offloading.

## Regression-test contract for bug fixes

Every PR that fixes a bug must land with a regression test that fails on
`main` and passes on the PR. This is enforced by checkbox in the PR
template and by reviewer policy: `fix/*` PRs are not approved without
the box checked.

The test can live wherever fits the bug:

- **Unit test** in the same crate as the fixed code, when the bug is
  in pure logic.
- **Integration test** in the same repo, when the bug requires a real
  database, storage backend, or HTTP client.
- **End-to-end test** in
  [`artifact-keeper-test`](https://github.com/artifact-keeper/artifact-keeper-test),
  when the bug surfaces only when the deployed system is exercised
  through its native client (`mvn`, `npm`, `docker pull`, etc.).

For PRs that aren't bug fixes (`feat/`, `chore/`, `docs/`, `ci/`,
`refactor/`), check the "N/A" box on the template.

This is part of [Hardening Core](https://github.com/orgs/artifact-keeper/projects/2),
the stability program tracking the work to make every release deploy
clean from a fresh helm install.

## Reporting Security Issues

Please do **not** open a public issue for security vulnerabilities. Instead, email the maintainers directly or use GitHub's private vulnerability reporting.

## License

By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).