diff --git a/.cargo/config.toml.example b/.cargo/config.toml.example new file mode 100644 index 00000000..e5865a10 --- /dev/null +++ b/.cargo/config.toml.example @@ -0,0 +1,50 @@ +# Cargo configuration for the Lab271 JFrog Artifactory registry. +# +# THIS FILE IS A TEMPLATE AND IS NOT ACTIVE. Copy it to `.cargo/config.toml` +# (gitignored) to opt in: +# +# cp .cargo/config.toml.example .cargo/config.toml +# +# It is deliberately not committed as `.cargo/config.toml`. This repository is +# public, and the Artifactory Cargo index requires authentication — an +# anonymous request to it returns 401, not a fallthrough to crates.io. A +# committed source replacement would therefore break `cargo build` for every +# outside contributor and every fork. See ADR-0038. +# +# Requires a JFrog identity token. Cargo reads it from +# `CARGO_REGISTRIES_LAB_CARGO_DEV_TOKEN` (preferred, nothing on disk) or from +# `~/.cargo/credentials.toml`. Never put a token in this file or in +# `.cargo/config.toml`. + +# --- Resolving public crates through the Artifactory cache ------------------ +# +# Source replacement, so no Cargo.toml or Cargo.lock change is needed. Cargo +# requires a replacement source to serve byte-identical crates, and verifies +# every `.crate` against the checksum already pinned in Cargo.lock — so this +# cannot silently substitute a different dependency, and Cargo.lock stays +# portable (its `source` entries keep pointing at crates.io). +[source.crates-io] +replace-with = "lab-cargo-dev" + +[source.lab-cargo-dev] +registry = "sparse+https://schubergphilis.jfrog.io/artifactory/api/cargo/lab-cargo-dev/index/" + +# --- Publishing this crate, and consuming it elsewhere --------------------- +# +# The named-registry form. Needed by `cargo publish --registry lab-cargo-dev`, +# and by any other crate that depends on this one as +# `sqlite-rs = { version = "0.18", registry = "lab-cargo-dev" }`. +# +# Prefer that named form over relying on the source replacement above when +# depending on sqlite-rs: under source replacement, the consumer's Cargo.lock +# records our private crate as `source = "registry+https://github.com/ +# rust-lang/crates.io-index"`, which is untrue and makes `deny.toml`'s +# `sources` check (`allow-registry = [crates.io]`) pass it silently. +[registries.lab-cargo-dev] +index = "sparse+https://schubergphilis.jfrog.io/artifactory/api/cargo/lab-cargo-dev/index/" + +# Promoted releases only — backed by `lab-cargo-prod-local` with no remote, so +# it can never reach crates.io. That also makes it the only one of the two +# immune to the name collision described in docs/src/jfrog-registry.md. +[registries.lab-cargo-prod] +index = "sparse+https://schubergphilis.jfrog.io/artifactory/api/cargo/lab-cargo-prod/index/" diff --git a/.github/workflows/jfrog.yml b/.github/workflows/jfrog.yml new file mode 100644 index 00000000..79139655 --- /dev/null +++ b/.github/workflows/jfrog.yml @@ -0,0 +1,304 @@ +name: JFrog + +# Private crate registry: resolve through the Artifactory cache, publish the +# packaged crate with build-info, promote a release to prod. See +# docs/src/jfrog-registry.md and ADR-0038. +# +# Every job is gated on the JF_ACCESS_TOKEN secret being present and skips +# cleanly when it is not. Forked pull requests never receive secrets, so this +# workflow must never be the reason a contributor's PR shows red — a +# JFrog-caused failure is a finding against the evaluation's reliability +# criterion, and the threshold there is zero. + +on: + pull_request: + branches: [main] + paths: + - ".github/workflows/jfrog.yml" + - ".cargo/config.toml.example" + - "Cargo.toml" + - "Cargo.lock" + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: read + +env: + JF_PROJECT: ${{ vars.JF_PROJECT }} + JF_URL: ${{ vars.JF_URL }} + +jobs: + # Is the credential available at all? Forked PRs get no secrets, and + # `secrets` cannot be referenced in a job-level `if:`, so the answer has to + # be computed in a job and passed on as an output. + preflight: + name: Credential available + runs-on: ubuntu-latest + outputs: + have-token: ${{ steps.check.outputs.have-token }} + steps: + - id: check + env: + TOKEN: ${{ secrets.JF_ACCESS_TOKEN }} + run: | + if [ -n "$TOKEN" ] && [ -n "${{ vars.JF_URL }}" ]; then + echo "have-token=true" >> "$GITHUB_OUTPUT" + else + echo "have-token=false" >> "$GITHUB_OUTPUT" + echo "::notice::No JFrog credential on this ref (expected on forked PRs) — JFrog jobs will skip." + fi + + # Resolve the whole dependency closure through `lab-cargo-dev`, then prove a + # separate crate can consume the promoted `sqlite-rs` from `lab-cargo-prod`. + # The second half is the point of the exercise: eighteen releases in, no + # colleague has ever been able to write `sqlite-rs = "0.18"` and have it + # resolve. + resolve: + name: Resolve through lab-cargo-dev + needs: preflight + if: needs.preflight.outputs.have-token == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable + + # Written to the runner's CARGO_HOME, never to the working tree — the + # repository is public and a committed source replacement would 401 + # every anonymous clone (ADR-0038). Credentials go via the environment, + # so no token is ever written to disk. + - name: Configure Cargo for Artifactory + run: | + mkdir -p "$HOME/.cargo" + cat >> "$HOME/.cargo/config.toml" < "$probe/Cargo.toml" < "$probe/src/main.rs" <<'EOF' + fn main() { + // Touch real public items so the dependency is genuinely + // compiled and linked, not merely resolved and discarded. + assert_eq!(sqlite_rs::header::HEADER_LEN, 100); + println!("consumed sqlite-rs from lab-cargo-prod"); + } + EOF + cd "$probe" && cargo run --quiet + grep -A3 'name = "sqlite-rs"' Cargo.lock + + # Package and upload with build-info. Deliberately does not use + # `cargo publish`: `package.publish` stays `false` (ADR-0038), and only the + # upload path records build-info. Tags and manual runs only — a PR does not + # publish. + publish: + name: Publish to lab-cargo-dev-local + needs: [preflight, resolve] + if: >- + needs.preflight.outputs.have-token == 'true' && + (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') + runs-on: ubuntu-latest + outputs: + version: ${{ steps.pack.outputs.version }} + digest: ${{ steps.upload.outputs.digest }} + already-published: ${{ steps.guard.outputs.already-published }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # build-add-git needs real history to read the revision from. + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable + - uses: jfrog/setup-jfrog-cli@279b1f629f43dd5bc658d8361ac4802a7ef8d2d5 # v4.9.1 + env: + JF_URL: ${{ vars.JF_URL }} + JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }} + + # `cargo package` works fine under `publish = false`; it is only + # `cargo publish` that the flag blocks. + - id: pack + name: cargo package + run: | + version=$(cargo metadata --no-deps --format-version=1 \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])') + echo "version=$version" >> "$GITHUB_OUTPUT" + cargo package --locked + + # Never re-publish a version that already exists. This is not + # defensive tidiness — it is load-bearing, and it was found the hard + # way: + # + # * `cargo package` is not byte-reproducible across machines. The + # same commit produced sha256:eb1deeae… on a laptop and + # sha256:84074b9f… on a runner. + # * `lab-cargo-*-local` are mutable. Re-uploading the same version + # overwrote the bytes silently, with no error, including a version + # already promoted with status `Released`. + # * Cargo pins checksums in Cargo.lock. So an overwrite is not a + # harmless re-upload; every existing consumer breaks outright with + # "checksum for `sqlite-rs v0.18.10` changed between lock files". + # + # Repository immutability on `lab-cargo-prod-local` + # (Lab271/labs-jfrog-poc#4) is the real fix and needs platform admin we + # do not have. Until then this guard is the only thing standing between + # a re-run and a broken downstream lockfile. + - id: guard + name: Refuse to overwrite an existing version + run: | + v="${{ steps.pack.outputs.version }}" + found=false + for repo in lab-cargo-dev-local lab-cargo-prod-local; do + code=$(jf rt curl -s -o /dev/null -w '%{http_code}' \ + "/api/storage/${repo}/crates/sqlite-rs/sqlite-rs-${v}.crate") + if [ "$code" = "200" ]; then + echo "::warning::sqlite-rs ${v} already exists in ${repo}." + found=true + fi + done + echo "already-published=$found" >> "$GITHUB_OUTPUT" + if [ "$found" = "true" ]; then + echo "::notice::Nothing to publish — bump the version to release new bytes. Skipping publish and promote." + fi + + # `--project` is mandatory on every build-info command. Without it the + # CLI targets the platform-level `artifactory-build-info` repository and + # gets a flat 403 whose message names that repository rather than the + # missing flag. + - id: upload + name: Upload with build-info + if: steps.guard.outputs.already-published == 'false' + run: | + v="${{ steps.pack.outputs.version }}" + n="${{ github.run_number }}" + jf rt upload "target/package/sqlite-rs-${v}.crate" \ + "lab-cargo-dev-local/crates/sqlite-rs/sqlite-rs-${v}.crate" \ + --build-name=sqlite-rs --build-number="$n" --project="$JF_PROJECT" + jf rt build-add-git sqlite-rs "$n" --project="$JF_PROJECT" + jf rt build-publish sqlite-rs "$n" --project="$JF_PROJECT" + # Recorded BEFORE any promotion: `jf rt build-promote` moves by + # default, and after a move there is nothing left in dev to compare + # the promoted bytes against. + digest=$(jf rt curl -s \ + "/api/storage/lab-cargo-dev-local/crates/sqlite-rs/sqlite-rs-${v}.crate" \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["checksums"]["sha256"])') + echo "digest=$digest" >> "$GITHUB_OUTPUT" + echo "::notice::dev digest sha256:${digest}" + + # Promote the same bytes to prod. The `jfrog-prod` environment is what makes + # the approver visible in the audit trail. + # + # NOTE FOR A HUMAN: the environment does not exist yet. GitHub creates it on + # this workflow's first run with NO protection rules, which means this job is + # currently ungated. Add required reviewers under + # Settings -> Environments -> jfrog-prod before treating it as an approval + # gate. Until then the environment records who triggered the run, not who + # approved it. + promote: + name: Promote to lab-cargo-prod-local + needs: [preflight, publish] + if: >- + needs.preflight.outputs.have-token == 'true' && + needs.publish.outputs.already-published == 'false' + runs-on: ubuntu-latest + environment: jfrog-prod + steps: + - uses: jfrog/setup-jfrog-cli@279b1f629f43dd5bc658d8361ac4802a7ef8d2d5 # v4.9.1 + env: + JF_URL: ${{ vars.JF_URL }} + JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }} + + # `--copy=true` is not optional. build-promote MOVES by default, which + # would delete the crate from dev and break anything still resolving + # that version from `lab-cargo-dev`. + - name: Promote, then assert the digest is unchanged + run: | + v="${{ needs.publish.outputs.version }}" + jf rt build-promote sqlite-rs "${{ github.run_number }}" \ + lab-cargo-prod-local --project="$JF_PROJECT" \ + --status=Released --copy=true + for _ in $(seq 1 12); do + prod=$(jf rt curl -s \ + "/api/storage/lab-cargo-prod-local/crates/sqlite-rs/sqlite-rs-${v}.crate" \ + | python3 -c 'import json,sys; print(json.load(sys.stdin).get("checksums",{}).get("sha256",""))') + [ -n "$prod" ] && break + sleep 5 + done + echo "dev sha256:${{ needs.publish.outputs.digest }}" + echo "prod sha256:${prod}" + if [ "$prod" != "${{ needs.publish.outputs.digest }}" ]; then + echo "::error::Promoted digest differs from the dev digest — the bytes were not preserved." + exit 1 + fi + echo "::notice::Promoted bytes identical to dev — no rebuild in the path." diff --git a/.gitignore b/.gitignore index 0144c84d..19d03864 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,10 @@ __pycache__/ # mdBook build output (`make docs`) /docs/book/ + +# Local opt-in for the Artifactory Cargo registry (ADR-0038). Committing this +# would 401 every anonymous `cargo build` on this public repo, so only the +# `.example` template is tracked. Ignored rather than merely absent so a local +# copy cannot be committed by accident. +/.cargo/config.toml +/.cargo/credentials.toml diff --git a/.openspec/adr/0038-cargo-registry-opt-in-not-committed.md b/.openspec/adr/0038-cargo-registry-opt-in-not-committed.md new file mode 100644 index 00000000..3c589907 --- /dev/null +++ b/.openspec/adr/0038-cargo-registry-opt-in-not-committed.md @@ -0,0 +1,121 @@ +# 0038: Artifactory Cargo access is opt-in local config, never a committed source replacement + +Date: 2026-09-03 + +## Context + +This crate has shipped eighteen releases and no colleague can depend on it, +because `publish = false` and there is nowhere to publish to. Lab271's JFrog +evaluation ([labs-jfrog-poc#12](https://github.com/Lab271/labs-jfrog-poc/issues/12)) +made a private Cargo registry available: `lab-cargo-dev`, a virtual repository +over `lab-cargo-dev-local`, `lab-cargo-prod-local` and a `crates.io` cache. + +Cargo has no per-project registry config that lives outside the working tree. +npm has `.npmrc`, and the JFrog CLI can sidestep even that with +`jf npm-config`, which writes `.jfrog/projects/npm.yaml`. **There is no +`jf cargo-config` and no `jf cargo` command at all** — `jf --help` says so +outright: "Cargo has no dedicated 'jf cargo-config' command — it reads the +Artifactory registry directly from `.cargo/config.toml`." So the only +mechanisms Cargo offers are `.cargo/config.toml` (project or user level) and +`CARGO_*` environment variables. + +That matters because **this repository is public** and the Artifactory Cargo +index is not anonymously readable. Measured directly: + +``` +$ curl -o /dev/null -w '%{http_code}' \ + https://schubergphilis.jfrog.io/artifactory/api/cargo/lab-cargo-dev/index/config.json +401 +``` + +The index's own `config.json` also advertises `"auth-required": true`, which +Cargo honours for crate *downloads* as well as index reads. A committed +`.cargo/config.toml` carrying `[source.crates-io] replace-with = ...` would +therefore turn every anonymous `cargo build` — every outside contributor, +every fork, every CI run without the secret — into a 401 on the first +dependency fetch. The same trade-off already came up in the npm pilot of the +same evaluation, where a committed `.npmrc` was rejected for exactly this +reason. + +## Decision + +Artifactory access is **opt-in and untracked**. The repository commits +`.cargo/config.toml.example` and gitignores `.cargo/config.toml` and +`.cargo/credentials.toml`. A developer who wants the cache copies the +template; everyone else is unaffected and resolves from `crates.io` as +before. No file in the default clone points at Artifactory. + +`package.publish` **stays `false`**. Publishing to Artifactory does not +require changing it: `cargo package` works under `publish = false`, and the +resulting `.crate` can be deployed with `jf rt upload`, which Artifactory +indexes into a valid Cargo registry entry on its own (verified: index entry +appeared ~5s after upload, with `deps`, `features` and `cksum` parsed from +`Cargo.toml`). Whether to make this crate publishable at all is a separate, +unmade decision and this ADR does not pre-empt it. + +Two consumption forms are documented, and the named-registry form is +preferred for depending on *this* crate: + +- `[source.crates-io] replace-with = ...` — for the upstream cache only. + Cargo enforces byte-identical content in a replacement source and checks + every `.crate` against the checksum already in `Cargo.lock`, so this is + provenance-preserving for public crates and leaves `Cargo.lock` portable. +- `sqlite-rs = { version = "0.18", registry = "lab-cargo-dev" }` — for + depending on this crate. Under source replacement instead, the consumer's + lockfile records our private crate as + `source = "registry+https://github.com/rust-lang/crates.io-index"`, which + is false, and which makes `deny.toml`'s `sources` check + (`allow-registry = ["https://github.com/rust-lang/crates.io-index"]`, + `unknown-registry = "deny"`) pass a private-registry dependency silently. + The named form records the real index URL and the gate sees it. + +## Alternatives rejected + +- **Commit `.cargo/config.toml` with the source replacement**, as + [doc 3 of the PoC](https://github.com/Lab271/labs-jfrog-poc/blob/main/docs/03-promotion-and-xray.md) + suggests per-repo registry config generally. Rejected on the measured 401 + above: it breaks the public contributor path outright, and it breaks it at + dependency-fetch time with an authentication error that gives an outside + contributor no hint that the fix is to delete a file they did not add. +- **Set `package.publish = ["lab-cargo-dev"]`.** This does work, and it is + narrower than it looks — `cargo publish --registry crates-io` still fails + with "The registry `crates-io` is not listed in the `package.publish` + value", and a bare `cargo publish` auto-targets the single allowed registry + ("found `lab-cargo-dev` as only allowed registry"). So it would *not* + silently open a path to crates.io. Rejected anyway because it is + unnecessary: the `jf rt upload` path publishes a `publish = false` crate + fine, and it is the path that also produces build-info + (`--build-name`/`--build-number`), which `cargo publish` cannot. Keeping + `publish = false` leaves the crates.io question untouched, which is where it + belongs. Revisit if we ever want `cargo publish` itself in the release path. +- **A user-level `~/.cargo/config.toml` only, with nothing in the repo.** + Rejected as undiscoverable: the point of the exercise is that a colleague + can consume this crate, and a mechanism documented nowhere in the + repository does not achieve that. The `.example` file is the discoverable + half; the gitignore is the safety half. +- **Vendoring (`cargo vendor`) instead of a registry.** Rejected: it solves + offline builds, not distribution. A colleague still could not write + `sqlite-rs = "0.18"` in their own `Cargo.toml`, which is the actual problem. + +## Consequences + +- The default clone is unchanged. `cargo build`, `cargo test` and CI on a + fork resolve from `crates.io` exactly as before; nothing in the tracked + tree references Artifactory except documentation and one `.example` file. +- Resolving through `lab-cargo-dev` leaves `Cargo.lock` untouched — all 116 + `source` entries still read `registry+https://github.com/rust-lang/crates.io-index` + after a full `cargo fetch --locked` through the proxy. Verified, and it is a + guarantee rather than an accident: Cargo refuses a replacement source whose + checksums differ. +- **`sqlite-rs` is already taken on crates.io** — an unrelated crate, 19 + versions up to 0.3.7. `lab-cargo-dev` merges its cache with our local + repository, so that index path serves 20 versions from two different + projects with nothing distinguishing them. Our `0.18.10` resolves correctly + today only because the version ranges happen not to overlap. + `lab-cargo-prod` has no remote and serves `0.18.10` alone. Consumers of + released versions should point at `lab-cargo-prod`; the collision is + recorded in `docs/src/jfrog-registry.md` and is an argument for renaming the + crate before any public release. +- Anyone who copies the template still needs a JFrog identity token. Nothing + in this repository can mint one — see `docs/src/jfrog-registry.md` for what + a human has to create. diff --git a/.openspec/adr/index.md b/.openspec/adr/index.md index cb18749d..2b4d7e6c 100644 --- a/.openspec/adr/index.md +++ b/.openspec/adr/index.md @@ -41,3 +41,4 @@ Specs record what the system must do; ADRs record **why it is shaped this way** | [0035](0035-wal-resume-hint-cache-supersedes-0026.md) | `Pager`-cached WAL resume hint supersedes ADR-0026's per-flush rescan | 2026-08-29 | | [0036](0036-pragma-synchronous-fsync-policy.md) | `PRAGMA synchronous` fsync-skip policy, and why `SynchronousMode` lives in `header.rs` | 2026-08-29 | | [0037](0037-macos-plain-fsync-not-fullfsync.md) | On macOS, `Vfs::sync` calls plain `fsync(2)`, not `std`'s `F_FULLFSYNC` | 2026-08-30 | +| [0038](0038-cargo-registry-opt-in-not-committed.md) | Artifactory Cargo access is opt-in local config, never a committed source replacement | 2026-09-03 | diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 4d54c4d2..646f414c 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -15,6 +15,7 @@ # Reference - [Plan](plan.md) +- [Private crate registry](jfrog-registry.md) - [ADR Index](adr/index.md) - [0001 — `-shm` access via pread/pwrite, not mmap](adr/0001-shm-access-pread-not-mmap.md) - [0002 — Value blocks over layer-ordered development](adr/0002-value-blocks-over-layers.md) diff --git a/docs/src/jfrog-registry.md b/docs/src/jfrog-registry.md new file mode 100644 index 00000000..8a104507 --- /dev/null +++ b/docs/src/jfrog-registry.md @@ -0,0 +1,130 @@ +# Private crate registry (JFrog Artifactory) + +`package.publish` is `false` and this crate is not on crates.io, so for +eighteen releases there has been no way for anyone else to depend on it. This +page describes the private Cargo registry that fixes that, set up as part of +[Lab271's JFrog evaluation](https://github.com/Lab271/labs-jfrog-poc). It is +**opt-in**: nothing in a default clone points at it, and `cargo build` on a +fresh clone or a fork resolves from crates.io exactly as before. See +[ADR-0038](adr/0038-cargo-registry-opt-in-not-committed.md) for why it must +stay that way. + +## Repositories + +| Key | Kind | Contents | +|---|---|---| +| `lab-cargo-dev` | virtual | everything: dev, prod, and the crates.io cache | +| `lab-cargo-prod` | virtual | promoted releases only — **no remote**, cannot reach the internet | +| `lab-cargo-dev-local` | local | where `sqlite-rs` is published | +| `lab-cargo-prod-local` | local | where a release is promoted to | +| `lab-crates-io-remote` | remote | caching proxy for `index.crates.io` | + +The index URL for any of them is +`sparse+https://schubergphilis.jfrog.io/artifactory/api/cargo//index/`. + +## Opting in + +```bash +cp .cargo/config.toml.example .cargo/config.toml +export CARGO_REGISTRIES_LAB_CARGO_DEV_TOKEN="" +cargo fetch --locked +``` + +`.cargo/config.toml` is gitignored. Read the template's comments before +editing it — it explains which of the two mechanisms to use when. + +Prefer the environment variable to `~/.cargo/credentials.toml`; it keeps the +token off disk. Nothing in this repository can create a token — a human with +access to the `lab` project has to issue one from the JFrog UI. + +`Cargo.lock` is unaffected. After a full `cargo fetch --locked` through +Artifactory all 116 `source` entries still read +`registry+https://github.com/rust-lang/crates.io-index`, because Cargo +requires a replacement source to serve byte-identical crates and verifies each +one against the checksum already in the lockfile. The lockfile stays portable +and public clones keep working. + +## Publishing + +`publish = false` does **not** have to change. `cargo package` works under it, +and the resulting `.crate` is deployed with the JFrog CLI, which also records +build-info: + +```bash +cargo package --locked +jf rt upload target/package/sqlite-rs-.crate \ + "lab-cargo-dev-local/crates/sqlite-rs/sqlite-rs-.crate" \ + --build-name=sqlite-rs --build-number="$N" --project=lab +jf rt build-add-git sqlite-rs "$N" --project=lab +jf rt build-publish sqlite-rs "$N" --project=lab +``` + +`--project=lab` is mandatory on the build-info commands; without it the CLI +targets a platform-level repository and gets a flat 403 whose message names +the repository rather than the missing flag. + +Artifactory generates the Cargo index entry from the uploaded `.crate` itself +— parsing `Cargo.toml` for `deps`, `features` and the checksum — a few seconds +after the upload lands. There is no `jf cargo` command and none is needed. + +`cargo publish` is the other option, but it requires `package.publish` to name +the registry and it cannot produce build-info. ADR-0038 records why the upload +path is preferred. + +### Promoting a release + +```bash +jf rt build-promote sqlite-rs "$N" lab-cargo-prod-local \ + --project=lab --status=Released --copy=true +``` + +`--copy=true` is not optional in practice: **`build-promote` moves by +default**, which would delete the crate from `lab-cargo-dev-local` and break +anything still resolving that version from `lab-cargo-dev`. Record the dev +digest before promoting, because after a move there is nothing left to compare +against. + +## Consuming it from another crate + +```toml +[dependencies] +sqlite-rs = { version = "0.18", registry = "lab-cargo-prod" } +``` + +with the matching `[registries.lab-cargo-prod]` block from the template in the +consumer's own `.cargo/config.toml`. + +Use this named-registry form, not the source replacement, when depending on +`sqlite-rs`. Under source replacement the consumer's lockfile records + +```toml +source = "registry+https://github.com/rust-lang/crates.io-index" +``` + +for a crate that is not on crates.io at all. That is untrue, and it makes +[`deny.toml`](https://github.com/Lab271/sqlite-rs/blob/main/deny.toml)'s +`sources` check — `unknown-registry = "deny"` with `allow-registry` set to +crates.io only — pass a private-registry dependency without comment. The +named form records the real index URL, and the gate sees it. + +## Two things to know before relying on this + +**The crate name is already taken on crates.io.** An unrelated `sqlite-rs` +has 19 published versions, up to `0.3.7`. `lab-cargo-dev` merges its crates.io +cache with our local repository, so that one index path serves 20 versions +belonging to two different projects, with nothing in the metadata +distinguishing them. Our `0.18.10` resolves correctly today only because the +version ranges do not overlap. `lab-cargo-prod` has no remote and serves +`0.18.10` alone, which is why release consumers should point there. Renaming +the crate is the real fix and should happen before any public release. + +**Xray does not scan this crate's dependency graph.** `jf audit` on the source +tree reports the project as `[unknown]` and generates an SBOM with *no library +components* — it does not recognise `Cargo.toml`/`Cargo.lock` as a dependency +manifest at all, so the 116-crate closure that `make check-deny` and +`sqlite-rs-dev.cdx.json` already cover is never examined. Xray does have Cargo +CVE data and does apply it to a `.crate` artifact scanned with `jf scan` +(confirmed against a deliberately vulnerable `time 0.1.44`, which reports +`CVE-2020-26235`, type `cargo`), but this crate has zero runtime dependencies, +so there is nothing there for it to find. `make check-deny` remains the +supply-chain gate; Artifactory adds distribution, not assurance.