Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/nightly-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
# the same tree by anyone following the local-run docs, so baking key or
# credential material into it would leak just as widely.
release_paths = [
Path("release/docker/Dockerfile.discovery"),
Comment thread
jeremi marked this conversation as resolved.
Path("release/docker/Dockerfile.evidence"),
Path("release/docker/Dockerfile.mint"),
Path("release/docker/Dockerfile.relay"),
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release-canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
PY
printf '{"spdxVersion":"SPDX-2.3","name":"release-canary"}\n' \
> canary/bundle-root/registry-stack-${tag}.sbom.spdx.json
image_names=(relay evidence mint)
image_names=(relay evidence mint discovery)

write_image_reports() {
local name="$1"
Expand Down Expand Up @@ -105,6 +105,7 @@ jobs:
schema_version:"registry-stack.advisory-verdict.v2",
verdict:"passed",
subjects:[
"discovery-image",
"evidence-image",
"mint-image",
"relay-image"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-repeatability.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ jobs:
raise SystemExit(f"expected one release manifest for {version}")
artifacts = matches[0].get("artifacts", {})
binary_names = {
"discovery": "discovery",
"evidence": "evidence",
"evidencectl": "evidencectl",
"evidence-oid4vci": "evidence-oid4vci",
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/registry-discovery/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ server = [
"dep:axum",
"dep:clap",
"dep:http",
"dep:registry-platform-buildinfo",
"dep:registry-platform-httpsec",
"dep:serde_yaml_ng",
"dep:tokio",
Expand All @@ -35,6 +36,7 @@ axum = { workspace = true, optional = true }
clap = { workspace = true, optional = true }
http = { workspace = true, optional = true }
registry-discovery-profile.workspace = true
registry-platform-buildinfo = { workspace = true, optional = true }
registry-platform-canonical-json.workspace = true
registry-platform-httpsec = { workspace = true, features = ["server"], optional = true }
serde.workspace = true
Expand Down
3 changes: 2 additions & 1 deletion crates/registry-discovery/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use tracing_subscriber::prelude::*;
#[derive(Debug, Parser)]
#[command(
name = "discovery",
about = "Serve one immutable Registry Discovery index"
about = "Serve one immutable Registry Discovery index",
version = registry_platform_buildinfo::DISPLAY_VERSION
)]
struct Arguments {
#[arg(long, value_name = "FILE")]
Expand Down
44 changes: 44 additions & 0 deletions crates/registry-discovery/tests/version_cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#![cfg(feature = "server")]

use std::process::Command;

fn version_output(flag: &str) -> String {
let output = Command::new(env!("CARGO_BIN_EXE_discovery"))
.arg(flag)
.output()
.unwrap_or_else(|err| panic!("discovery {flag} runs: {err}"));

assert!(
output.status.success(),
"discovery {flag} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).into_owned()
}

#[test]
fn version_output_uses_user_facing_command_name() {
for flag in ["--version", "-V"] {
assert_eq!(
version_output(flag),
format!(
"discovery {}\n",
registry_platform_buildinfo::DISPLAY_VERSION
)
);
}
}

#[test]
fn version_output_marks_a_build_that_is_not_a_release() {
let expected = if registry_platform_buildinfo::IS_RELEASE_BUILD {
env!("CARGO_PKG_VERSION").to_owned()
} else {
format!("{}-dev", env!("CARGO_PKG_VERSION"))
};

assert_eq!(
version_output("--version"),
format!("discovery {expected}\n")
);
}
2 changes: 1 addition & 1 deletion docs/site/src/content/docs/reference/api-stability.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ The enforcement column names the repository CI checks so the mechanism is audita
| Configuration formats and documented environment variables | Relay's `registry.yaml` and `runtime.yaml` grammars, the frozen Evidence Gateway schemas `products/evidence/contracts/runtime.schema.yaml` and `products/evidence/contracts/bundle.schema.yaml`, plus the [environment variable reference](../environment-variables/) | Relay and Evidence Gateway both parse with `deny_unknown_fields`, so a retired or misspelled key is a startup refusal rather than an ignored field; Evidence Gateway's config parser is tested against the frozen contract schemas (`crates/registry-evidence/src/config.rs`), and `products/evidence/scripts/check-config-key-paths.sh` holds its configuration reference in exact parity with them |
| Registry Manifest schema and rendered artifacts | `registry-manifest/v1` and the rendered artifact schema versions, governed by [RS-DM-MANIFEST](../../spec/rs-dm-manifest/) | `validate_manifest` accepts only `registry-manifest/v1`; REQ-DM-MANIFEST-013 requires strict unknown-key rejection at parse time |
| Command-line interfaces | Documented commands and flags of `relay`, `evidence`, and `registry-manifest`, and their machine-readable output modes | CLI reference pages; the `evidence` command surface is stated in `products/evidence/OPERATOR-CONTRACT.md` and exercised end to end against the built binary in `crates/registry-evidence/tests/cli.rs`; the release candidate workflow asserts each built binary reports the release version |
| Release artifacts and verification interface | Released binary asset names, the released `ghcr.io/registrystack/relay`, `ghcr.io/registrystack/evidence`, and `ghcr.io/registrystack/mint` container images, and the signature and provenance layout in `release/VERIFY.md` | Release workflow; signed assets and exact image digests verified as documented in [SECURITY.md](https://github.com/registrystack/registry-stack/blob/v0.21.0/SECURITY.md) |
| Release artifacts and verification interface | Released binary asset names, the released `ghcr.io/registrystack/relay`, `ghcr.io/registrystack/evidence`, and `ghcr.io/registrystack/mint` container images, joined by `ghcr.io/registrystack/discovery` from `v0.24.0`, and the signature and provenance layout in `release/VERIFY.md` | Release workflow; signed assets and exact image digests verified as documented in [SECURITY.md](https://github.com/registrystack/registry-stack/blob/v0.21.0/SECURITY.md) |
Comment thread
jeremi marked this conversation as resolved.

Relay's covered HTTP surface is its route inventory, not the content of any one deployment's
OpenAPI document. Adding a resource, an operation, or a statistical dataset to a Registry contract
Expand Down
32 changes: 27 additions & 5 deletions release/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ container package as private, so publish a clearly non-release bootstrap
artifact without putting a token on the command line:

```sh
package="${PACKAGE:?set PACKAGE to relay, evidence, or mint}"
package="${PACKAGE:?set PACKAGE to relay, evidence, mint, or discovery}"
case "${package}" in
relay|evidence|mint) ;;
relay|evidence|mint|discovery) ;;
*) echo "unsupported release image package: ${package}" >&2; exit 1 ;;
esac

Expand All @@ -58,10 +58,11 @@ oras push \
In the organization package settings, change only the selected package to
public and grant `registrystack/registry-stack` Actions access with Write.
Starting with `v0.21.0`, the release requires public `relay`, `evidence`, and
`mint` packages. Verify all three before candidate dispatch:
`mint` packages, joined by `discovery` from `v0.24.0`. Verify all four before
candidate dispatch:

```sh
for package in relay evidence mint; do
for package in relay evidence mint discovery; do
gh api "/orgs/registrystack/packages/container/${package}" \
--jq '[.name,.package_type,.visibility]'
done
Expand All @@ -72,6 +73,27 @@ Each result must name the requested package and report `container` and
then remove only that bootstrap version. This is a package-identity setup step,
not part of later releases.

A new release image also needs its own reviewed advisory baseline at
`release/security/<name>-advisory-baseline.json` before its first candidate.
The candidate refuses to run without that file, and the pinned Debian 13
runtime carries findings that a baseline with no exception cannot clear. Author
it from a real candidate image, never by copying another service's file: run
the candidate once to publish the private candidate image, regenerate the
scanner and rootfs evidence with the procedure in "Renew an image advisory
fingerprint" below, and record the reviewed runtime block, exception set,
owner, and expiry from that evidence. `discovery` is the first image to need
this since the baselines were introduced.

Enrol the new candidate package in the daily cleanup only after that first
candidate publishes `ghcr.io/registrystack/<name>-candidate`. The cleanup lists
exactly the names in `CANDIDATE_PACKAGES` in
`release/scripts/cleanup-release-candidates.py` and fails closed on a package it
cannot list, so naming an unpublished package would abort the whole scheduled
run. Add `discovery-candidate` to that allowlist, with a matching fixture in
`release/scripts/test_cleanup_release_candidates.py`, as part of the `v0.24.0`
release. The public `discovery` name is already on the `PUBLIC_PACKAGES`
denylist, so cleanup can never reach a released image.

### Provision client registries

Registry Stack v0.22.0 promotes the exact candidate Evidence and Relay client
Expand Down Expand Up @@ -275,7 +297,7 @@ evidence with the scanner versions pinned in the candidate workflow:
```sh
run_id=<failed-run-id>
run_attempt=<failed-run-attempt>
name=relay # or evidence or mint
name=relay # or evidence, mint, or discovery
candidate_tag="ghcr.io/registrystack/${name}-candidate:candidate-${run_id}-${run_attempt}"
digest="$(crane digest "${candidate_tag}")"
candidate_ref="ghcr.io/registrystack/${name}-candidate@${digest}"
Expand Down
17 changes: 10 additions & 7 deletions release/VERIFY.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ Inspect the compact public release manifest:
manifest="registry-stack-${tag}-release-manifest.json"

jq -e --arg tag "${tag}" '
(if ($tag | test("^v0\\.(19|20)\\."))
then ["relay"] else ["evidence", "mint", "relay"] end) as $image_names |
(if ($tag | test("^v0\\.(19|20)\\.")) then ["relay"]
elif ($tag | test("^v0\\.(21|22|23)\\.")) then ["evidence", "mint", "relay"]
else ["discovery", "evidence", "mint", "relay"] end) as $image_names |
.schema_version == "registry-stack.release-candidate.v2" and
.repository == "registrystack/registry-stack" and
.release.tag == $tag and
Expand All @@ -91,13 +92,14 @@ jq -e --arg tag "${tag}" '
```

Starting with `v0.21.0`, the exact image set is Evidence Gateway, Registry
Mint, and Registry Relay. The final release tags recorded in the manifest
must resolve to the same digests as their candidate bindings:
Mint, and Registry Relay; starting with `v0.24.0`, Registry Discovery joins
them. The final release tags recorded in the manifest must resolve to the same
digests as their candidate bindings:

```sh
while IFS=$'\t' read -r name digest final_ref; do
case "${name}" in
evidence|mint|relay) ;;
discovery|evidence|mint|relay) ;;
*) echo "unexpected release image: ${name}" >&2; exit 1 ;;
esac
test "$(crane digest "${final_ref}")" = "${digest}"
Expand Down Expand Up @@ -127,8 +129,9 @@ tar -tzf "${evidence}"
```

Starting with `v0.21.0`, the archive contains image-specific SPDX and Syft
reports and Grype reports for `evidence`, `mint`, and `relay`; `v0.19.x` and
`v0.20.x` archives contain those reports for `relay` only. The archive also
reports and Grype reports for `evidence`, `mint`, and `relay`, joined by
`discovery` from `v0.24.0`; `v0.19.x` and `v0.20.x` archives contain those
reports for `relay` only. The archive also
contains the advisory verdict used for candidate acceptance. Each report names
the exact candidate digest that was promoted. The archive hash is covered by
the authenticated checksum chain. The candidate workflow also evaluates any
Expand Down
37 changes: 37 additions & 0 deletions release/docker/Dockerfile.discovery
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e

ARG SOURCE_DATE_EPOCH=0

FROM debian:trixie-slim@sha256:3a39a0592364683e6bab97937b72cad5a8fa6dcbbee90edb3bb48c7f8e94f258 AS runtime-root
ARG SOURCE_DATE_EPOCH

# Discovery serves one immutable index and writes nothing, so the image
# pre-owns no managed state or audit directory. The runtime file and the index
# it names are startup-only deployment artifacts mounted read-only together
# under /etc/registry-discovery: the index path is resolved relative to the
# runtime file's own directory.
RUN --mount=type=bind,source=dist/image-bin,target=/workspace/image-bin \
--mount=type=bind,source=LICENSE,target=/workspace/LICENSE \
mkdir -p \
/workspace/runtime-root/etc/registry-discovery \
/workspace/runtime-root/licenses/discovery \
/workspace/runtime-root/usr/local/bin \
&& install -m 0755 /workspace/image-bin/discovery /workspace/runtime-root/usr/local/bin/discovery \
&& install -m 0644 /workspace/LICENSE /workspace/runtime-root/licenses/discovery/LICENSE \
&& find /workspace/runtime-root -exec touch -h --date="@${SOURCE_DATE_EPOCH}" {} +

FROM gcr.io/distroless/cc-debian13:nonroot@sha256:d97bc0a941b8d4be647dc0ee75b264ddbb772f1ac5ba690a4309c00723b23775 AS runtime

LABEL org.registrystack.runtime.uid="65532" \
org.registrystack.runtime.gid="65532"

COPY --from=runtime-root /workspace/runtime-root/ /

EXPOSE 8080

# Discovery serves GET /health for the platform's HTTP probe. The Distroless
# image has no shell or HTTP client, and Discovery has no healthcheck
# subcommand. It also reads no environment variable, so the command is what
# binds the runtime file.
ENTRYPOINT ["/usr/local/bin/discovery"]
CMD ["--runtime", "/etc/registry-discovery/runtime.yaml"]
70 changes: 44 additions & 26 deletions release/scripts/build-release-binaries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ fi

version="$1"
tag="v${version}"
# The Discovery binary joins the release payload at 0.24.0. A candidate rebuilt
# for an earlier version must stage exactly the assets its recorded inventory
# names, so seal-candidate keeps accepting it.
IFS=. read -r version_major version_minor _version_patch <<<"${version}"
include_discovery=0
if ((version_major > 0 || version_minor >= 24)); then
include_discovery=1
fi
default_builder_image="rust:1.95-trixie@sha256:f49565f188ee00bc2a18dd418183f2c5f23ef7d6e691890517ed341a598f67c3"
if [[ -n "${RELEASE_BUILDER_IMAGE:-}" && "${RELEASE_BUILDER_IMAGE}" != "${default_builder_image}" ]]; then
printf 'RELEASE_BUILDER_IMAGE must remain pinned to %s\n' "${default_builder_image}" >&2
Expand Down Expand Up @@ -48,6 +56,7 @@ docker run --rm \
--env CARGO_INCREMENTAL=0 \
--env CARGO_TERM_COLOR="${CARGO_TERM_COLOR:-always}" \
--env HOME=/workspace \
--env RELEASE_INCLUDE_DISCOVERY="${include_discovery}" \
--env RELEASE_TAG="${tag}" \
--env REGISTRY_RELEASE_TAG="${tag}" \
--env RELEASE_RUSTFLAGS="${release_rustflags}" \
Expand Down Expand Up @@ -83,41 +92,50 @@ docker run --rm \
cp target/release/evidence-oid4vci "dist/bin/evidence-oid4vci-${RELEASE_TAG}-linux-amd64"
cp target/release/evidence dist/image-bin/evidence
cp target/release/mint dist/image-bin/mint

if [[ "${RELEASE_INCLUDE_DISCOVERY}" -eq 1 ]]; then
cargo build --release --locked \
-p registry-discovery \
--bin discovery
cp target/release/discovery "dist/bin/discovery-${RELEASE_TAG}-linux-amd64"
cp target/release/discovery dist/image-bin/discovery
fi
'

printf '%s\n' "${release_builder_image}" > "${repo_root}/dist/image-bin/RELEASE_BUILDER_IMAGE"
chmod 0755 \
"${repo_root}/dist/bin/registry-manifest-${tag}-linux-amd64" \
"${repo_root}/dist/bin/relay-${tag}-linux-amd64" \
"${repo_root}/dist/bin/relayctl-${tag}-linux-amd64" \
"${repo_root}/dist/bin/evidence-${tag}-linux-amd64" \
"${repo_root}/dist/bin/evidencectl-${tag}-linux-amd64" \
"${repo_root}/dist/bin/mint-${tag}-linux-amd64" \
"${repo_root}/dist/bin/evidence-oid4vci-${tag}-linux-amd64" \
"${repo_root}/dist/image-bin/evidence" \
"${repo_root}/dist/image-bin/mint" \
"${repo_root}/dist/image-bin/relay"
# The staged asset lists follow the same gate as the build above, so a version
# that predates an asset neither checksums nor chmods a file it never built.
bin_assets=()
image_bin_binaries=()
if [[ "${include_discovery}" -eq 1 ]]; then
bin_assets+=("discovery-${tag}-linux-amd64")
image_bin_binaries+=(discovery)
fi
bin_assets+=(
"evidence-${tag}-linux-amd64"
"evidencectl-${tag}-linux-amd64"
"mint-${tag}-linux-amd64"
"evidence-oid4vci-${tag}-linux-amd64"
"registry-manifest-${tag}-linux-amd64"
"relay-${tag}-linux-amd64"
"relayctl-${tag}-linux-amd64"
)
image_bin_binaries+=(evidence mint relay)

for asset in "${bin_assets[@]}"; do
chmod 0755 "${repo_root}/dist/bin/${asset}"
done
for asset in "${image_bin_binaries[@]}"; do
chmod 0755 "${repo_root}/dist/image-bin/${asset}"
done

(
cd -- "${repo_root}/dist/bin"
sha256sum -- \
"evidence-${tag}-linux-amd64" \
"evidencectl-${tag}-linux-amd64" \
"mint-${tag}-linux-amd64" \
"evidence-oid4vci-${tag}-linux-amd64" \
"registry-manifest-${tag}-linux-amd64" \
"relay-${tag}-linux-amd64" \
"relayctl-${tag}-linux-amd64" \
> SHA256SUMS
sha256sum -- "${bin_assets[@]}" > SHA256SUMS
)
(
cd -- "${repo_root}/dist/image-bin"
sha256sum -- \
RELEASE_BUILDER_IMAGE \
evidence \
mint \
relay \
> SHA256SUMS
sha256sum -- RELEASE_BUILDER_IMAGE "${image_bin_binaries[@]}" > SHA256SUMS
)

printf 'built release binaries for %s with canonical container paths\n' "${tag}"
2 changes: 1 addition & 1 deletion release/scripts/build-release-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ release_image_context="${RELEASE_IMAGE_CONTEXT:-${repo_root}}"
created_builder=false

case "${name}" in
evidence|mint|relay)
discovery|evidence|mint|relay)
dockerfile="${repo_root}/release/docker/Dockerfile.${name}"
;;
*)
Expand Down
Loading