diff --git a/.github/workflows/validate-agentic-docs.yml b/.github/workflows/validate-agentic-docs.yml new file mode 100644 index 0000000000..25be3529db --- /dev/null +++ b/.github/workflows/validate-agentic-docs.yml @@ -0,0 +1,274 @@ +name: Validate Agentic Documentation + +on: + pull_request: + paths: + - 'agentic/**' + - '*.md' + - '.github/workflows/validate-agentic-docs.yml' + push: + branches: + - main + +jobs: + structure: + name: Validate Structure + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check AGENTS.md length + run: | + lines=$(wc -l < AGENTS.md) + echo "AGENTS.md has $lines lines" + if [ "$lines" -gt 150 ]; then + echo "AGENTS.md too long ($lines lines). Keep under 150." + exit 1 + fi + echo "AGENTS.md length OK" + + - name: Verify directory structure + run: | + required_dirs="design-docs domain exec-plans decisions references generated" + for dir in $required_dirs; do + if [ ! -d "agentic/$dir" ]; then + echo "Missing required directory: agentic/$dir" + exit 1 + fi + done + echo "Directory structure OK" + + - name: Check required files exist + run: | + required_files="design-docs/index.md domain/index.md decisions/index.md DESIGN.md DEVELOPMENT.md TESTING.md SECURITY.md" + for file in $required_files; do + if [ ! -f "agentic/$file" ]; then + echo "Missing required file: agentic/$file" + exit 1 + fi + done + echo "Required files OK" + + frontmatter: + name: Validate Frontmatter + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check exec-plan frontmatter + run: | + for file in agentic/exec-plans/active/*.md agentic/exec-plans/completed/*.md; do + if [ -f "$file" ]; then + if ! head -n 1 "$file" | grep -q "^---$"; then + echo "$file missing YAML frontmatter" + exit 1 + fi + fi + done + echo "Exec-plan frontmatter OK" + + - name: Check ADR frontmatter + run: | + for file in agentic/decisions/adr-*.md; do + if [ -f "$file" ] && [ "$(basename "$file")" != "adr-template.md" ]; then + if ! head -n 1 "$file" | grep -q "^---$"; then + echo "$file missing YAML frontmatter" + exit 1 + fi + fi + done + echo "ADR frontmatter OK" + + links: + name: Validate Links + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check for broken internal links + run: | + broken=0 + while IFS= read -r file; do + # Extract relative markdown links + grep -oP '\[.*?\]\(\./[^)]+\)' "$file" 2>/dev/null | grep -oP '\(\K[^)]+' | while read -r link; do + # Remove anchor + path="${link%%#*}" + # Resolve relative to the file's directory + dir=$(dirname "$file") + resolved="$dir/$path" + if [ ! -f "$resolved" ] && [ ! -d "$resolved" ]; then + echo "Broken link in $file: $link (resolved to $resolved)" + broken=$((broken + 1)) + fi + done + done < <(find agentic -name "*.md" -type f) + + # Also check root md files + for file in AGENTS.md ARCHITECTURE.md; do + if [ -f "$file" ]; then + grep -oP '\[.*?\]\(\./[^)]+\)' "$file" 2>/dev/null | grep -oP '\(\K[^)]+' | while read -r link; do + path="${link%%#*}" + if [ ! -f "$path" ] && [ ! -d "$path" ]; then + echo "Broken link in $file: $link" + fi + done + fi + done + echo "Link check complete" + + freshness: + name: Check Freshness + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for stale TODOs + run: | + stale_count=0 + while IFS= read -r file; do + last_modified=$(git log -1 --format=%ct "$file" 2>/dev/null || echo 0) + now=$(date +%s) + days=$(( (now - last_modified) / 86400 )) + + if [ "$days" -gt 30 ] && grep -q "TODO" "$file"; then + echo "WARNING: $file has TODO and hasn't been updated in $days days" + stale_count=$((stale_count + 1)) + fi + done < <(find agentic -name "*.md" -type f) + + if [ "$stale_count" -gt 5 ]; then + echo "Too many stale TODOs ($stale_count). Update or move to tech-debt-tracker.md" + exit 1 + fi + echo "TODO freshness OK ($stale_count stale)" + + - name: Check for stale exec-plans + run: | + stale_plans=0 + while IFS= read -r file; do + last_modified=$(git log -1 --format=%ct "$file" 2>/dev/null || echo 0) + now=$(date +%s) + days=$(( (now - last_modified) / 86400 )) + + if [ "$days" -gt 90 ]; then + echo "WARNING: Active exec-plan $file hasn't been updated in $days days -- consider completing or abandoning" + stale_plans=$((stale_plans + 1)) + fi + done < <(find agentic/exec-plans/active -name "*.md" -type f 2>/dev/null) + + if [ "$stale_plans" -gt 3 ]; then + echo "Too many stale active exec-plans ($stale_plans). Clean up agentic/exec-plans/active/" + exit 1 + fi + echo "Exec-plan freshness OK ($stale_plans stale)" + + - name: Validate code paths referenced in docs + run: | + broken=0 + while IFS= read -r doc; do + # Extract backtick-quoted file paths that look like Python source files + grep -oP '`(?:artcommon/artcommonlib|doozer/doozerlib|elliott/elliottlib|pyartcd/pyartcd|ocp-build-data-validator/validator)/[a-zA-Z0-9_/]+\.py`' "$doc" 2>/dev/null | tr -d '`' | sort -u | while read -r path; do + if [ ! -f "$path" ]; then + echo "BROKEN PATH in $doc: $path does not exist" + broken=$((broken + 1)) + fi + done + done < <(find agentic AGENTS.md ARCHITECTURE.md -name "*.md" -type f 2>/dev/null) + + if [ "$broken" -gt 0 ]; then + echo "Found references to non-existent source files. Update the docs." + exit 1 + fi + echo "Code path validation OK" + + - name: Check doc staleness relative to code + run: | + warn_count=0 + + # Map: concept doc -> source files it documents + check_staleness() { + local doc="$1" + shift + local sources=("$@") + + if [ ! -f "$doc" ]; then + return + fi + + doc_modified=$(git log -1 --format=%ct "$doc" 2>/dev/null || echo 0) + + for src in "${sources[@]}"; do + if [ ! -f "$src" ]; then + continue + fi + src_modified=$(git log -1 --format=%ct "$src" 2>/dev/null || echo 0) + if [ "$src_modified" -eq 0 ] || [ "$doc_modified" -eq 0 ]; then + continue + fi + + src_days_ago=$(( ($(date +%s) - src_modified) / 86400 )) + doc_days_ago=$(( ($(date +%s) - doc_modified) / 86400 )) + drift=$(( doc_days_ago - src_days_ago )) + + # Warn if code changed recently (last 30 days) but doc is 60+ days stale + if [ "$src_days_ago" -lt 30 ] && [ "$doc_days_ago" -gt 60 ]; then + echo "WARNING: $src changed ${src_days_ago}d ago but $doc last updated ${doc_days_ago}d ago (drift: ${drift}d)" + warn_count=$((warn_count + 1)) + fi + done + } + + # Concept docs and their primary source files + check_staleness agentic/domain/concepts/runtime.md \ + artcommon/artcommonlib/runtime.py doozer/doozerlib/runtime.py elliott/elliottlib/runtime.py + + check_staleness agentic/domain/concepts/assembly.md \ + artcommon/artcommonlib/assembly.py + + check_staleness agentic/domain/concepts/metadata.md \ + artcommon/artcommonlib/metadata.py doozer/doozerlib/image.py doozer/doozerlib/rpmcfg.py + + check_staleness agentic/domain/concepts/brew-koji.md \ + doozer/doozerlib/brew.py elliott/elliottlib/brew.py + + check_staleness agentic/domain/concepts/distgit.md \ + doozer/doozerlib/distgit.py + + check_staleness agentic/domain/concepts/errata-advisories.md \ + elliott/elliottlib/errata.py elliott/elliottlib/errata_async.py + + check_staleness agentic/domain/concepts/konflux.md \ + doozer/doozerlib/backend/konflux_client.py artcommon/artcommonlib/konflux/konflux_db.py + + check_staleness agentic/domain/concepts/plashet.md \ + doozer/doozerlib/plashet.py + + check_staleness agentic/domain/concepts/model-missing.md \ + artcommon/artcommonlib/model.py + + check_staleness agentic/domain/concepts/ocp-build-data.md \ + artcommon/artcommonlib/gitdata.py + + # Component docs and their entry points + check_staleness agentic/design-docs/components/doozer.md \ + doozer/doozerlib/cli/__main__.py doozer/doozerlib/runtime.py + + check_staleness agentic/design-docs/components/elliott.md \ + elliott/elliottlib/cli/__main__.py elliott/elliottlib/runtime.py + + check_staleness agentic/design-docs/components/pyartcd.md \ + pyartcd/pyartcd/__main__.py + + check_staleness agentic/design-docs/components/artcommon.md \ + artcommon/artcommonlib/runtime.py artcommon/artcommonlib/assembly.py + + check_staleness agentic/design-docs/components/validator.md \ + ocp-build-data-validator/validator/__main__.py + + if [ "$warn_count" -gt 5 ]; then + echo "Too many stale docs ($warn_count). Code has changed significantly -- update the affected docs." + exit 1 + fi + echo "Doc staleness check OK ($warn_count warnings)" diff --git a/.gitignore b/.gitignore index b5233cce63..422251deeb 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,7 @@ sjb/generated/*.sh # git mergetool backup files *.orig + +# Generated metrics dashboard (regenerated on demand) +agentic/metrics-dashboard.html +agentic/METRICS_REPORT.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..bddbe4bf52 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,141 @@ +# art-tools - Agent Navigation + +CLI tools for managing OpenShift Container Platform (OCP) releases: builds, advisories, and pipelines. + +## What This Repository Does + +A Python 3.11 monorepo of five packages that automate OCP release engineering -- building images/RPMs, managing errata advisories, and orchestrating release pipelines. + +## Quick Navigation by Intent + +| I want to... | Go to | +|----------------------------------|----------------------------------------------------| +| Understand the system | [ARCHITECTURE.md](./ARCHITECTURE.md) | +| Build/rebase images or RPMs | [agentic/design-docs/components/doozer.md](./agentic/design-docs/components/doozer.md) | +| Manage advisories/errata | [agentic/design-docs/components/elliott.md](./agentic/design-docs/components/elliott.md) | +| Understand a pipeline | [agentic/design-docs/components/pyartcd.md](./agentic/design-docs/components/pyartcd.md) | +| Understand shared utilities | [agentic/design-docs/components/artcommon.md](./agentic/design-docs/components/artcommon.md) | +| Understand a domain concept | [agentic/domain/glossary.md](./agentic/domain/glossary.md) | +| Set up dev environment | [CLAUDE.md](./CLAUDE.md) | +| Implement a feature | Create exec-plan in `agentic/exec-plans/active/` first, read TESTING.md | +| Fix a bug | [ARCHITECTURE.md#components](./ARCHITECTURE.md#components), [agentic/DEVELOPMENT.md](./agentic/DEVELOPMENT.md) | +| Find a past decision | [agentic/decisions/index.md](./agentic/decisions/index.md) | + +## Repository Structure + +``` +art-tools/ + artcommon/artcommonlib/ # Shared library (assembly, model, exectools, konflux) + doozer/doozerlib/ # Build management CLI (images, RPMs, distgit) + elliott/elliottlib/ # Advisory/errata management CLI + pyartcd/pyartcd/ # Release pipeline orchestration + ocp-build-data-validator/ # Schema validator for ocp-build-data + agentic/ # Agent documentation and exec-plans + hack/ # Developer utility scripts + Makefile # Build/test/lint targets + pyproject.toml # Root project configuration +``` + +## Component Boundaries + +``` +pyartcd (orchestrates) + | | + v v +doozer elliott + | | + v v +artcommon (shared) + +External: ocp-build-data, Brew/Koji, Errata Tool, Konflux, Jira, Slack +``` + +## Core Concepts + +| Concept | Definition | Docs | +|---------------|------------------------------------------------------------------|-----------------------------------| +| Group | An OCP version target (e.g. `openshift-4.17`) | [ARCHITECTURE.md](./ARCHITECTURE.md#runtime-initialization-flow) | +| Assembly | A named release or checkpoint (STREAM, STANDARD, CANDIDATE, CUSTOM, PREVIEW) | `artcommonlib/assembly.py` | +| Runtime | Central orchestration object initialized per group | `doozerlib/runtime.py`, `elliottlib/runtime.py` | +| Metadata | ImageMetadata or RPMMetadata wrapping a build component | `doozerlib/metadata.py` | +| Model/Missing | Safe YAML config traversal; Missing singleton prevents KeyError | `artcommonlib/model.py` | +| Distgit | Internal source repositories managed by doozer | `doozerlib/distgit.py` | +| Plashet | RPM repo composition from Brew tags | `doozerlib/plashet.py` | +| Advisory | Errata advisory for shipping fixes | `elliottlib/errata.py` | +| Konflux | New build system replacing OSBS/Brew | `doozerlib/backend/` | +| ocp-build-data| External YAML config repo defining groups, images, RPMs | `artcommonlib/gitdata.py` | + +## Components + +| Component | Entry Point | Purpose | Docs | +|-------------|--------------------------------------|--------------------------------------|-----------------------------------| +| doozer | `doozerlib/cli/__main__.py` | Build management (images, RPMs) | [doozer.md](./agentic/design-docs/components/doozer.md) | +| elliott | `elliottlib/cli/__main__.py` | Advisory and errata management | [elliott.md](./agentic/design-docs/components/elliott.md) | +| pyartcd | `pyartcd/__main__.py` | Pipeline orchestration | [pyartcd.md](./agentic/design-docs/components/pyartcd.md) | +| artcommon | (library) | Shared utilities and abstractions | [artcommon.md](./agentic/design-docs/components/artcommon.md) | +| validator | `validator/__main__.py` | ocp-build-data schema validation | `ocp-build-data-validator/` | + +## Key Invariants + +1. **doozerlib and elliottlib are peers.** Cross-imports exist (~12 files) but are tech debt. Both import from artcommonlib. +2. **pyartcd MAY import from doozerlib and elliottlib.** It orchestrates both. +3. **Runtime must be initialized before use** -- `Runtime.initialize()` clones ocp-build-data, loads group config, creates Metadata objects. +4. **ocp-build-data-validator is standalone** -- no imports from other packages. +5. **Never push to `origin` remote** -- always push to `dev` remote. PRs against `main`. + +## Critical Code Locations + +| What | File | +|-----------------------------|-----------------------------------------------| +| Doozer Runtime | `doozer/doozerlib/runtime.py` | +| Elliott Runtime | `elliott/elliottlib/runtime.py` | +| GroupRuntime base class | `artcommon/artcommonlib/runtime.py` | +| Assembly system | `artcommon/artcommonlib/assembly.py` | +| Model/Missing | `artcommon/artcommonlib/model.py` | +| Distgit management | `doozer/doozerlib/distgit.py` | +| Errata API | `elliott/elliottlib/errata.py` | +| Konflux build client | `doozer/doozerlib/backend/konflux_client.py` | + +## External Dependencies + +| System | Purpose | Auth | +|-------------|----------------------------|------------| +| Brew/Koji | Build system | Kerberos | +| Errata Tool | Advisory management | Kerberos | +| Bugzilla | Bug tracking | API key | +| Jira | Issue tracking | API token | +| Konflux | New build system | Kubernetes | +| Jenkins | Pipeline triggers | API token | +| Slack | Notifications | Bot token | +| GitHub | Source repos | OAuth | +| Quay.io | Container registry | Registry | +| BigQuery | Analytics | Service account | +| Redis | Caching/locking | Connection | + +## Build and Test + +```bash +make venv # Create venv and install all packages +make format # Auto-format with ruff +make lint # Lint checks +make unit # Run all unit tests +make test # lint + unit tests +uv run pytest --verbose --color=yes doozer/tests/ # Package-specific tests +``` + +## Documentation Structure + +``` +agentic/ + design-docs/components/ # Per-component deep dives + domain/ # Domain glossary and concepts + exec-plans/ # Feature execution plans + decisions/ # Architecture decision records + references/ # Reference material +``` + +## When You're Stuck + +- Start with [ARCHITECTURE.md](./ARCHITECTURE.md) for system-wide understanding. +- Check [CLAUDE.md](./CLAUDE.md) for dev setup, commands, and working conventions. +- Search `agentic/decisions/` for prior architectural decisions. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000000..97ce56f386 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,149 @@ +# Architecture Overview + +art-tools is a Python 3.11 monorepo of CLI tools for managing OpenShift Container Platform (OCP) releases. It automates building container images and RPMs, managing errata advisories, and orchestrating multi-step release pipelines. + +## System Context + +| External System | Direction | Interface | Key Files | +|-------------------|-----------|--------------------|-------------------------------------------------------------------| +| ocp-build-data | Inbound | Git clone | `artcommon/artcommonlib/gitdata.py` | +| Brew/Koji | In/Out | Koji API | `doozer/doozerlib/brew.py`, `elliott/elliottlib/brew.py` | +| Errata Tool | In/Out | REST API | `elliott/elliottlib/errata.py`, `elliott/elliottlib/errata_async.py` | +| Bugzilla | In/Out | REST API | `elliott/elliottlib/bzutil.py` | +| Jira | In/Out | REST API | `pyartcd/pyartcd/jira_client.py` | +| Konflux | In/Out | Kubernetes API | `doozer/doozerlib/backend/konflux_client.py`, `artcommon/artcommonlib/konflux/` | +| Jenkins | Trigger | REST API | `pyartcd/pyartcd/jenkins.py` | +| Slack | Out | Bot API | `pyartcd/pyartcd/slack.py` | +| UMB | In/Out | STOMP | `pyartcd/pyartcd/umb_client.py` | +| GitHub | In/Out | REST + Git | `artcommon/artcommonlib/github_auth.py` | +| Quay.io | Out | Registry API | `pyartcd/pyartcd/pipelines/promote.py` | +| BigQuery | Out | Client API | `artcommon/artcommonlib/bigquery.py` | +| Redis | In/Out | Client API | `artcommon/artcommonlib/redis.py` | + +## Package Layering + +``` + +----------+ + | pyartcd | (orchestrates pipelines, may import doozerlib + elliottlib) + +----+-----+ + | + +-----------+-----------+ + | | + +----v----+ +------v---+ + | doozer | | elliott | (peer CLI tools, MUST NOT import each other) + +----+----+ +-----+----+ + | | + +----------+-----------+ + | + +-----v------+ + | artcommon | (shared library) + +------------+ + + +----------------------------+ + | ocp-build-data-validator | (standalone, no cross-imports) + +----------------------------+ +``` + +## Dependency Rules + +1. **doozerlib and elliottlib are peers.** The intended design is no cross-imports, but in practice some CLI modules import from each other for Konflux, shipment, and plashet functionality. These cross-imports exist in ~12 files and represent tech debt. +2. **Both doozerlib and elliottlib import shared code from artcommonlib.** +3. **pyartcd MAY import from doozerlib, elliottlib, and artcommonlib.** It is the orchestration layer. +4. **ocp-build-data-validator is standalone.** It does not import from any other package in this repo. +5. **artcommonlib is the base layer.** It has a small number of imports from doozerlib (2 files) which are tech debt to be resolved. + +## Components + +| Component | Entry Point | Critical Code | Purpose | Details | +|-------------|-----------------------------------------------|-------------------------------------------------------|---------------------------------------------|----------------------------------| +| doozer | `doozer/doozerlib/cli/__main__.py` | `runtime.py`, `distgit.py`, `image.py`, `backend/` | Build/rebase images and RPMs via Brew/Konflux | [doozer.md](./agentic/design-docs/components/doozer.md) | +| elliott | `elliott/elliottlib/cli/__main__.py` | `runtime.py`, `errata.py`, `bzutil.py`, `brew.py` | Manage errata advisories and bugs | [elliott.md](./agentic/design-docs/components/elliott.md) | +| pyartcd | `pyartcd/pyartcd/__main__.py` | `pipelines/ocp.py`, `pipelines/promote.py` | Orchestrate release pipelines | [pyartcd.md](./agentic/design-docs/components/pyartcd.md) | +| artcommon | (library, no CLI) | `assembly.py`, `model.py`, `runtime.py`, `gitdata.py` | Shared utilities, config, abstractions | [artcommon.md](./agentic/design-docs/components/artcommon.md) | +| validator | `ocp-build-data-validator/validator/__main__.py` | `schema/`, `releases.py` | Validate ocp-build-data YAML schemas | `ocp-build-data-validator/` | + +## Data Flow + +``` +ocp-build-data (YAML configs: group.yml, images/*.yml, rpms/*.yml, releases.yml) + | + v +Runtime.initialize(group="openshift-4.17", assembly="stream") + | + +-- Clones ocp-build-data repo + +-- Loads group.yml configuration + +-- Resolves assembly definition from releases.yml + | + v +Metadata objects (ImageMetadata, RPMMetadata) + | + +-- Each metadata object wraps one component's YAML config + +-- Source resolver locates upstream source repos + | + v +Build commands (images:build, images:rebase, rpms:build) + | + +-- Doozer: distgit operations, Dockerfile generation, build submission + +-- Backend: Brew/OSBS or Konflux pipeline runs + | + v +Build results (Brew NVRs, Konflux build records) + | + v +Advisory/Payload (Elliott attaches builds to advisories, pyartcd promotes to payload) +``` + +## Runtime Initialization Flow + +When `doozer --group openshift-4.17 images:build` is executed: + +1. **CLI parsing**: Click parses `--group openshift-4.17` and other global options. The `Runtime` object is instantiated with these parameters. + +2. **`Runtime.initialize()` called**: This is the core setup method in `doozer/doozerlib/runtime.py` (inherits from `GroupRuntime` in `artcommon/artcommonlib/runtime.py`). + +3. **Working directory setup**: Creates or uses `--working-dir` for temporary files, logs, and cloned repos. + +4. **ocp-build-data clone**: Clones the ocp-build-data Git repository (default branch derived from group name, e.g. `openshift-4.17`). The data path can be overridden with `--data-path`. + +5. **Group config loading**: Reads `group.yml` from cloned ocp-build-data. This defines default settings: Brew tags, repos, arches, freeze state, etc. Loaded as a `Model` object. + +6. **Assembly resolution**: If `--assembly` is provided (defaults to `stream`), loads the assembly definition from `releases.yml`. The assembly type (STREAM, STANDARD, CANDIDATE, CUSTOM, PREVIEW) determines constraint enforcement. `assembly_basis_event()` determines the Brew event that pins build state. + +7. **Metadata creation**: For each YAML file in `images/` and `rpms/` directories of ocp-build-data (filtered by `--images`/`--rpms` CLI options), creates `ImageMetadata` or `RPMMetadata` objects. Each wraps the component's config, provides distgit operations, and tracks build state. + +8. **Brew session**: A shared Koji client session is established (`Runtime.shared_koji_client_session()`), optionally constrained to a point-in-time via `brew_event`. + +9. **Command dispatch**: The specific subcommand (e.g. `images:build`) receives the fully initialized Runtime and operates on the loaded metadata objects. + +## Critical Code Locations + +| Function / Concern | File | Why Critical | +|-------------------------------------|-------------------------------------------------------|-----------------------------------------------------------------| +| Doozer Runtime initialization | `doozer/doozerlib/runtime.py` | Bootstraps all doozer operations; clones data, loads metadata | +| Elliott Runtime initialization | `elliott/elliottlib/runtime.py` | Bootstraps all elliott operations | +| GroupRuntime base class | `artcommon/artcommonlib/runtime.py` | Shared initialization logic (logging, Konflux DB) | +| Assembly type resolution | `artcommon/artcommonlib/assembly.py` | Determines constraint enforcement for releases | +| Model / Missing config traversal | `artcommon/artcommonlib/model.py` | Every YAML config access goes through Model | +| Distgit management | `doozer/doozerlib/distgit.py` | 3000+ lines; core of image/RPM source management | +| Image build logic | `doozer/doozerlib/image.py` | ImageMetadata class; build, rebase, config resolution | +| Konflux build client | `doozer/doozerlib/backend/konflux_client.py` | Interface to new Konflux build system | +| Errata Tool API | `elliott/elliottlib/errata.py` | Creates/modifies advisories; core elliott functionality | +| Errata async operations | `elliott/elliottlib/errata_async.py` | Async advisory operations for performance | +| Bug tracking integration | `elliott/elliottlib/bzutil.py` | Bugzilla and Jira bug queries and attachment | +| OCP build pipeline | `pyartcd/pyartcd/pipelines/ocp.py` | Main build pipeline; orchestrates doozer + elliott | +| OCP4 Konflux pipeline | `pyartcd/pyartcd/pipelines/ocp4_konflux.py` | Konflux variant of main build pipeline | +| Prepare release pipeline | `pyartcd/pyartcd/pipelines/prepare_release_konflux.py`| Sets up advisory, bugs, and payload for a release | +| Promote pipeline | `pyartcd/pyartcd/pipelines/promote.py` | Promotes builds to release payload and mirrors | +| Plashet repo composition | `doozer/doozerlib/plashet.py` | Composes RPM repositories from Brew tags | +| Source resolver | `doozer/doozerlib/source_resolver.py` | Locates and clones upstream source repos for builds | +| Git data loading | `artcommon/artcommonlib/gitdata.py` | Loads YAML metadata files from ocp-build-data | +| Exec tools | `artcommon/artcommonlib/exectools.py` | Subprocess execution utilities used everywhere | +| Konflux DB integration | `artcommon/artcommonlib/konflux/konflux_db.py` | Build record storage for Konflux builds | + +## Related Documentation + +- [AGENTS.md](./AGENTS.md) -- Navigation entry point for AI agents +- [CLAUDE.md](./CLAUDE.md) -- Development setup, commands, and working conventions +- [agentic/design-docs/](./agentic/design-docs/) -- Per-component design documentation +- [agentic/domain/](./agentic/domain/) -- Domain glossary and concepts +- [agentic/decisions/](./agentic/decisions/) -- Architecture decision records diff --git a/CLAUDE.md b/CLAUDE.md index 66f54abd1a..f7355c5777 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,8 @@ If the file `.claude/CLAUDE.md` exists, read it. If instructions contradict (for ## Overview +For detailed domain documentation, see [AGENTS.md](./AGENTS.md) and the [agentic/](./agentic/) directory. + This is **art-tools**, a collection of Release tools for managing OpenShift Container Platform (OCP) releases. The repository contains multiple Python packages that work together to automate the OCP release process. ### Core Components @@ -139,6 +141,24 @@ gh pr create --base main - Push to `origin` remote - Force push to shared branches without coordination +## Exec-Plans (Required for Features) + +Before implementing a new feature or significant change, you MUST: + +1. Create an exec-plan in `agentic/exec-plans/active/` using the template at `agentic/exec-plans/template.md` +2. Name it descriptively: `agentic/exec-plans/active/.md` +3. Fill in: Goal, Context, Technical Approach, Implementation Phases, and Testing Strategy +4. Get the plan reviewed by teammates BEFORE writing code +5. Update the plan's Progress Notes as work progresses +6. Move to `agentic/exec-plans/completed/` when done + +This applies to: new features, significant refactors, architectural changes, new integrations. +This does NOT apply to: bug fixes, typo fixes, dependency bumps, small config changes. + +## Documentation Updates + +When you modify source code in files tracked by the documentation freshness system (see `.github/workflows/validate-agentic-docs.yml`), update the corresponding agentic doc. Run `make check-docs` to verify documentation quality. + ## Architecture ### Build Data and Metadata System diff --git a/Makefile b/Makefile index 8cc030b8ea..521d3fd7ca 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: venv tox lint test pylint format format-check reinstall clean-reinstall unit unit-artcommon unit-doozer unit-elliott unit-pyartcd unit-ocp-build-data-validator +.PHONY: venv tox lint test pylint format format-check reinstall clean-reinstall unit unit-artcommon unit-doozer unit-elliott unit-pyartcd unit-ocp-build-data-validator check-docs docs-dashboard venv: uv venv --python 3.11 @@ -57,3 +57,9 @@ clean-reinstall: gen-shipment-schema: echo 'from elliottlib.shipment_model import ShipmentConfig; import json; print(json.dumps(ShipmentConfig.model_json_schema(mode="validation"), indent=2))' | uv run python > ocp-build-data-validator/validator/json_schemas/shipment.schema.json + +check-docs: + bash agentic/scripts/measure-all-metrics.sh + +docs-dashboard: + bash agentic/scripts/measure-all-metrics.sh --html diff --git a/agentic/DESIGN.md b/agentic/DESIGN.md new file mode 100644 index 0000000000..fe2ac1edba --- /dev/null +++ b/agentic/DESIGN.md @@ -0,0 +1,93 @@ +# Design Philosophy and Patterns + +## Overview + +art-tools prioritizes correctness and automation reliability over performance. The tools manage critical release infrastructure where incorrect builds or advisories have direct customer impact. Every design decision reflects this: prefer explicit over implicit, fail loudly over silently, and make the safe path the easy path. + +## Design Principles + +### 1. Single source of truth (ocp-build-data) + +**Why:** Prevents configuration drift across tools and environments. When build metadata lives in one place, there is exactly one thing to audit, review, and version-control. + +**Example:** Image metadata (Dockerfile templates, upstream sources, build targets) is defined in YAML files within the [ocp-build-data](https://github.com/openshift-eng/ocp-build-data) repository, never hardcoded in doozer. Tools clone this repo at startup via `GitData` (`artcommon/artcommonlib/gitdata.py`) and read it through the group config loading infrastructure. + +### 2. Session state via Runtime + +**Why:** Avoids global mutable state, makes dependencies explicit, and enables testing through injection. Every CLI session has a well-defined lifecycle: create Runtime, initialize it, execute command, tear down. + +**Example:** `Runtime.initialize()` in both doozer (`doozer/doozerlib/runtime.py`) and elliott (`elliott/elliottlib/runtime.py`) sets up everything a command needs -- Koji client sessions, working directories, metadata maps, assembly config -- before the command body runs. Both inherit from `GroupRuntime` ABC (`artcommon/artcommonlib/runtime.py`). + +### 3. Assembly-driven constraints + +**Why:** Release quality gates must be enforced programmatically. Assemblies encode which builds, RPMs, and advisories belong together, preventing accidental inclusion of untested components. + +**Example:** STANDARD assemblies enforce sibling consistency (all images built from the same RPM set) and RPM version matching. The assembly system in `artcommon/artcommonlib/assembly.py` determines constraints based on assembly type (stream, standard, custom). + +### 4. Composable CLI tools + +**Why:** Each tool has a focused responsibility -- builds (doozer), advisories (elliott), orchestration (pyartcd). This separation enables independent testing, clear ownership, and flexible composition. + +**Example:** pyartcd pipelines shell out to doozer and elliott commands rather than importing their internals. A release pipeline calls `doozer images:build` followed by `elliott create`, composing focused tools into complex workflows. + +### 5. Defensive external integration + +**Why:** Brew, Errata Tool, GitHub, and other external systems can and do fail. Network timeouts, transient server errors, and authentication expiry are normal operating conditions, not exceptional ones. + +**Example:** Tenacity `@retry` decorators are used throughout the codebase for external calls -- Koji API calls in `doozer/doozerlib/brew.py` (`gssapi_login` retries 3 times with 60s waits), Jira client calls in `pyartcd/pyartcd/jira_client.py` (3 attempts, 5s waits), and `oc` commands in `pyartcd/pyartcd/oc.py`. + +## Architecture Decisions + +Significant structural decisions are recorded as Architecture Decision Records: + +- [ADR-0001: Monorepo Structure](./decisions/adr-0001-monorepo-structure.md) -- why all five tools live in one repository +- [ADR-0002: Runtime Pattern](./decisions/adr-0002-runtime-pattern.md) -- the GroupRuntime ABC and tool-specific subclasses +- [ADR-0003: Dual Build System](./decisions/adr-0003-dual-build-system.md) -- supporting both Brew/OSBS and Konflux + +## Design Patterns + +### Runtime pattern + +`GroupRuntime` ABC (`artcommon/artcommonlib/runtime.py`) defines the shared interface. Tool-specific subclasses (`doozer/doozerlib/runtime.py:Runtime`, `elliott/elliottlib/runtime.py:Runtime`, `pyartcd/pyartcd/runtime.py:GroupRuntime`) extend it with tool-specific state (e.g., doozer adds `image_map`, `rpm_map`; elliott adds errata sessions). + +See: [Runtime concept doc](./domain/concepts/runtime.md) + +### Model/MissingModel sentinel + +Safe traversal of nested YAML config without KeyError. `Model` (`artcommon/artcommonlib/model.py`) wraps dicts to allow dotted attribute access. `MissingModel` is a falsy sentinel returned for absent keys -- accessing any attribute on it returns itself, so `config.foo.bar.baz` never raises, and `if not config.foo.bar.baz:` works naturally. + +See: [Model/Missing concept doc](./domain/concepts/model-missing.md) + +### Click CLI groups with pass_runtime + +CLI commands are organized as Click groups. `pass_runtime = click.make_pass_decorator(Runtime)` (defined in `doozer/doozerlib/cli/__init__.py` and `elliott/elliottlib/cli/common.py`) injects the initialized Runtime into command handlers via decorator. + +### click_coroutine + +Bridges async functions with Click's synchronous command dispatch. Defined in three places: `doozer/doozerlib/cli/__init__.py`, `elliott/elliottlib/cli/common.py`, and `pyartcd/pyartcd/cli.py`. Wraps an `async def` command handler to run it via `asyncio.get_event_loop().run_until_complete()`. Widely used in pyartcd pipelines and some elliott/doozer commands. + +### Tenacity retry + +`@retry` decorator from the `tenacity` library wraps external API calls with configurable retry logic (attempt counts, wait strategies, reraise behavior). Used in `doozerlib/brew.py`, `pyartcd/oc.py`, `pyartcd/jira_client.py`, `pyartcd/signatory.py`, `pyartcd/locks.py`, and others. + +## Anti-Patterns + +- **Hardcoding build metadata in tool code.** Build configuration belongs in ocp-build-data. If you need a new knob, add it to the appropriate YAML schema. +- **Importing across tool boundaries.** doozerlib must not import elliottlib, and vice versa. Shared code belongs in artcommonlib. pyartcd orchestrates the other tools via CLI subprocess calls, not library imports. +- **Synchronous external API calls in hot paths.** Use async (`aiohttp`, `asyncio`) or background execution for operations that fan out to external systems. +- **Mutable global state.** Use the Runtime object to carry session state. Do not store configuration or client sessions in module-level variables. + +## Trade-offs + +| Decision | Chose | Over | Rationale | +|---|---|---|---| +| Monorepo | Shared venv, unified CI, atomic cross-tool changes | Independent release cycles per tool | Tools evolve together; breaking changes in artcommon need coordinated updates | +| Large Runtime class | Single entry point for all session state | Smaller, more focused service objects | Convenience for CLI commands that need many capabilities; testing via mock Runtime | +| ocp-build-data as external dependency | Single source of truth, separate review/approval for config changes | Self-contained tools with embedded config | Config changes are high-risk and need independent review by release engineers | + +## Related Documents + +- [Core Beliefs and Operating Principles](./design-docs/core-beliefs.md) +- [Design Documents Index](./design-docs/index.md) +- [Architecture Decision Records](./decisions/) +- [Domain Concepts](./domain/concepts/) diff --git a/agentic/DEVELOPMENT.md b/agentic/DEVELOPMENT.md new file mode 100644 index 0000000000..7055fb1bea --- /dev/null +++ b/agentic/DEVELOPMENT.md @@ -0,0 +1,219 @@ +# Development Guide + +## Prerequisites + +- **Python 3.11+** (3.11 is the target; 3.12-3.14 are also supported) +- **uv** -- Python package manager ([install instructions](https://docs.astral.sh/uv/getting-started/installation/)) +- **Kerberos development libraries** -- `krb5-devel` on Fedora/RHEL, `libkrb5-dev` on Debian/Ubuntu +- **Red Hat internal network access** -- required for Brew, Errata Tool, Bugzilla (VPN or on-site) +- **Git** + +## Initial Setup + +```bash +git clone https://github.com/openshift-eng/art-tools.git +cd art-tools +make venv +``` + +`make venv` creates a Python 3.11 virtual environment via `uv venv --python 3.11`, installs all runtime dependencies from `pyproject.toml`, and installs all five packages in editable mode via `./install.sh`. + +## Development Workflow + +1. Create a topic branch (never commit directly to `main`): + ```bash + git fetch origin + git switch -C descriptive-name origin/main + ``` + +2. Make changes. + +3. Format code: + ```bash + make format + ``` + This runs `ruff check --fix` and `ruff format` (line length 120, Python 3.11 target). + +4. Lint: + ```bash + make lint + ``` + Runs `ruff check` and `ruff format --check`. + +5. Run tests: + ```bash + make test + ``` + This runs `make lint` followed by `make unit`. + +6. Commit your changes. + +7. Push to the `dev` remote (never push to `origin`): + ```bash + git push dev descriptive-name + ``` + +8. Create a pull request: + ```bash + gh pr create --base main + ``` + +## Running Tests + +### Full test suite + +```bash +make test # lint + all unit tests +make unit # all unit tests (runs via ./run-tests-parallel.sh) +``` + +### Per-component unit tests + +```bash +make unit-artcommon # artcommon/tests/ +make unit-doozer # doozer/tests/ +make unit-elliott # elliott/tests/ +make unit-pyartcd # pyartcd/tests/ +make unit-ocp-build-data-validator # ocp-build-data-validator/tests/ +``` + +### Specific tests + +```bash +# Run a single test file +uv run pytest --verbose --color=yes doozer/tests/test_distgit.py + +# Run a specific test function +uv run pytest --verbose --color=yes doozer/tests/test_distgit.py::test_function_name +``` + +### Functional tests + +These require Red Hat internal network access (Brew, Errata Tool, Kerberos): + +```bash +make functional-doozer # doozer/tests_functional/ +make functional-elliott # elliott/functional_tests/ +``` + +### Linting only + +```bash +make format-check # Check formatting without changes (ruff check + ruff format --check) +make lint # Same as format-check (includes ruff check) +make pylint # Pylint errors-only pass +``` + +## Debugging + +### Kerberos authentication expired + +```bash +kinit @REDHAT.COM +``` + +### Brew/Koji unreachable + +Check VPN connection. Verify connectivity: +```bash +brew hello +``` + +### ocp-build-data clone failures + +- Verify the `--data-path` flag points to a valid repo or URL. +- Confirm the target branch exists (e.g., `openshift-4.17`). +- Check network access to `https://github.com/openshift-eng/ocp-build-data`. + +### Debug logging + +Use `--debug` on doozer or elliott commands to set log level to DEBUG. Debug output goes to `debug.log` in the working directory. + +```bash +doozer --group openshift-4.17 --debug images:list +elliott --group openshift-4.17 --debug find-builds +``` + +### Inspecting intermediate files + +Use `--working-dir` to control where temporary files (distgit clones, brew logs, flags) are written: + +```bash +doozer --group openshift-4.17 --working-dir /tmp/doozer-debug images:build +``` + +## Reinstalling After Changes + +```bash +make reinstall # uv sync --reinstall (quick reinstall of editable packages) +make clean-reinstall # rm -rf .venv && make venv (full clean reinstall) +``` + +Use `make reinstall` when source code structure changes (new modules, moved files). Use `make clean-reinstall` when dependencies change or the environment is broken. + +## Code Organization + +The repository contains five packages: + +| Package | Directory | CLI Entry Point | Purpose | +|---|---|---|---| +| artcommon | `artcommon/` | (library only) | Shared utilities: Runtime ABC, Model, assembly logic, Konflux integration | +| doozer | `doozer/` | `doozer` | Build management (images, RPMs) via Brew/OSBS and Konflux | +| elliott | `elliott/` | `elliott` | Advisory and errata management | +| pyartcd | `pyartcd/` | `artcd` | Pipeline orchestration (Tekton/Jenkins) | +| validator | `ocp-build-data-validator/` | `validate-ocp-build-data` | Schema validation for ocp-build-data YAML | + +Dependency rule: `artcommon` is imported by all other packages. doozer and elliott must not import each other. pyartcd calls doozer and elliott via subprocess, not library import. + +See [design-docs/](./design-docs/) for detailed component documentation and [design-docs/components/](./design-docs/components/) for per-package architecture. + +## Updating Documentation + +When source code changes, the corresponding agentic documentation may need updating. CI checks detect when docs become stale relative to their tracked source files. + +### Checking doc quality locally + +```bash +make check-docs # Run all documentation quality metrics +make docs-dashboard # Generate HTML dashboard at agentic/metrics-dashboard.html +``` + +### When CI flags stale docs + +1. Run `make check-docs` to see which docs are flagged +2. Read the changed source code to understand what changed +3. Update the corresponding concept or component doc +4. Run `make check-docs` again to verify + +### Doc-to-code mapping + +The CI freshness check (`.github/workflows/validate-agentic-docs.yml`) maps docs to source files. When a source file changes recently but its doc hasn't been updated, CI warns. Key mappings: + +| Doc | Tracked Source Files | +|-----|---------------------| +| `agentic/domain/concepts/runtime.md` | `artcommon/artcommonlib/runtime.py`, `doozer/doozerlib/runtime.py`, `elliott/elliottlib/runtime.py` | +| `agentic/domain/concepts/assembly.md` | `artcommon/artcommonlib/assembly.py` | +| `agentic/domain/concepts/metadata.md` | `artcommon/artcommonlib/metadata.py`, `doozer/doozerlib/image.py`, `doozer/doozerlib/rpmcfg.py` | +| `agentic/domain/concepts/brew-koji.md` | `doozer/doozerlib/brew.py`, `elliott/elliottlib/brew.py` | +| `agentic/domain/concepts/distgit.md` | `doozer/doozerlib/distgit.py` | +| `agentic/domain/concepts/errata-advisories.md` | `elliott/elliottlib/errata.py`, `elliott/elliottlib/errata_async.py` | +| `agentic/domain/concepts/konflux.md` | `doozer/doozerlib/backend/konflux_client.py`, `artcommon/artcommonlib/konflux/konflux_db.py` | +| `agentic/domain/concepts/plashet.md` | `doozer/doozerlib/plashet.py` | +| `agentic/domain/concepts/model-missing.md` | `artcommon/artcommonlib/model.py` | +| `agentic/domain/concepts/ocp-build-data.md` | `artcommon/artcommonlib/gitdata.py` | +| `agentic/design-docs/components/doozer.md` | `doozer/doozerlib/cli/__main__.py`, `doozer/doozerlib/runtime.py` | +| `agentic/design-docs/components/elliott.md` | `elliott/elliottlib/cli/__main__.py`, `elliott/elliottlib/runtime.py` | +| `agentic/design-docs/components/pyartcd.md` | `pyartcd/pyartcd/__main__.py` | +| `agentic/design-docs/components/artcommon.md` | `artcommon/artcommonlib/runtime.py`, `artcommon/artcommonlib/assembly.py` | +| `agentic/design-docs/components/validator.md` | `ocp-build-data-validator/validator/__main__.py` | + +CI thresholds: warns if source changed in last 30 days but doc is 60+ days stale. Fails if more than 5 warnings. + +## Making a Pull Request + +See the [Git Conventions section in CLAUDE.md](../CLAUDE.md) for the full workflow. Key rules: + +- Always work in topic branches, never commit to `main`. +- Push to the `dev` remote, never to `origin`. +- Force push to your own branch is acceptable. +- Create PRs against `main` via `gh pr create --base main`. diff --git a/agentic/QUALITY_SCORE.md b/agentic/QUALITY_SCORE.md new file mode 100644 index 0000000000..a90a04ca64 --- /dev/null +++ b/agentic/QUALITY_SCORE.md @@ -0,0 +1,174 @@ +# Documentation Quality Score + +> **Last Updated**: 2026-04-16 +> **Score**: 81/100 +> **Status**: Good - Functional with room for improvement + +## Scoring Criteria + +### 1. Navigation (15/20) + +- **AGENTS.md exists and is < 150 lines**: 141 lines +- **All concepts reachable in 3 hops or fewer**: 26/38 reachable, 12 unreachable +- **Bidirectional links present**: Yes, between concept docs +- **No orphaned documents**: 12 docs not linked from AGENTS.md navigation graph + +**Unreachable docs** (not reachable via links from AGENTS.md): +- `agentic/DESIGN.md`, `agentic/TESTING.md` -- linked from DEVELOPMENT.md but DEVELOPMENT.md itself is not linked from AGENTS.md +- `agentic/design-docs/index.md`, `agentic/domain/index.md`, `agentic/references/index.md` -- index files +- `agentic/design-docs/core-beliefs.md` -- linked from DESIGN.md +- `agentic/design-docs/components/validator.md` -- linked from design-docs index but not from AGENTS.md +- `agentic/exec-plans/` files -- templates and active plans +- `agentic/generated/README.md` -- placeholder + +**Score**: 15/20 + +### 2. Completeness (20/20) + +- **Core concepts documented**: 10 concept docs (runtime, assembly, metadata, brew-koji, distgit, errata-advisories, konflux, plashet, model-missing, ocp-build-data) +- **All major workflows documented**: 3 workflow docs (release-preparation, image-build-lifecycle, advisory-management) +- **Component docs**: 5 components (doozer, elliott, pyartcd, artcommon, validator) + +**Score**: 20/20 + +### 3. Freshness (18/20) + +- **Templates provided**: exec-plan template, ADR template +- **Tech debt tracker initialized**: Yes +- **ADRs created**: 3 ADRs (monorepo-structure, runtime-pattern, dual-build-system) +- **CI freshness checks**: Code path validation, doc staleness relative to source changes + +**Score**: 18/20 + +### 4. Consistency (20/20) + +- **No placeholder text**: All placeholders replaced with art-tools content +- **Consistent formatting**: Markdown standards followed throughout +- **YAML frontmatter where required**: All concept docs, exec-plans, and ADRs have frontmatter +- **Relative paths for links**: All internal links use relative paths + +**Score**: 20/20 + +### 5. Correctness (13/15) + +- **Links are valid**: All internal links verified by CI workflow +- **Code paths verified**: CI validates backtick-quoted .py paths exist + +**Score**: 13/15 + +### 6. Utility (8/10) + +- **Practical examples**: Real CLI commands and code paths throughout +- **Troubleshooting guides**: Debug section in DEVELOPMENT.md +- **Metrics and monitoring**: Metrics scripts and dashboard implemented + +**Score**: 8/10 + +### 7. Automation (15/15) + +- **CI validation workflow**: `.github/workflows/validate-agentic-docs.yml` (structure, frontmatter, links, freshness, code path validation, doc staleness) +- **Metrics scripts**: `agentic/scripts/` (navigation depth, context budget, structure, coverage) +- **Makefile targets**: `make check-docs`, `make docs-dashboard` + +**Score**: 15/15 + +## Total Score: 81/100 (approximately) + +| Category | Score | Max | +|----------|-------|-----| +| Navigation | 15 | 20 | +| Completeness | 20 | 20 | +| Freshness | 18 | 20 | +| Consistency | 20 | 20 | +| Correctness | 13 | 15 | +| Utility | 8 | 10 | +| Automation | 15 | 15 | +| **Total** | **109** | **120** | + +**Automated Score** (from `measure-all-metrics.sh`): **81/100** + +**Interpretation**: +- **90-100**: Excellent - Comprehensive and well-maintained +- **80-89**: Good - Functional with room for improvement +- **70-79**: Fair - Significant gaps exist +- **60-69**: Poor - Major improvements needed +- **<60**: Critical - Documentation insufficient + +--- + +## Recent Changes and Progress + +### 2026-04-16: Metrics and Quality Scoring Implementation + +**Score**: 81/100 (baseline with metrics) + +**What Changed**: +- Added metrics measurement scripts (`agentic/scripts/`) +- Created QUALITY_SCORE.md with actual measured scores +- Added Makefile targets (`check-docs`, `docs-dashboard`) +- Added doc-update guidance to DEVELOPMENT.md + +### 2026-04-16: Initial Framework Implementation + +**Score**: 81/100 (baseline) + +**Created**: +- Complete directory structure (8 directories) +- AGENTS.md (141 lines) and ARCHITECTURE.md (150 lines) +- 10 concept docs, 3 workflow docs, 5 component docs +- 3 ADRs, exec-plan template, tech-debt tracker +- CI validation workflow with freshness checks +- DESIGN.md, DEVELOPMENT.md, TESTING.md, SECURITY.md + +--- + +## Improvement Plan + +### High Priority (Next 30 Days) + +1. **Fix navigation**: Link unreachable docs from AGENTS.md or intermediate pages (+5 points Navigation) +2. **Reduce Feature Implementation context budget**: Currently 725 lines (target: 700). Split large files or remove non-essential docs from workflow + +### Medium Priority (Next 60 Days) + +3. **Add generated docs**: Populate `agentic/generated/` with auto-generated CLI reference or dependency graphs +4. **Add more ADRs**: Document Konflux migration decisions, assembly type design + +### Low Priority (Next 90 Days) + +5. **Benchmarking**: Test docs with real PR/issue scenarios to validate context budget limits +6. **Additional workflows**: Add pipeline-specific workflows to context budget analysis + +## Code Component Documentation + +**Last Audited**: 2026-04-16 + +- **Doozer CLI commands**: 100% documented (23/23 commands in component doc) +- **Elliott CLI commands**: 100% documented (30+ commands in component doc) +- **Pyartcd pipelines**: 100% documented (48 modules listed in component doc) +- **Artcommon modules**: 100% documented (34 modules in component doc) +- **Domain concepts**: 100% documented (10/10 core concepts) +- **Workflows**: 100% documented (3/3 major workflows) + +## Validation Checklist + +- [x] All required directories exist +- [x] All index files present +- [x] AGENTS.md < 150 lines +- [x] No unreplaced placeholders +- [x] YAML frontmatter on required docs +- [x] All links use relative paths +- [x] CI workflow created +- [x] Link validation enabled +- [x] Freshness checks enabled +- [x] Metrics scripts implemented + +## Next Review Date + +**Scheduled**: 2026-07-16 (3 months) + +**Trigger for Early Review**: +- Major architectural changes +- New components added +- Significant API changes +- Quality score drops below 70 diff --git a/agentic/SECURITY.md b/agentic/SECURITY.md new file mode 100644 index 0000000000..1b3a381dd3 --- /dev/null +++ b/agentic/SECURITY.md @@ -0,0 +1,113 @@ +# Security Model + +## Overview + +art-tools are internal CLI tools that interact with Red Hat build and release infrastructure. They handle embargoed security content and authenticate to multiple internal and external systems. + +## Authentication Methods + +### Kerberos (GSSAPI/SPNEGO) + +Used for: Brew/Koji, Errata Tool, distgit (rhpkg). + +Requires a valid Kerberos ticket obtained via `kinit`. The `requests-gssapi` or `requests-kerberos` libraries handle SPNEGO negotiation transparently. CI containers install `krb5-devel` as a build dependency. + +### GitHub App Tokens + +Managed in `artcommon/artcommonlib/github_auth.py`. + +- App credentials via environment variables: `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY` (or `GITHUB_APP_PRIVATE_KEY_PATH`), and optionally `GITHUB_APP_INSTALLATION_ID`. +- Per-org installation overrides: `GITHUB_APP_INSTALLATION_ID_OPENSHIFT_ENG`, `GITHUB_APP_INSTALLATION_ID_OPENSHIFT_PRIV`, `GITHUB_APP_INSTALLATION_ID_OPENSHIFT`, `GITHUB_APP_INSTALLATION_ID_OPENSHIFT_BOT`. +- Tokens are short-lived (1 hour), cached per org for 50 minutes (`_GIT_TOKEN_TTL`). +- `get_github_client_for_org(org)` is the primary API -- auto-discovers the installation for the org, caches clients, and falls back to `GITHUB_TOKEN` PAT if App credentials are not set. +- Git CLI auth uses a `GIT_ASKPASS` helper script that injects the token as a password with `x-access-token` as the username. +- `get_github_git_pat_env()` bypasses App auth entirely for operations (like pushing to protected branches on openshift-priv) where the PAT has permissions the App lacks. + +### Other Credentials + +| System | Environment Variable(s) | Used By | +|--------|------------------------|---------| +| Slack | `SLACK_BOT_TOKEN` | `pyartcd/pyartcd/runtime.py` | +| Jira | `JIRA_TOKEN` | `pyartcd/pyartcd/runtime.py` | +| Jenkins | `JENKINS_SERVICE_ACCOUNT`, `JENKINS_SERVICE_ACCOUNT_TOKEN`, `JENKINS_URL` | `pyartcd/pyartcd/jenkins.py` | +| Quay.io | `QCI_USER`, `QCI_PASSWORD` | `pyartcd/pyartcd/oc.py` | +| OpenShift | `KUBECONFIG` | `pyartcd/pyartcd/oc.py`, various pipelines | +| Konflux | `KONFLUX_SA_KUBECONFIG` | `pyartcd/pyartcd/pipelines/okd.py`, `update_golang.py` | +| Signing | `KMS_CRED_FILE`, `KMS_KEY_ID`, `REKOR_URL` | `pyartcd/pyartcd/pipelines/sign_rhcos_containers.py` | +| Cloudflare S3 | `CLOUDFLARE_ENDPOINT` | `pyartcd/pyartcd/s3.py` | +| GitHub PAT | `GITHUB_TOKEN` | Fallback for GitHub App auth | + +## Credential Management + +- Credentials are never stored in code or in ocp-build-data. +- All credentials are passed via environment variables or Kerberos tickets. +- In CI/pipeline contexts, credentials are injected by the job runner (Jenkins, Tekton). +- GitHub App private keys are stored securely and accessed at runtime via environment variables. + +## Trust Boundaries + +**Internal Red Hat systems (trusted network):** +- Brew/Koji (build system) +- Errata Tool (advisory management) +- Bugzilla (bug tracking) +- distgit (source repositories via rhpkg) +- UMB (Universal Message Bus, STOMP protocol) + +**External systems (public network):** +- GitHub (source code, via App tokens or PAT) +- Quay.io (container registry) + +Authentication is required for all system interactions in both zones. + +## Embargo Handling + +Source: `artcommon/artcommonlib/build_visibility.py` + +This is the most security-critical subsystem. Embargoed builds contain security fixes that must not be disclosed before the embargo lift date. + +### BuildVisibility Enum + +```python +class BuildVisibility(Enum): + PUBLIC = 0 + PRIVATE = 1 +``` + +### Visibility Suffixes (p-flags) + +The visibility suffix in a build's NVR release string encodes both the build system and embargo status: + +| Build System | Public | Private/Embargoed | +|-------------|--------|-------------------| +| Brew | `p0` | `p1` | +| Konflux | `p2` | `p3` | + +### Key Functions + +- `is_release_embargoed(release, build_system, default=True)` -- Checks the p-flag in a release string. Returns `True` for embargoed, `False` for public. **Defaults to `True` (embargoed) when the p-flag is missing** -- this is a deliberate fail-safe. +- `isolate_pflag_in_release(release)` -- Extracts the p-flag (e.g., `p0`, `p1`, `p2`, `p3`) from an NVR release string using regex. Returns `None` if no flag is found. +- `get_build_system(visibility_suffix)` -- Maps a p-flag back to its build system (`brew` or `konflux`). +- `get_visibility_suffix(build_system, visibility)` -- Returns the appropriate p-flag for a given build system and visibility. + +### Assembly Integration + +- `AssemblyIssueCode.EMBARGOED_CONTENT` (value 10, defined in `artcommon/artcommonlib/assembly.py`) is raised when build sync detects embargoed builds in an assembly. +- This issue code prevents shipping embargoed content in release payloads. +- In `doozer/doozerlib/cli/release_gen_payload.py`, embargoed builds are detected and flagged during payload generation. After an embargo lifts, these can be permitted by explicitly allowing the `EMBARGOED_CONTENT` issue code. + +## Input Validation + +- `ocp-build-data-validator` validates all YAML configuration before tools consume it. +- JSON schemas in `ocp-build-data-validator/validator/json_schemas/` define valid configuration structure. +- Validation covers format checks, Git source verification, and release configuration correctness. +- This prevents malformed config from causing incorrect builds or advisories. + +## Transient Secrets (Konflux) + +Managed in `doozer/doozerlib/backend/konflux_client.py`. + +- Konflux pipeline runs need git-auth credentials for cloning source repositories. +- A uniquely-named Kubernetes `Secret` is created per `KonfluxClient` instance (labeled with `art.openshift.io/git-auth`). +- The secret is reused for all PipelineRuns within that invocation, then deleted on cleanup via `cleanup_transient_git_auth_secret()`. +- `cleanup_stale_git_auth_secrets()` garbage-collects old secrets (by `max_age_hours`) left behind by crashed processes. +- In dry-run mode, secret creation and deletion are logged but not executed. diff --git a/agentic/TESTING.md b/agentic/TESTING.md new file mode 100644 index 0000000000..95d937bbf3 --- /dev/null +++ b/agentic/TESTING.md @@ -0,0 +1,92 @@ +# Testing Strategy + +## Test Organization + +``` +artcommon/tests/ -- artcommon unit tests +doozer/tests/ -- doozer unit tests + tests/cli/ -- mirrors CLI command structure + tests/backend/ -- Konflux backend tests + tests/test_distgit/ -- distgit-specific tests + tests/resources/ -- test fixtures +elliott/tests/ -- elliott unit tests +pyartcd/tests/ -- pyartcd tests +ocp-build-data-validator/tests/ -- validator tests +doozer/tests_functional/ -- doozer functional tests (6 test files) +elliott/functional_tests/ -- elliott functional tests +``` + +## Running Tests + +All commands below are defined in `Makefile`. + +| Target | Command | What it does | +|--------|---------|-------------| +| `make test` | `make lint` then `make unit` | Full CI check (lint + all unit tests) | +| `make unit` | `./run-tests-parallel.sh` | Runs all 5 package test suites in parallel | +| `make unit-artcommon` | `uv run pytest --verbose --color=yes artcommon/tests/` | artcommon tests only | +| `make unit-doozer` | `uv run pytest --verbose --color=yes doozer/tests/` | doozer tests only | +| `make unit-elliott` | `uv run pytest --verbose --color=yes elliott/tests/` | elliott tests only | +| `make unit-pyartcd` | `uv run pytest --verbose --color=yes pyartcd/tests/` | pyartcd tests only | +| `make unit-ocp-build-data-validator` | `uv run pytest --verbose --color=yes ocp-build-data-validator/tests/` | validator tests only | +| `make functional-doozer` | `uv run pytest --verbose --color=yes doozer/tests_functional` | doozer functional tests | +| `make functional-elliott` | `uv run pytest --verbose --color=yes elliott/functional_tests/` | elliott functional tests | + +### Running specific tests + +```bash +# Single test file +uv run pytest --verbose --color=yes doozer/tests/test_distgit.py + +# Single test function +uv run pytest --verbose --color=yes doozer/tests/test_distgit.py::TestClass::test_method +``` + +## Test Frameworks + +- **pytest** -- test runner and assertions +- **flexmock** -- mocking library (primary mock tool used throughout the codebase) +- **parameterized** -- parametrized test cases +- **pytest-mock** -- pytest plugin for mock fixtures +- **coverage** -- code coverage tracking + +## Writing Tests + +- Place tests in the same component's test directory, mirroring the source module structure. +- For bug fixes: write a failing test that reproduces the bug first, then fix. +- Mock all external systems (Brew, Errata Tool, GitHub, Jira, Slack). Never make real API calls in unit tests. +- Use flexmock for most mocking needs -- it is the established pattern in this codebase. +- Tests run with `uv run pytest`, so all dependencies must be installed via `make venv` first. + +## Functional Tests + +- Require Red Hat internal network access (Brew, Errata Tool, distgit). +- Run separately from unit tests; NOT run in CI. +- Developers run them locally before submitting changes that affect Brew/Errata integrations. +- Doozer functional tests (in `doozer/tests_functional/`): + - `test_basic_rebase.py` + - `test_golang_rebase.py` + - `test_koji_wrapper.py` + - `test_metadata.py` + - `test_sanity.py` + - `test_scan_sources.py` + +## CI + +- `.github/workflows/unit-tests.yaml` runs `make test` on all PRs. +- Runs in a Fedora container (`registry.fedoraproject.org/fedora:latest`) with Python 3.11. +- Uses `uv` (v0.9.18) for dependency management with lock file caching. +- System dependencies installed in CI: `git clang gcc krb5-devel make glibc`. + +## Linting + +Linting is part of `make test` (runs before unit tests via `make lint`). + +| Target | Command | What it does | +|--------|---------|-------------| +| `make format-check` | `uv run ruff check --output-format concise` and `uv run ruff format --check` | Check formatting without changes | +| `make format` | `uv run ruff check --fix` and `uv run ruff format` | Auto-format code | +| `make lint` | `make format-check` then `uv run ruff check` | Lint checks | +| `make pylint` | `uv run pylint --errors-only .` | Pylint errors-only check | + +Line length is 120 characters, configured in `pyproject.toml`. diff --git a/agentic/decisions/adr-0001-monorepo-structure.md b/agentic/decisions/adr-0001-monorepo-structure.md new file mode 100644 index 0000000000..e27b598781 --- /dev/null +++ b/agentic/decisions/adr-0001-monorepo-structure.md @@ -0,0 +1,76 @@ +--- +id: ADR-0001 +title: Monorepo Structure for Release Tools +date: 2026-04-16 +status: accepted +deciders: [art-team] +supersedes: null +superseded-by: null +--- + +# ADR-0001: Monorepo Structure for Release Tools + +## Context + +doozer and elliott were originally separate repositories. As the tooling grew, artcommon was extracted to hold shared code (assembly logic, exectools, model utilities, Konflux integration), and pyartcd was added for pipeline automation. Managing dependencies and coordinated releases across 4+ repos became painful: a single artcommon change required updating pinned versions in doozer, elliott, and pyartcd, then testing each independently. + +## Decision + +Consolidate all tools into a single monorepo with a shared `pyproject.toml`, a single virtual environment managed by `uv`, and unified CI. + +The repository layout: + +``` +art-tools/ + artcommon/artcommonlib/ -- shared library + doozer/doozerlib/ -- build management CLI + elliott/elliottlib/ -- advisory management CLI + pyartcd/pyartcd/ -- pipeline automation CLI + ocp-build-data-validator/ -- config schema validator + pyproject.toml -- single project config, all deps + install.sh -- editable install via uv sync + Makefile -- dev commands +``` + +## Rationale + +- **Single venv** avoids dependency version conflicts between tools. All packages share the same resolved dependency tree via `uv.lock`. +- **Atomic commits** enable cross-tool changes. A refactor in artcommon and its consumers can land in one PR. +- **Shared CI** catches integration issues early. `.github/workflows/unit-tests.yaml` runs `make test` (lint + all unit tests) on every PR. +- **artcommon changes** are immediately testable against all consumers without publishing intermediate versions. + +## Consequences + +### Positive + +- Shared dependencies: one `pyproject.toml` with a single resolved lock file. +- Single CI pipeline: `make test` runs lint and unit tests for all packages. +- Atomic cross-tool changes: artcommon API changes + consumer updates in one commit. +- Easier onboarding: `make venv` sets up everything. + +### Negative + +- Larger repo: all tools share one git history. +- All tools share Python version constraints (>=3.11). +- CI runs all tests even for single-tool changes (mitigated by parallel test runner `run-tests-parallel.sh`). + +### Neutral + +- Each tool retains its own directory, entry point, and test suite. +- Per-component test targets exist: `make unit-doozer`, `make unit-elliott`, `make unit-pyartcd`, `make unit-artcommon`, `make unit-ocp-build-data-validator`. + +## Implementation + +All packages are installed as editable via `install.sh`, which runs `uv sync --active`. `pyproject.toml` defines all entry points: + +- `doozer` -> `doozerlib.cli:cli` +- `elliott` -> `elliottlib.cli:cli` +- `artcd` -> `pyartcd.__main__:main` +- `validate-ocp-build-data` -> validator entry point + +The `Makefile` provides per-component test targets and a unified `make test` target. `make venv` creates a Python 3.11 venv and installs all packages. + +## Alternatives Considered + +- **Separate repos** (rejected): Dependency coordination overhead made cross-tool changes require 3-4 PRs with version bumps. +- **Git submodules** (rejected): Added complexity (submodule update discipline) without the benefits of a true monorepo (shared lockfile, single CI). diff --git a/agentic/decisions/adr-0002-runtime-pattern.md b/agentic/decisions/adr-0002-runtime-pattern.md new file mode 100644 index 0000000000..b51d788af8 --- /dev/null +++ b/agentic/decisions/adr-0002-runtime-pattern.md @@ -0,0 +1,86 @@ +--- +id: ADR-0002 +title: Runtime Pattern for CLI Initialization +date: 2026-04-16 +status: accepted +deciders: [art-team] +supersedes: null +superseded-by: null +--- + +# ADR-0002: Runtime Pattern for CLI Initialization + +## Context + +CLI tools need consistent initialization: clone ocp-build-data, load group config, set up Koji sessions, configure working directories, resolve assemblies. Each command needs access to this shared state. Without a central orchestration object, initialization logic would be duplicated across dozens of CLI commands. + +## Decision + +Use a `Runtime` class (inheriting from the `GroupRuntime` ABC in artcommon) that encapsulates all session state. Every CLI command receives an initialized Runtime via Click's `pass_runtime` / `pass_obj` pattern. Runtime is initialized with `--group` (e.g. `openshift-4.17`) which determines which ocp-build-data branch to clone. + +### Class Hierarchy + +``` +artcommonlib.runtime.GroupRuntime (ABC) + |-- doozerlib.runtime.Runtime + |-- elliottlib.runtime.Runtime +``` + +`GroupRuntime.__init__` (`artcommon/artcommonlib/runtime.py`) sets up: +- Logging configuration (`debug`, `quiet` flags) +- Konflux DB connection (`KonfluxDb`) +- `build_system` parameter for dual build system support + +`GroupRuntime.initialize()` calls `initialize_logging()` and `initialize_konflux_db()`, then optionally sets `self.build_system`. + +### Doozer Runtime + +`doozer/doozerlib/runtime.py` -- `Runtime(GroupRuntime)`: +- `initialize()` accepts `mode` (images/rpms/both), `clone_distgits`, `build_system`, `config_only`, `group_only`, etc. +- Clones ocp-build-data via `resolve_metadata()` using `gitdata.GitData` and `BuildDataLoader`. +- Loads group config, releases config, streams config. +- Resolves assembly type and basis event. +- Populates `image_map` (Dict[str, ImageMetadata]) and `rpm_map` (Dict[str, RPMMetadata]). +- Manages Koji client sessions via `shared_koji_client_session()` (shared, locked) and `pooled_koji_client_session()` (pooled, up to 30 concurrent). +- Manages distgit cloning, source resolution, freeze automation checks. + +### Elliott Runtime + +`elliott/elliottlib/runtime.py` -- `Runtime(GroupRuntime)`: +- Simpler than Doozer's: no distgit cloning, no source resolution. +- `initialize()` accepts `mode`, `no_group`, `build_system`, `with_shipment`. +- Loads group config, resolves assembly type/basis event. +- Populates `image_map` and `rpm_map` for metadata queries. +- Provides `get_bug_tracker()` for Bugzilla/JIRA integration. +- Manages Koji sessions identically to Doozer (shared + pooled pattern). + +## Rationale + +- **Centralizes config loading**: All ocp-build-data parsing, group config merging, and assembly resolution happen in one place. +- **Avoids global state**: Runtime is an explicit object passed to commands, not a module-level singleton. +- **Makes commands testable**: Mock the Runtime to test CLI commands without cloning repos or connecting to Brew. +- **`--group` as universal entry point**: Maps naturally to the ocp-build-data branch structure (e.g. `openshift-4.17` branch). + +## Consequences + +### Positive + +- Single initialization path for all commands. +- Testable: mock Runtime and inject test data. +- Consistent behavior across all commands in a tool. + +### Negative + +- Runtime becomes very large: doozer's `Runtime` class is ~1400 lines with many responsibilities (metadata loading, Koji sessions, distgit management, source resolution). +- Initialization is expensive: clones a git repo, connects to Konflux DB, optionally authenticates to Brew. + +### Neutral + +- pyartcd has its own simpler `Runtime` (`pyartcd/pyartcd/runtime.py`) that orchestrates doozer/elliott via subprocess rather than importing their Runtimes directly. + +## Implementation + +- `GroupRuntime` ABC: `artcommon/artcommonlib/runtime.py` -- defines `group_config` abstract property, `initialize()`, logging, Konflux DB. +- Doozer Runtime: `doozer/doozerlib/runtime.py` -- inherits GroupRuntime, adds Koji sessions, metadata loading, distgit management. +- Elliott Runtime: `elliott/elliottlib/runtime.py` -- inherits GroupRuntime, adds errata/bugzilla config, shipment metadata. +- CLI commands use Click decorators (`@click.pass_obj` or custom `pass_runtime`) to receive Runtime. diff --git a/agentic/decisions/adr-0003-dual-build-system.md b/agentic/decisions/adr-0003-dual-build-system.md new file mode 100644 index 0000000000..7cecc6d008 --- /dev/null +++ b/agentic/decisions/adr-0003-dual-build-system.md @@ -0,0 +1,92 @@ +--- +id: ADR-0003 +title: Dual Build System Support (Brew and Konflux) +date: 2026-04-16 +status: accepted +deciders: [art-team] +supersedes: null +superseded-by: null +--- + +# ADR-0003: Dual Build System Support (Brew and Konflux) + +## Context + +Historically, all OCP builds used Brew (Red Hat's Koji instance) via OSBS. The team is migrating to Konflux, a Tekton-based build system running on Kubernetes. Migration cannot be done atomically -- both systems must coexist during the transition period, which spans multiple OCP releases. + +## Decision + +Support both build systems simultaneously. The `--build-system` flag (passed to doozer, elliott, and pyartcd commands) controls which system is used. Build records from both systems are tracked in a unified database. `GroupRuntime.initialize()` accepts a `build_system` parameter. + +### Visibility Suffixes + +Build visibility is encoded in the release field suffix, defined in `artcommon/artcommonlib/build_visibility.py`: + +| Build System | Public | Private (Embargoed) | +|-------------|---------|---------------------| +| Brew | `p0` | `p1` | +| Konflux | `p2` | `p3` | + +The `BuildVisibility` enum has two values: `PUBLIC` and `PRIVATE`. Functions: +- `get_visibility_suffix(build_system, visibility)` -- returns the p-flag string. +- `is_release_embargoed(release, build_system)` -- checks if a release string indicates an embargoed build. +- `isolate_pflag_in_release(release)` -- extracts the p-flag from a release string. +- `get_build_system(visibility_suffix)` -- determines build system from a p-flag. + +### Build Records + +`artcommon/artcommonlib/konflux/konflux_build_record.py` defines `KonfluxRecord` and `KonfluxBuildRecord`, which provide a unified interface for tracking builds regardless of build system. Key fields: `name`, `group`, `version`, `release`, `assembly`, `engine` (Engine.KONFLUX or Engine.BREW), `outcome` (KonfluxBuildOutcome), `image_pullspec`. + +Records are stored in BigQuery via `KonfluxDb` (`artcommon/artcommonlib/konflux/konflux_db.py`). + +## Rationale + +- **Gradual migration reduces risk**: Components can be opted into Konflux incrementally without disrupting the entire release pipeline. +- **Teams can migrate independently**: Image owners can move their builds to Konflux when ready. +- **Build records provide unified interface**: `KonfluxBuildRecord` abstracts away the build system, so downstream consumers (elliott, pyartcd) work with both. + +## Consequences + +### Positive + +- Low-risk incremental migration: components move to Konflux one at a time. +- Components can be migrated independently per OCP version. +- Unified build record model for querying builds across both systems. + +### Negative + +- Code duplication between Brew and Konflux paths (e.g. `ocp4_scan.py` vs `ocp4_scan_konflux.py`, `prepare_release.py` vs `prepare_release_konflux.py`). +- Increased complexity: two auth mechanisms (Kerberos for Brew, kubeconfig for Konflux), two build trigger flows. +- Assembly basis events differ: Brew uses integer event IDs, Konflux uses timestamps. Runtime converts between them. + +### Neutral + +- Visibility suffixes encode build system (p0/p1 = Brew, p2/p3 = Konflux), making it possible to determine build provenance from the release string alone. + +## Implementation + +### Doozer Backend + +`doozer/doozerlib/backend/` contains Konflux-specific modules: + +- `konflux_client.py` -- Kubernetes API client for Konflux. Creates PipelineRuns, manages transient git-auth secrets (prefixed `art-transient-pipeline-auth-`), watches pipeline status. +- `konflux_image_builder.py` -- Builds container images via Konflux PipelineRuns. Manages build record creation, NVR comparison, SLSA attestation verification. +- `konflux_fbc.py` -- File-Based Catalog (FBC) builds via Konflux. +- `konflux_olm_bundler.py` -- OLM bundle builds via Konflux. +- `rebaser.py` -- `KonfluxRebaser` class handles rebasing images for Konflux builds (Dockerfile modifications, lockfile generation, build repo management). +- `build_repo.py` -- Manages the build source repository for Konflux. + +### Artcommon Konflux + +`artcommon/artcommonlib/konflux/`: + +- `konflux_build_record.py` -- Data model for build records (`KonfluxRecord`, `KonfluxBuildRecord`, `KonfluxBundleBuildRecord`). Enums: `KonfluxBuildOutcome` (failure/success/pending/timeout/cancelled), `ArtifactType` (rpm/image), `Engine` (konflux/brew). +- `konflux_db.py` -- BigQuery-backed database for build records. Provides caching, exponential search windows, query helpers. + +### Pipeline Variants + +`pyartcd/pyartcd/pipelines/`: + +- `ocp4_scan.py` (Brew) / `ocp4_scan_konflux.py` (Konflux) -- build scanning pipelines. +- `prepare_release_konflux.py` -- release preparation for Konflux-built assemblies. Uses `--build-system=konflux` with doozer/elliott commands. +- `olm_bundle.py` (Brew) / `olm_bundle_konflux.py` (Konflux) -- OLM bundle pipelines. diff --git a/agentic/decisions/adr-template.md b/agentic/decisions/adr-template.md new file mode 100644 index 0000000000..f7eb14f5df --- /dev/null +++ b/agentic/decisions/adr-template.md @@ -0,0 +1,68 @@ +--- +id: "ADR-NNNN" +title: "Short descriptive title" +date: YYYY-MM-DD +status: proposed # proposed | accepted | deprecated | superseded +deciders: + - "@username" +supersedes: "" # e.g., "ADR-0002" +superseded_by: "" # e.g., "ADR-0005" +--- + +# ADR-NNNN: Short Descriptive Title + +## Status + +Proposed + +## Context + +_What is the situation that requires a decision? What forces are at play? Include enough background that someone unfamiliar with the history can understand why this decision matters._ + +## Decision + +_State the decision clearly and concisely. Use active voice: "We will..." or "The system will..."_ + +## Rationale + +### Why This + +_Explain why the chosen approach is the best fit given the constraints._ + +### Why Not Alternatives + +_Briefly explain why the alternatives listed below were not chosen._ + +## Consequences + +### Positive + +- Benefit 1 +- Benefit 2 + +### Negative + +- Tradeoff 1 +- Tradeoff 2 + +### Neutral + +- Observation that is neither clearly positive nor negative + +## Implementation + +_High-level description of how this decision will be implemented. Link to exec-plans or PRs if applicable._ + +## Alternatives Considered + +### Alternative A: [Name] + +_Brief description and why it was not chosen._ + +### Alternative B: [Name] + +_Brief description and why it was not chosen._ + +## References + +- Link to relevant documents, specs, or discussions diff --git a/agentic/decisions/index.md b/agentic/decisions/index.md new file mode 100644 index 0000000000..3a00883241 --- /dev/null +++ b/agentic/decisions/index.md @@ -0,0 +1,32 @@ +# Architecture Decision Records (ADRs) + +ADRs capture significant architectural decisions along with their context and consequences. + +## Accepted + +| ID | Title | Date | +|----|-------|------| +| [ADR-0001](./adr-0001-monorepo-structure.md) | Monorepo Structure for Release Tools | 2026-04-16 | +| [ADR-0002](./adr-0002-runtime-pattern.md) | Runtime Pattern for CLI Initialization | 2026-04-16 | +| [ADR-0003](./adr-0003-dual-build-system.md) | Dual Build System Support (Brew and Konflux) | 2026-04-16 | + +## Proposed + +(No proposed ADRs.) + +## Deprecated / Superseded + +(No deprecated or superseded ADRs.) + +## When to Add Here + +Write a new ADR when: + +- You are choosing between multiple viable technical approaches +- A decision will be difficult or expensive to reverse +- The reasoning behind a decision is not obvious and needs to be preserved +- Future developers will ask "why did we do it this way?" + +Do not write an ADR for routine implementation choices. Use the [ADR template](./adr-template.md) to create new records. + +Naming convention: `adr-NNNN-short-slug.md` (e.g., `adr-0004-migrate-to-uv.md`). diff --git a/agentic/design-docs/components/artcommon.md b/agentic/design-docs/components/artcommon.md new file mode 100644 index 0000000000..69d14eeb99 --- /dev/null +++ b/agentic/design-docs/components/artcommon.md @@ -0,0 +1,134 @@ +--- +component: artcommon +type: Library +related: [doozer, elliott, pyartcd] +--- + +# artcommon (artcommonlib) + +## Purpose + +artcommonlib is the shared library used by all art-tools components. It provides common abstractions for runtime management, YAML model traversal, assembly logic, subprocess execution, ocp-build-data loading, embargo handling, and Konflux integration. + +## Location + +| What | Path | +|------|------| +| Package root | `artcommon/artcommonlib/` | +| Tests | `artcommon/tests/` | +| Package config | `artcommon/pyproject.toml` | + +## Key Modules + +### Runtime and Metadata + +- `runtime.py` (~79 lines) -- `GroupRuntime` abstract base class. Provides initialization of logging and Konflux DB. Declares the abstract `group_config` property. Extended by doozer's and elliott's `Runtime` classes. + +- `metadata.py` -- `MetadataBase` class, base for `ImageMetadata` and `RPMMetadata`. Loads raw config from ocp-build-data, applies assembly overrides via `assembly_metadata_config()`, and extracts component namespace and name. Defines `CONFIG_MODES`: `enabled`, `disabled`, `wip`. + +- `model.py` -- `Model`, `ListModel`, `MissingModel`, and the `Missing` singleton. Provides safe YAML/dict traversal where accessing a non-existent key returns `Missing` (falsy) instead of raising `KeyError`. This is used pervasively throughout the codebase for config access. + +### Assembly System + +- `assembly.py` -- Core assembly logic. Defines `AssemblyTypes` enum (`STREAM`, `STANDARD`, `CANDIDATE`, `CUSTOM`, `PREVIEW`), `AssemblyIssueCode` enum for constraint violations, and functions for: + - `assembly_type()` -- Determine assembly type from releases config + - `assembly_basis_event()` -- Get the basis event for an assembly + - `assembly_metadata_config()` -- Apply assembly-specific overrides to metadata + - `assembly_streams_config()` -- Apply assembly-specific stream overrides + +### Build Data Loading + +- `gitdata.py` (~13KB) -- `GitData` class for cloning and reading ocp-build-data. Handles git clone, checkout of specific commitish, and loading YAML data files from paths (images/, rpms/). + +- `config/` -- Configuration loading subsystem: + - `config/__init__.py` -- `BuildDataLoader` class for loading group config, releases config, and per-component configs with variable substitution and assembly overrides + - `config/plashet.py` -- Pydantic models for plashet (RPM repository) configuration + - `config/repo.py` -- Pydantic models for repository configuration (`Repo`, `RepoList`, `ContentSet`, `RepoSync`) + +### Execution Utilities + +- `exectools.py` (~31KB) -- Subprocess execution utilities. Provides: + - `cmd_assert()` -- Run command, raise on non-zero exit + - `cmd_gather()` -- Run command, capture stdout/stderr/rc + - `parallel_exec()` -- Parallel execution with thread pool + - `limit_concurrency()` -- Semaphore-based concurrency limiting + - Timer context managers for performance tracking + +### General Utilities + +- `util.py` (~49KB, largest module) -- General-purpose utilities including: + - `isolate_el_version_in_brew_tag()` -- Extract RHEL version from Brew tag + - `deep_merge()` -- Deep dictionary merging + - `convert_remote_git_to_https()` -- Git URL normalization + - Various string, version, and data manipulation helpers + +### Embargo and Build Visibility + +- `build_visibility.py` -- `BuildVisibility` enum (`PUBLIC`, `PRIVATE`) and visibility suffix system. Maps build system (brew/konflux) and visibility to p-flags: + - Brew: `p0` (public), `p1` (private) + - Konflux: `p2` (public), `p3` (private) + - `is_release_embargoed()` -- Check if a release string indicates embargo (defaults to embargoed if unknown) + - `isolate_pflag_in_release()` -- Extract p-flag from release string + +### Git and Source Management + +- `git_helper.py` -- Git operation helpers +- `github_auth.py` (~16KB) -- GitHub App authentication for API access +- `gitlab.py` -- GitLab integration + +### Konflux Integration + +Located in `artcommon/artcommonlib/konflux/`: + +- `konflux_db.py` (~61KB) -- `KonfluxDb` class for querying the Konflux build database (BigQuery-backed). Handles build record queries, caching, and batch operations. +- `konflux_build_record.py` (~17KB) -- `KonfluxBuildRecord` dataclass and `KonfluxBuildOutcome` enum. Represents individual Konflux build records with their metadata. +- `package_rpm_finder.py` -- Utilities for finding RPMs in Konflux builds. + +### Brew Integration + +- `brew.py` -- Shared Brew/Koji utilities including `BuildStates` enum. + +### Other Modules + +- `arch_util.py` -- Architecture name mapping and utilities +- `bigquery.py` -- BigQuery client wrapper (used by Konflux DB) +- `build_util.py` -- Build-related utility functions +- `constants.py` -- Shared constants (URLs, tags, advisory types) +- `dotconfig.py` -- User configuration file management (~/.config/) +- `exceptions.py` -- Common exception classes +- `format_util.py` -- Terminal output formatting (colored printing) +- `jira_config.py` -- Jira server configuration constants +- `lock.py` -- Local locking utilities +- `logutil.py` -- Logging setup and configuration +- `oc_image_info.py` -- Parse `oc image info` output +- `pushd.py` -- `Dir` context manager for directory changes +- `redis.py` -- Redis client wrapper +- `release_util.py` -- Release name and version utilities +- `rhcos.py` -- RHCOS-related utilities +- `rpm_utils.py` -- RPM version comparison and manipulation +- `telemetry.py` -- OpenTelemetry integration +- `variants.py` -- `BuildVariant` enum (`OCP`, `OKD`) + +## Architecture + +artcommonlib follows a layered design: + +1. **Data layer** -- `model.py` (safe dict/list traversal), `gitdata.py` (ocp-build-data loading), `config/` (structured config loading) +2. **Domain layer** -- `assembly.py` (release semantics), `build_visibility.py` (embargo logic), `metadata.py` (component metadata) +3. **Runtime layer** -- `runtime.py` (session management ABC) +4. **Integration layer** -- `konflux/` (Konflux DB), `brew.py` (Koji), `github_auth.py` (GitHub) +5. **Utility layer** -- `exectools.py`, `util.py`, `format_util.py`, `rpm_utils.py` + +## Dependencies + +artcommonlib has minimal external dependencies by design. It is imported by doozer, elliott, and pyartcd. Key external dependencies include: +- `pyyaml` -- YAML parsing +- `pydantic` -- Config model validation (in config/ submodule) +- `google-cloud-bigquery` -- Konflux DB queries +- `tenacity` -- Retry logic (used in consuming tools) + +## Related Components + +- **doozer** -- Extends GroupRuntime, uses Model/Missing, assembly, metadata, exectools +- **elliott** -- Extends GroupRuntime, uses Model/Missing, assembly, build_visibility +- **pyartcd** -- Uses exectools, assembly, logging utilities diff --git a/agentic/design-docs/components/doozer.md b/agentic/design-docs/components/doozer.md new file mode 100644 index 0000000000..48f689785f --- /dev/null +++ b/agentic/design-docs/components/doozer.md @@ -0,0 +1,140 @@ +--- +component: doozer +type: CLI +related: [elliott, pyartcd, artcommon] +--- + +# Doozer + +## Purpose + +Doozer is a CLI tool for managing OCP builds -- both RPMs and container images -- via Brew/OSBS and Konflux. It handles the full lifecycle: rebasing source into distgit, building, scanning, and generating release payloads. + +## Location + +| What | Path | +|------|------| +| Entry point | `doozer/doozerlib/cli/__main__.py` -- `main()` function, calls `cli(obj={})` | +| CLI group definition | `doozer/doozerlib/cli/__init__.py` -- Click group with global options, creates `Runtime` | +| Core runtime | `doozer/doozerlib/runtime.py` -- `Runtime(GroupRuntime)`, the central session object | +| Core logic | `doozer/doozerlib/` -- image.py, distgit.py, brew.py, rpmcfg.py, repos.py | +| CLI commands | `doozer/doozerlib/cli/` -- one module per command group | +| Tests | `doozer/tests/` | +| Package config | `doozer/pyproject.toml` | + +## Responsibilities + +- **Build management** -- Build RPMs and container images via Brew/Koji (`brew.py`) and Konflux (`backend/konflux_image_builder.py`) +- **Distgit management** -- Clone, rebase, and push changes to distgit repositories (`distgit.py`) +- **Source rebasing** -- Pull upstream source, apply downstream patches, update Dockerfiles with correct base images and RPM versions +- **Image scanning** -- FIPS compliance scanning, OSH (OpenSCAP) scanning, source change detection +- **Release payload generation** -- Generate release payloads and assemblies from built images +- **OLM bundle management** -- Rebase and build Operator Lifecycle Manager bundles +- **FBC (File-Based Catalog)** -- Import and manage file-based operator catalogs + +## Architecture + +### CLI Command Groups + +Commands are registered in `doozer/doozerlib/cli/__main__.py`. The main groups: + +**Image operations** (`doozer/doozerlib/cli/images.py`): +- `images:build` -- Build container images via Brew +- `images:rebase` -- Rebase upstream source into distgit +- `images:push` -- Push distgit changes +- `images:clone` -- Clone distgit repos +- `images:list`, `images:print` -- Query image metadata +- `images:foreach` -- Run arbitrary commands on each image +- `images:show-tree`, `images:show-ancestors` -- Visualize image dependency tree + +**Konflux image operations** (`doozer/doozerlib/cli/images_konflux.py`): +- `images:konflux:rebase` -- Rebase for Konflux builds +- `images:konflux:build` -- Build via Konflux + +**OKD image operations** (`doozer/doozerlib/cli/images_okd.py`): +- `images:okd` -- OKD-specific image operations +- `images:okd:prs` -- Manage OKD pull requests + +**Image streams** (`doozer/doozerlib/cli/images_streams.py`): +- `images:streams` -- Manage image streams +- `images:streams:gen-buildconfigs` -- Generate BuildConfig YAML +- `images:streams:mirror` -- Mirror stream images + +**RPM operations** (`doozer/doozerlib/cli/rpms.py`): +- `rpms:build` -- Build RPMs +- `rpms:rebase` -- Rebase RPM sources +- `rpms:rebase-and-build` -- Combined rebase + build +- `rpms:clone`, `rpms:clone-sources` -- Clone RPM distgits/sources +- `rpms:print` -- Query RPM metadata + +**Config operations**: +- `config:*` (`doozer/doozerlib/cli/config.py`) -- Read/print/commit group config, read assemblies/releases +- `config:plashet` (`doozer/doozerlib/cli/config_plashet.py`) -- Generate plashet (RPM repo) from Brew builds +- `config:tag-rpms` (`doozer/doozerlib/cli/config_tag_rpms.py`) -- Tag RPMs in Brew + +**Release operations**: +- `release:gen-payload` (`doozer/doozerlib/cli/release_gen_payload.py`) -- Generate release payload +- `release:gen-assembly` (`doozer/doozerlib/cli/release_gen_assembly.py`) -- Generate assembly definition from nightlies +- `release:calc-upgrade-tests` (`doozer/doozerlib/cli/release_calc_upgrade_tests.py`) -- Calculate upgrade test matrix + +**Scan operations**: +- `scan:sources` (`doozer/doozerlib/cli/scan_sources.py`) -- Detect source changes since last build +- `scan:sources:konflux` (`doozer/doozerlib/cli/scan_sources_konflux.py`) -- Konflux variant +- `scan:fips` (`doozer/doozerlib/cli/scan_fips.py`) -- Scan images for FIPS compliance +- `scan:osh` (`doozer/doozerlib/cli/scan_osh.py`) -- OpenSCAP scanning + +**Other commands**: +- `detect-embargo` (`doozer/doozerlib/cli/detect_embargo.py`) -- Detect embargoed builds +- `olm-bundle` (`doozer/doozerlib/cli/olm_bundle.py`) -- Manage OLM operator bundles +- `fbc` (`doozer/doozerlib/cli/fbc.py`) -- File-Based Catalog operations (`fbc:rebase-and-build`, `fbc:import`) +- `images:health` (`doozer/doozerlib/cli/images_health.py`) -- Check image build health +- `get-nightlies` (`doozer/doozerlib/cli/get_nightlies.py`) -- Fetch nightly build info + +### Runtime Initialization + +The `Runtime.initialize()` method in `doozer/doozerlib/runtime.py` performs: + +1. Creates working directory structure (distgits, sources, brew-logs, flags) +2. Calls `super().initialize()` (GroupRuntime: logging, Konflux DB) +3. Resolves metadata from ocp-build-data via `resolve_metadata()` which creates `GitData` and `BuildDataLoader` +4. Determines assembly type from `releases.yml` +5. Loads group config (with assembly and variant overrides) +6. Sets assembly basis event (locks Brew queries to a point-in-time) +7. Creates `SourceResolver` for upstream source management +8. Loads image and RPM metadata into `image_map` and `rpm_map` +9. Resolves parent/child image relationships and generates build ordering (`generate_image_tree()`) +10. Optionally clones distgit repos + +### Build Ordering + +Images are built in dependency order. `Runtime.generate_image_tree()` builds a tree of parent-child relationships and produces `image_order` -- a flat list where parents always precede children. Cyclic dependencies are detected and rejected during initialization. + +## Interfaces + +**Input:** +- ocp-build-data YAML configs (via `--data-path` or `--group`) +- `--group` (e.g., `openshift-4.17`) -- required for most commands +- `--assembly` -- selects release assembly (default: `test`) +- `--images` / `--rpms` -- filter to specific components +- `--arches` -- target architectures + +**Output:** +- Brew/Konflux builds (submitted via koji/Konflux APIs) +- Distgit commits and pushes +- Release payload definitions +- `record.log` -- structured log of operations performed +- Assembly definitions (YAML) + +## Dependencies + +- **artcommonlib** -- GroupRuntime, Model, assembly, exectools, gitdata, metadata, build_visibility +- **Brew/Koji** -- Build system API (`doozerlib/brew.py`, `KojiWrapper`) +- **Konflux** -- Cloud-native build system (`doozerlib/backend/konflux_image_builder.py`) +- **distgit/rhpkg** -- Red Hat package git repos for RPMs and container builds +- **Click** -- CLI framework + +## Related Components + +- **elliott** -- Consumes doozer build outputs for advisory management +- **pyartcd** -- Orchestrates doozer commands in automated pipelines +- **artcommon** -- Provides shared runtime, model, assembly, and utility code diff --git a/agentic/design-docs/components/elliott.md b/agentic/design-docs/components/elliott.md new file mode 100644 index 0000000000..6b8a921b2e --- /dev/null +++ b/agentic/design-docs/components/elliott.md @@ -0,0 +1,138 @@ +--- +component: elliott +type: CLI +related: [doozer, pyartcd, artcommon] +--- + +# Elliott + +## Purpose + +Elliott is a CLI tool for managing release advisories, errata, and bugs for OCP releases. It interfaces with Red Hat's Errata Tool, Jira, Bugzilla, and Brew to automate the advisory lifecycle -- from creation through bug/build attachment to release verification. + +## Location + +| What | Path | +|------|------| +| Entry point | `elliott/elliottlib/cli/__main__.py` -- `main()` function | +| CLI group definition | `elliott/elliottlib/cli/common.py` -- Click group, creates `Runtime` | +| Core runtime | `elliott/elliottlib/runtime.py` -- `Runtime(GroupRuntime)` | +| Core logic | `elliott/elliottlib/` -- errata.py, errata_async.py, bzutil.py, brew.py | +| CLI commands | `elliott/elliottlib/cli/` -- one module per command | +| Tests | `elliott/tests/` | +| Package config | `elliott/pyproject.toml` | + +## Responsibilities + +- **Advisory lifecycle management** -- Create, modify, change state, and drop advisories +- **Bug finding and sweeping** -- Find bugs matching various criteria and attach them to advisories +- **Build finding and attaching** -- Find Brew/Konflux builds and attach them to advisories +- **CVE flaw management** -- Attach CVE flaws to RHSA advisories +- **Advisory verification** -- Verify attached bugs, operators, CVP tests, payload consistency +- **Konflux release management** -- Create and watch Konflux releases +- **Shipment management** -- Manage release shipments + +## CLI Command Groups + +Commands are registered in `elliott/elliottlib/cli/__main__.py` via `cli.add_command()`. + +### Advisory Operations + +- `create` (`elliott/elliottlib/cli/create_cli.py`) -- Create a new advisory +- `create-placeholder` (`elliott/elliottlib/cli/create_placeholder_cli.py`) -- Create placeholder advisory +- `create-textonly` (`elliott/elliottlib/cli/create_textonly_cli.py`) -- Create text-only advisory +- `advisory-date` (`elliott/elliottlib/cli/advisory_date_cli.py`) -- Set advisory release date +- `advisory-drop` (`elliott/elliottlib/cli/advisory_drop_cli.py`) -- Drop an advisory +- `advisory-images` (`elliott/elliottlib/cli/advisory_images_cli.py`) -- List images on an advisory +- `get` -- Get advisory details (defined inline in `__main__.py`) +- `change-state` (`elliott/elliottlib/cli/change_state_cli.py`) -- Change advisory state + +### Bug Operations + +- `find-bugs` (`elliott/elliottlib/cli/find_bugs_cli.py`) -- General bug finder +- `find-bugs:sweep` (`elliott/elliottlib/cli/find_bugs_sweep_cli.py`) -- Sweep bugs into advisories +- `find-bugs:blocker` (`elliott/elliottlib/cli/find_bugs_blocker_cli.py`) -- Find blocker bugs +- `find-bugs:golang` (`elliott/elliottlib/cli/find_bugs_golang_cli.py`) -- Find Golang-related bugs +- `find-bugs:kernel` (`elliott/elliottlib/cli/find_bugs_kernel_cli.py`) -- Find kernel bugs +- `find-bugs:kernel-clones` (`elliott/elliottlib/cli/find_bugs_kernel_clones_cli.py`) -- Find kernel clone bugs +- `find-bugs:qe` (`elliott/elliottlib/cli/find_bugs_qe_cli.py`) -- Find QE bugs +- `find-bugs:second-fix` (`elliott/elliottlib/cli/find_bugs_second_fix_cli.py`) -- Find second-fix bugs +- `attach-bugs` (`elliott/elliottlib/cli/attach_bugs_cli.py`) -- Attach bugs to an advisory +- `remove-bugs` (`elliott/elliottlib/cli/remove_bugs_cli.py`) -- Remove bugs from an advisory +- `repair-bugs` (`elliott/elliottlib/cli/repair_bugs_cli.py`) -- Repair bug states + +### Build Operations + +- `find-builds` (`elliott/elliottlib/cli/find_builds_cli.py`) -- Find Brew builds for an advisory +- `move-builds` (`elliott/elliottlib/cli/move_builds_cli.py`) -- Move builds between advisories +- `pin-builds` (`elliott/elliottlib/cli/pin_builds_cli.py`) -- Pin builds in assembly definitions +- `remove-builds` (`elliott/elliottlib/cli/remove_builds_cli.py`) -- Remove builds from an advisory +- `tag-builds` (`elliott/elliottlib/cli/tag_builds_cli.py`) -- Tag builds in Brew +- `attach-cve-flaws` (`elliott/elliottlib/cli/attach_cve_flaws_cli.py`) -- Attach CVE flaws to advisories +- `poll-signed` -- Poll for RPM signing status (defined inline in `__main__.py`) + +### Verification + +- `verify-attached-bugs` (`elliott/elliottlib/cli/verify_attached_bugs_cli.py`) -- Verify bugs attached to advisories +- `verify-attached-operators` (`elliott/elliottlib/cli/verify_attached_operators_cli.py`) -- Verify operator builds +- `verify-cvp` (`elliott/elliottlib/cli/verify_cvp_cli.py`) -- Verify CVP test results +- `verify-payload` (`elliott/elliottlib/cli/verify_payload.py`) -- Verify release payload consistency +- `verify-conforma` (`elliott/elliottlib/cli/conforma_cli.py`) -- Verify Conforma test results +- `validate-rhsa` (`elliott/elliottlib/cli/validate_rhsa.py`) -- Validate RHSA advisory completeness + +### Konflux + +- `konflux-release` (`elliott/elliottlib/cli/konflux_release_cli.py`) -- Create Konflux releases +- `konflux-release-watch` (`elliott/elliottlib/cli/konflux_release_watch_cli.py`) -- Watch Konflux release progress + +### Utilities + +- `get-golang-report` (`elliott/elliottlib/cli/get_golang_report_cli.py`) -- Report on Golang versions +- `get-golang-versions` (`elliott/elliottlib/cli/get_golang_versions_cli.py`) -- Get Golang version info +- `get-network-mode` (`elliott/elliottlib/cli/get_network_mode_cli.py`) -- Get network mode for images +- `shipment` (`elliott/elliottlib/cli/shipment_cli.py`) -- Manage release shipments +- `snapshot` (`elliott/elliottlib/cli/snapshot_cli.py`) -- Take advisory/build snapshots +- `tarball-sources` (`elliott/elliottlib/cli/tarball_sources_cli.py`) -- Generate source tarballs +- `rhcos` (`elliott/elliottlib/cli/rhcos_cli.py`) -- RHCOS-related operations +- `find-unconsumed-rpms` (`elliott/elliottlib/cli/find_unconsumed_rpms.py`) -- Find RPMs not consumed by images +- `process-release-from-fbc-bugs` (`elliott/elliottlib/cli/process_release_from_fbc_bugs_cli.py`) -- Process FBC release bugs + +## Architecture + +### Runtime Initialization + +Elliott's `Runtime` extends `GroupRuntime` similarly to doozer. Key differences: +- Default assembly is `stream` (doozer defaults to `test`) +- Supports `--shipment-path` for Konflux release data +- Does not clone distgit repos by default + +### Errata Tool Integration + +Two interfaces exist: +- `elliott/elliottlib/errata.py` -- Synchronous Errata Tool API wrapper (uses `errata_tool` library) +- `elliott/elliottlib/errata_async.py` -- Async replacement (preferred for new code) + +The `Advisory` class in `errata.py` wraps an Errata Tool advisory, providing methods for build attachment, state changes, and queries. + +### Bug Tracking + +- `elliott/elliottlib/bzutil.py` -- Bugzilla and Jira bug query utilities (uses tenacity retry for reliability) +- Bug sweeping queries are configured in ocp-build-data's `bug.yml` + +### Default Advisory Lookup + +The `find_default_advisory()` function in `elliott/elliottlib/cli/common.py` reads default advisory IDs from `group_config.advisories` (set in ocp-build-data's group.yml or releases.yml). The `--use-default-advisory` option allows commands to use these defaults instead of explicit advisory IDs. + +## Dependencies + +- **artcommonlib** -- GroupRuntime, Model, assembly, exectools, build_visibility +- **errata_tool** -- Python library for Errata Tool API +- **Brew/Koji** -- Build system queries (via `elliottlib/brew.py`) +- **Jira/Bugzilla** -- Bug tracking (via `elliottlib/bzutil.py`) +- **Click** -- CLI framework + +## Related Components + +- **doozer** -- Produces the builds that elliott attaches to advisories +- **pyartcd** -- Orchestrates elliott commands in release pipelines +- **artcommon** -- Provides shared runtime, model, assembly, and utility code diff --git a/agentic/design-docs/components/pyartcd.md b/agentic/design-docs/components/pyartcd.md new file mode 100644 index 0000000000..e311acfb93 --- /dev/null +++ b/agentic/design-docs/components/pyartcd.md @@ -0,0 +1,160 @@ +--- +component: pyartcd +type: PipelineOrchestrator +related: [doozer, elliott, artcommon] +--- + +# pyartcd + +## Purpose + +pyartcd (`artcd` command) is the automated release pipeline orchestrator. It sequences doozer and elliott commands into end-to-end pipelines for building, scanning, and releasing OCP. It integrates with Jenkins, Jira, Slack, and UMB messaging to drive the release process. + +## Location + +| What | Path | +|------|------| +| Entry point | `pyartcd/pyartcd/__main__.py` -- `main()` function, calls `cli()` | +| CLI group definition | `pyartcd/pyartcd/cli.py` -- Click group with global options, creates `Runtime` | +| Runtime | `pyartcd/pyartcd/runtime.py` -- `Runtime.from_config_file()` | +| Pipeline modules | `pyartcd/pyartcd/pipelines/` -- one module per pipeline | +| Scheduled pipelines | `pyartcd/pyartcd/pipelines/scheduled/` -- recurring scan/sync jobs | +| Support modules | `pyartcd/pyartcd/` -- jenkins.py, jira_client.py, slack.py, locks.py, etc. | +| Tests | `pyartcd/tests/` | +| Package config | `pyartcd/pyproject.toml` | + +## Responsibilities + +- Orchestrate doozer and elliott via subprocess calls +- Manage release pipelines (build, scan, prepare, promote) +- Integrate with Jenkins for CI/CD triggers +- Create and manage Jira tickets for release tracking +- Send Slack notifications for pipeline status +- Acquire distributed locks (Redis-based) to prevent concurrent conflicting operations +- Schedule recurring scan and sync jobs + +## Key Pipeline Categories + +### Build Pipelines + +- `ocp.py` -- Main OCP image/RPM build pipeline (Brew-based) +- `ocp4_konflux.py` -- OCP build pipeline for Konflux +- `build_sync.py` -- Synchronize built images to mirrors +- `build_sync_multi.py` -- Multi-version build sync +- `build_microshift.py` -- MicroShift build pipeline +- `build_microshift_bootc.py` -- MicroShift bootc build +- `build_rhcos.py` -- RHCOS build pipeline +- `build_plashets.py` -- Generate RPM plashets (repos) +- `build_fbc.py` -- Build File-Based Catalog images +- `build_merged_fbc.py` -- Build merged FBC images +- `build_layered_products.py` -- Build layered product images + +### Scan Pipelines + +- `ocp4_scan.py` -- Scan OCP images for source changes (Brew) +- `ocp4_scan_konflux.py` -- Scan OCP images for source changes (Konflux) +- `scan_fips.py` -- FIPS compliance scanning +- `scan_for_kernel_bugs.py` -- Kernel bug scanning +- `scan_operator.py` -- Operator scanning +- `scan_plashet_rpms.py` -- Scan plashet RPMs +- `brew_scan_osh.py` -- OpenSCAP scanning via Brew +- `layered_products_scan_konflux.py` -- Scan layered products (Konflux) + +### Release Pipelines + +- `prepare_release_konflux.py` -- Prepare a release (create advisories, sweep bugs, find builds) +- `promote.py` -- Promote a release (largest pipeline at ~128KB; handles signing, mirroring, Cincinnati graph updates) +- `release_from_fbc.py` -- Release from File-Based Catalog +- `gen_assembly.py` -- Generate assembly definitions from nightlies + +### OKD Pipelines + +- `okd.py` -- OKD build pipeline +- `okd_images_health.py` -- OKD images health check + +### Utility Pipelines + +- `update_golang.py` -- Update Golang versions across components +- `rebuild.py` -- Rebuild specific images +- `rebuild_golang_rpms.py` -- Rebuild Golang RPMs +- `cleanup_locks.py` -- Clean up stale distributed locks +- `art_notify.py` -- Send ART notifications +- `advisory_drop.py` -- Drop advisories +- `check_bugs.py` -- Check bug status +- `images_health.py` -- Check image build health +- `olm_bundle.py` -- OLM bundle operations (Brew) +- `olm_bundle_konflux.py` -- OLM bundle operations (Konflux) +- `operator_sdk_sync.py` -- Sync Operator SDK +- `review_cvp.py` -- Review CVP test results +- `tarball_sources.py` -- Generate source tarballs +- `tag_rpms.py` -- Tag RPMs in Brew +- `seed_lockfile.py` -- Seed Konflux lockfiles +- `sigstore_sign.py` -- Sigstore signing +- `quay_doomsday_backup.py` -- Backup Quay repositories +- `sync_rhcos_specialized.py` -- Sync RHCOS specialized images +- `fbc_import_from_index.py` -- Import FBC from index + +### Scheduled Pipelines + +Located in `pyartcd/pyartcd/pipelines/scheduled/`: + +- `schedule_ocp4_scan.py` -- Scheduled OCP source change scan +- `schedule_ocp4_scan_konflux.py` -- Scheduled Konflux source change scan +- `schedule_build_sync_multi.py` -- Scheduled multi-version build sync +- `schedule_layered_products_scan.py` -- Scheduled layered products scan +- `schedule_okd_scan.py` -- Scheduled OKD scan +- `schedule_scan_operator.py` -- Scheduled operator scan +- `schedule_scan_plashet_rpms.py` -- Scheduled plashet RPM scan + +## Key Support Modules + +- `jenkins.py` -- Jenkins API client (uses tenacity retry) +- `jira_client.py` -- Jira API client for release tracking tickets (uses tenacity retry) +- `slack.py` -- Slack messaging integration +- `locks.py` -- Redis-based distributed locking (uses tenacity retry) +- `git.py` -- Git operations helper (uses tenacity retry) +- `oc.py` -- OpenShift CLI wrapper (uses tenacity retry) +- `signatory.py` -- Build signing operations (uses tenacity retry) +- `constants.py` -- Pipeline constants and configuration + +## Architecture + +### Pipeline Execution Model + +Each pipeline module typically: + +1. Defines a Click command registered to the `artcd` CLI group +2. Accepts pipeline-specific parameters (group, assembly, version, etc.) +3. Creates pipeline-specific context (Jira tickets, Slack channels) +4. Shells out to doozer and/or elliott commands via subprocess +5. Handles results, sends notifications, updates tracking tickets + +Pipelines use `click_coroutine` (from `pyartcd/pyartcd/cli.py`) to run async pipeline code within Click's synchronous dispatch. + +### Runtime + +pyartcd's `Runtime` (in `pyartcd/pyartcd/runtime.py`) is distinct from doozer/elliott's GroupRuntime subclasses. It loads configuration from `~/.config/artcd.toml` and provides: +- Working directory management +- Dry-run mode +- Configuration for external service credentials (Jenkins, Jira, Slack tokens) + +### Distributed Locking + +`pyartcd/pyartcd/locks.py` implements Redis-based distributed locks to prevent concurrent pipeline runs from conflicting (e.g., two promote pipelines running simultaneously for the same version). + +## Dependencies + +- **doozer** -- Invoked as subprocess for build operations +- **elliott** -- Invoked as subprocess for advisory/bug operations +- **artcommonlib** -- Shared utilities (exectools, assembly, logging) +- **Jenkins** -- CI/CD trigger and status (via jenkins.py) +- **Jira** -- Release tracking tickets (via jira_client.py) +- **Slack** -- Notifications (via slack.py) +- **Redis** -- Distributed locking (via locks.py) +- **Click** -- CLI framework + +## Related Components + +- **doozer** -- Build tool orchestrated by pyartcd +- **elliott** -- Advisory tool orchestrated by pyartcd +- **artcommon** -- Shared library used by pyartcd directly diff --git a/agentic/design-docs/components/validator.md b/agentic/design-docs/components/validator.md new file mode 100644 index 0000000000..d8a2096ee0 --- /dev/null +++ b/agentic/design-docs/components/validator.md @@ -0,0 +1,130 @@ +--- +component: ocp-build-data-validator +type: CLI +related: [artcommon] +--- + +# ocp-build-data-validator + +## Purpose + +Schema validation for ocp-build-data YAML configuration files. Ensures that image, RPM, group, and release definitions conform to expected schemas and that referenced Git sources exist. Runs in CI as a pre-merge gate to prevent invalid configuration from entering ocp-build-data. + +## Location + +| What | Path | +|------|------| +| Entry point | `ocp-build-data-validator/validator/__main__.py` -- `main()` function | +| Core modules | `ocp-build-data-validator/validator/` | +| JSON schemas | `ocp-build-data-validator/validator/json_schemas/` | +| Python schemas | `ocp-build-data-validator/validator/schema/` | +| Package config | `ocp-build-data-validator/pyproject.toml` | + +## Entry Point + +The CLI command is `validate-ocp-build-data`. It accepts one or more file paths as arguments: + +``` +validate-ocp-build-data FILE [FILE ...] +``` + +Options: +- `--single-thread` -- Disable parallel validation (useful for debugging with `code.interact()`) +- `--schema-only` -- Only run schema validations, skip Git source verification +- `--images-dir` -- Path to the ocp-build-data images directory (auto-detected from file paths if not specified) + +By default, validation runs in parallel using `multiprocessing.Pool` with `cpu_count()` workers. Each worker validates files independently via the `validate()` function. + +## Validation Pipeline + +The `validate()` function in `ocp-build-data-validator/validator/__main__.py` runs a multi-stage pipeline for each file: + +### Stage 1: Format Checking + +Module: `ocp-build-data-validator/validator/format.py` + +Parses the file as YAML. If parsing fails, validation stops with an error. + +### Stage 2: Disabled Check + +Files with `mode: disabled` are skipped (via `support.is_disabled()`). + +### Stage 3: Schema Validation + +Module: `ocp-build-data-validator/validator/schema/` + +Validates the parsed YAML against JSON schemas and Python schema definitions. The artifact type is determined from the file path (image, rpm, releases, group). + +JSON schemas in `ocp-build-data-validator/validator/json_schemas/` cover: +- `image_config.schema.json`, `member_image.schema.json` -- Image metadata schemas +- `member_rpm.schema.json` -- RPM metadata schema +- `releases.schema.json`, `release.schema.json` -- Releases configuration schemas +- `streams.schema.json` -- Streams definition schema +- `repos.schema.json` -- Repository configuration schema +- `assembly.schema.json`, `assembly_basis.schema.json`, `assembly_dependencies.schema.json`, `assembly_group_config.schema.json`, `assembly_issues.schema.json` -- Assembly schemas +- `shipment.schema.json` -- Shipment schema +- `build_profiles.schema.json` -- Build profile schemas +- `rhcos.schema.json` -- RHCOS configuration schema +- `scanning.schema.json` -- Scanning configuration schema +- `arch.schema.json`, `arches_dict.schema.json` -- Architecture schemas +- `cachito.schema.json` -- Cachito configuration schema +- `permits.schema.json` -- Assembly permit schemas +- `source_modification.schema.json` -- Source modification schemas + +Python schemas in `ocp-build-data-validator/validator/schema/`: +- `group_schema.py` -- Group configuration validation +- `image_schema.py` -- Image configuration validation +- `rpm_schema.py` -- RPM configuration validation +- `releases_schema.py` -- Releases configuration validation +- `modification_schema.py` -- Source modification validation +- `streams_schema.py` -- Streams configuration validation +- `shipment_schema.py` -- Shipment configuration validation + +### Stage 4: Releases Validation + +Module: `ocp-build-data-validator/validator/releases.py` + +For `releases.yml` files, performs additional validation of release definitions beyond schema checks. + +### Stage 5: Git Source Verification + +Modules: +- `ocp-build-data-validator/validator/github.py` -- Validates GitHub repository references exist and are accessible +- `ocp-build-data-validator/validator/cgit.py` -- Validates CGit (internal) repository references +- `ocp-build-data-validator/validator/distgit.py` -- Validates distgit repository references + +This stage is skipped when `--schema-only` is specified. + +## Support Modules + +- `ocp-build-data-validator/validator/support.py` -- Utility functions: + - `is_disabled()` -- Check if a config file is disabled + - `get_artifact_type()` -- Determine artifact type from file path + - `load_group_config_for()` -- Load the group config associated with a given file + - `fail_validation()` -- Raise validation failure with appropriate exception + +- `ocp-build-data-validator/validator/exceptions.py` -- Exception classes: + - `ValidationFailed` -- Standard validation failure (exit code 1) + - `ValidationFailedWIP` -- Validation failure for WIP items (non-fatal) + +- `ocp-build-data-validator/validator/global_session.py` -- Manages a shared `requests.Session` for HTTP calls across multiprocessing workers + +## Architecture + +The validator is intentionally standalone with minimal coupling to other art-tools components. It validates the data that other tools consume, acting as a gate in the ocp-build-data CI pipeline. + +The validation approach is defensive: each stage runs independently, and failures at any stage produce clear error messages identifying the file and the nature of the problem. + +## Dependencies + +- `pyyaml` -- YAML parsing +- `jsonschema` -- JSON Schema validation +- `requests` -- HTTP requests for Git source verification +- Standard library `multiprocessing` for parallel validation + +The validator does not depend on artcommonlib, doozer, or elliott. This isolation is intentional -- the validator must work independently in CI environments without the full art-tools stack. + +## Related Components + +- **artcommon** -- Consumes the data that the validator validates; the Model/Missing system in artcommonlib relies on well-formed YAML +- **ocp-build-data** -- The external repository whose files this tool validates diff --git a/agentic/design-docs/core-beliefs.md b/agentic/design-docs/core-beliefs.md new file mode 100644 index 0000000000..8e95dc039e --- /dev/null +++ b/agentic/design-docs/core-beliefs.md @@ -0,0 +1,156 @@ +# Core Beliefs and Operating Principles + +This document captures the foundational design decisions and constraints that govern the art-tools codebase. Every contributor and automation agent should internalize these before making changes. + +## Operating Principles + +### 1. ocp-build-data is the single source of truth + +All build metadata -- image configs, RPM configs, group settings, assembly definitions, errata tool config, streams -- lives in the [ocp-build-data](https://github.com/openshift-eng/ocp-build-data) Git repository. Tools clone it at startup via `GitData` (`artcommon/artcommonlib/gitdata.py`) and `BuildDataLoader` (`artcommon/artcommonlib/config/__init__.py`). + +Never hardcode build metadata in tool code. If you need a new configuration knob, add it to the appropriate YAML file in ocp-build-data and read it through the existing loading infrastructure. + +The data path can be overridden via: +- `--data-path` CLI flag +- `DOOZER_DATA_PATH` or `ELLIOTT_DATA_PATH` environment variables + +### 2. Runtime encapsulates session state + +Every CLI session creates a `Runtime` object initialized with `--group` (e.g., `openshift-4.17`). The Runtime owns: + +- The Koji/Brew client session (`shared_koji_client_session`) +- The working directory and all subdirectories (distgits, sources, brew-logs, flags) +- Loaded metadata maps (`image_map`, `rpm_map`, `component_map`) +- Assembly configuration and type +- The group config loaded from ocp-build-data + +The inheritance chain is: + +``` +GroupRuntime (ABC) # artcommon/artcommonlib/runtime.py + -> doozerlib.runtime.Runtime # doozer/doozerlib/runtime.py + -> elliottlib.runtime.Runtime # elliott/elliottlib/runtime.py +``` + +`GroupRuntime` provides initialization of logging, Konflux DB connection, and declares the abstract `group_config` property. Tool-specific Runtime classes extend this with their own initialization logic (`doozerlib.runtime.Runtime.initialize()` handles distgit cloning, image tree generation, assembly basis events, etc.). + +This pattern avoids global state. All session data flows through the Runtime instance, which Click's `pass_runtime` decorator makes available to every command. + +### 3. Assembly defines release boundaries + +The assembly system is defined in `artcommon/artcommonlib/assembly.py`. Assembly types (`AssemblyTypes` enum): + +- **STREAM** -- Continuous development. Default. No basis event, relaxed constraints. +- **STANDARD** -- Named releases (e.g., `4.17.1`). All consistency checks enforced (sibling matching, RPM version alignment, RHCOS consistency). +- **CANDIDATE** -- Release candidate or feature candidate. +- **CUSTOM** -- No constraints enforced. +- **PREVIEW** -- Internal `.next` or preview releases. + +The assembly type controls constraint enforcement throughout the pipeline. STANDARD assemblies enforce all checks via `AssemblyIssueCode` validation. STREAM assemblies skip most checks to allow continuous development. + +Assembly definitions live in `releases.yml` in ocp-build-data. A basis event (Brew event ID or Konflux timestamp) pins the build state for reproducibility. + +### 4. CLI tools are composable + +pyartcd orchestrates releases by shelling out to doozer and elliott commands as subprocesses. This keeps each tool focused and independently testable: + +- **doozer** handles builds (images, RPMs) and distgit management +- **elliott** handles advisories, bugs, and errata +- **pyartcd** sequences them into pipelines + +Pipeline code in `pyartcd/pyartcd/pipelines/` invokes doozer/elliott via `artcommonlib/exectools.py` subprocess utilities. This separation means each tool can be tested, debugged, and run independently. + +### 5. External systems are unreliable + +Brew, Errata Tool, GitHub, Jira, Slack, and other external services can fail transiently. The codebase uses tenacity retry decorators for external calls. Examples found in: + +- `pyartcd/pyartcd/jenkins.py` +- `pyartcd/pyartcd/jira_client.py` +- `pyartcd/pyartcd/locks.py` +- `pyartcd/pyartcd/git.py` +- `pyartcd/pyartcd/signatory.py` +- `pyartcd/pyartcd/oc.py` +- `elliott/elliottlib/errata.py` +- `elliott/elliottlib/rhcos.py` +- `elliott/elliottlib/bzutil.py` +- `doozer/doozerlib/backend/konflux_image_builder.py` +- Multiple pipeline modules in `pyartcd/pyartcd/pipelines/` + +Handle transient failures gracefully. Use `@retry` with appropriate stop/wait strategies rather than bare try/except. + +--- + +## Non-Negotiable Constraints + +1. **Kerberos authentication required for Brew/Errata operations.** The `shared_koji_client_session()` context manager in `doozer/doozerlib/runtime.py` calls `gssapi_login()` on first use. Without a valid Kerberos ticket, Brew operations will fail. + +2. **Assembly constraints MUST be enforced for STANDARD assemblies.** The `AssemblyIssueCode` enum in `artcommon/artcommonlib/assembly.py` defines specific constraint violations (conflicting RPMs, mismatched siblings, outdated RPMs, inconsistent RHCOS). STANDARD assemblies fail if any non-permitted issues are detected. + +3. **Embargo handling must prevent premature disclosure.** The `BuildVisibility` enum in `artcommon/artcommonlib/build_visibility.py` distinguishes PUBLIC vs PRIVATE builds with visibility suffixes (`p0`/`p1` for Brew, `p2`/`p3` for Konflux). The `is_release_embargoed()` function defaults to treating unknown builds as embargoed (safe default). Never bypass this. + +4. **ocp-build-data schema must validate before use.** The `ocp-build-data-validator` component (`ocp-build-data-validator/validator/`) validates YAML files against JSON schemas in `validator/json_schemas/` and Python schemas in `validator/schema/`. This runs in CI before data merges. + +--- + +## Patterns We Use + +### Runtime pattern + +Abstract base in `artcommon/artcommonlib/runtime.py` (`GroupRuntime`), concrete implementations in each tool. Initialized via Click group command, passed to subcommands via `pass_runtime = click.make_pass_decorator(Runtime)`. + +```python +# artcommon/artcommonlib/runtime.py +class GroupRuntime(ABC): + @property + @abstractmethod + def group_config(self): + pass +``` + +### Model/Missing sentinel for safe YAML traversal + +Defined in `artcommon/artcommonlib/model.py`. `Model` wraps dicts, `ListModel` wraps lists. Accessing a non-existent key returns the `Missing` singleton (a `MissingModel` instance) instead of raising `KeyError`. This allows safe chained access: + +```python +value = config.some.nested.key # Returns Missing if any level is absent +if value is Missing: + # handle default +``` + +`Missing` is falsy (`__bool__` returns `False`), so `if config.optional_field:` works naturally. + +### Click CLI groups with pass_runtime + +Each tool defines a Click group as its entry point. The group callback creates the Runtime and stores it in the Click context. Subcommands receive it via `@pass_runtime`: + +- doozer: `doozer/doozerlib/cli/__init__.py` -- `@click.group` creates `Runtime`, stores in `ctx.obj` +- elliott: `elliott/elliottlib/cli/common.py` -- same pattern +- pyartcd: `pyartcd/pyartcd/cli.py` -- uses `Runtime.from_config_file()` + +### Async pipeline execution with click_coroutine + +All three tools define a `click_coroutine` wrapper that bridges Click's synchronous command dispatch with async pipeline code: + +- `doozer/doozerlib/cli/__init__.py` -- `click_coroutine()` +- `elliott/elliottlib/cli/common.py` -- `click_coroutine()` +- `pyartcd/pyartcd/cli.py` -- `click_coroutine()` + +This allows Click commands to be `async def` while maintaining synchronous CLI entry points. + +### Tenacity retry for external calls + +External API calls use `@retry` from tenacity with configurable stop conditions and wait strategies. This is used throughout the codebase for Brew, Errata Tool, GitHub, Jira, Jenkins, and other external service interactions. + +### Metadata classes + +`MetadataBase` in `artcommon/artcommonlib/metadata.py` is the base for `ImageMetadata` and `RPMMetadata`. It loads raw config from ocp-build-data, applies assembly overrides via `assembly_metadata_config()`, and extracts component namespace/name. + +--- + +## Deprecated Patterns + +1. **setup.py** -- Replaced by `pyproject.toml` + hatchling for all packages. Do not add new `setup.py` files. + +2. **Synchronous Errata API calls** -- Being replaced by `elliott/elliottlib/errata_async.py`. New Errata Tool interactions should prefer the async interface. + +3. **Direct OSBS builds** -- Being replaced by Konflux. The Konflux integration lives in `artcommon/artcommonlib/konflux/` and `doozer/doozerlib/backend/konflux_image_builder.py`. New build pipeline work should target Konflux. diff --git a/agentic/design-docs/index.md b/agentic/design-docs/index.md new file mode 100644 index 0000000000..fab860b181 --- /dev/null +++ b/agentic/design-docs/index.md @@ -0,0 +1,24 @@ +# Design Documentation + +## Core + +- [Core Beliefs](./core-beliefs.md) -- Guiding principles and architectural values for art-tools + +## Components + +- [artcommon](./components/artcommon.md) -- Shared library used by doozer, elliott, and pyartcd +- [doozer](./components/doozer.md) -- CLI tool for managing OCP builds (RPMs and container images) +- [elliott](./components/elliott.md) -- CLI tool for managing release advisories, errata, and bugs +- [pyartcd](./components/pyartcd.md) -- Automated release pipeline code +- [ocp-build-data-validator](./components/ocp-build-data-validator.md) -- Schema validator for ocp-build-data + +## When to Add Here + +Add a document to this section when: + +- A new component is added to the monorepo +- Core architectural beliefs are updated or refined +- A component's design evolves enough to warrant documentation beyond inline comments +- You need to explain "why the code is shaped this way" rather than "what the code does" + +For architectural decisions (choosing between alternatives), use an [ADR](../decisions/index.md) instead. diff --git a/agentic/domain/concepts/assembly.md b/agentic/domain/concepts/assembly.md new file mode 100644 index 0000000000..322e5250fc --- /dev/null +++ b/agentic/domain/concepts/assembly.md @@ -0,0 +1,73 @@ +--- +concept: Assembly +type: Pattern +related: + - runtime + - ocp-build-data + - metadata + - brew-koji + - model-missing +--- + +## Definition + +An Assembly is a named configuration that defines how a specific OCP release is composed, what builds are included, and what validation constraints are enforced. Assemblies are defined in the `releases.yml` file within ocp-build-data and support inheritance, allowing child assemblies to layer configuration on top of parent assemblies. The assembly system controls build pinning, RPM consistency checks, and release validation through a combination of types, basis events, and permits. + +## Purpose + +Assemblies exist to manage the complexity of releasing multiple versions of OpenShift simultaneously. The default `stream` assembly represents continuous development with no constraints, while `standard` assemblies (e.g., `4.17.3`) enforce strict consistency checks to ensure release quality. This system allows ART to pin specific builds, override metadata per-release, track validation issues, and support different release types (GA, candidate, preview, custom) all through declarative YAML configuration rather than code changes. + +## Location in Code + +- **Core module:** `artcommon/artcommonlib/assembly.py` -- Contains all assembly logic: type resolution, config merging, inheritance traversal, basis event calculation, metadata overrides, and permit checking. +- **AssemblyTypes enum:** Defined in `artcommon/artcommonlib/assembly.py` with values: + - `STREAM` -- Default. Continuous build, no basis event, minimal constraints. + - `STANDARD` -- All constraints enforced. Used for GA releases (e.g., `4.17.3`). + - `CANDIDATE` -- Release candidate or feature candidate. + - `CUSTOM` -- No constraints enforced. Used for ad-hoc builds. + - `PREVIEW` -- Preview/next releases (internal name: `.next`). +- **AssemblyIssueCode enum:** Classifies validation problems: + - `IMPERMISSIBLE` -- Cannot be permitted under any circumstances. + - `CONFLICTING_INHERITED_DEPENDENCY` -- Override dependency installed at wrong version. + - `CONFLICTING_GROUP_RPM_INSTALLED` -- Different members installed different versions of the same group RPM. + - `MISMATCHED_SIBLINGS` -- Containers from same source built from different commits. + - `OUTDATED_RPMS_IN_STREAM_BUILD` -- Container has different RPM version than assembly specifies. + - `INCONSISTENT_RHCOS_RPMS` -- Arch-specific RHCOS builds installed different RPM versions. + - `MISSING_INHERITED_DEPENDENCY` -- Expected dependency not installed. + - `EMBARGOED_CONTENT` -- Build sync contains embargoed builds. + - `UNSHIPPABLE_KERNEL` -- Kernel has early-kernel-stop-ship tag. + - And others (`MISSING_RHCOS_CONTAINER`, `FAILED_CONSISTENCY_REQUIREMENT`, `FAILED_CROSS_RPM_VERSIONS_REQUIREMENT`, `MISMATCHED_NETWORK_MODE`). +- **AssemblyIssue class:** Encapsulates a validation issue with a message, component name, and code. + +## Lifecycle + +1. **Definition:** Assemblies are defined in `releases.yml` in ocp-build-data under the `releases` key. Each assembly entry has an `assembly` block containing optional fields: `type`, `basis` (with `brew_event`, `time`, or parent `assembly`), `group` overrides, `members` overrides, `streams` overrides, `rhcos` config, `permits`, and `issues`. +2. **Resolution at Runtime:** During `Runtime.initialize()`, the assembly name (from `--assembly` CLI flag, defaulting to `"stream"` for elliott or `"test"` for doozer) is resolved: + - `assembly_type()` determines the `AssemblyTypes` enum value. + - `assembly_basis_event()` computes the basis event (Brew event ID or Konflux timestamp), recursing through inherited assemblies. +3. **Config Merging:** Assembly configuration is merged using `_merger()`, which supports special key suffixes: + - `key!` -- Force-set value (dominant). + - `key?` -- Set only if not already present (default value). + - `key-` -- Remove the key entirely. + - Lists are appended and deduplicated. +4. **Metadata Overrides:** `assembly_metadata_config()` merges per-component overrides from the assembly's `members.images` or `members.rpms` lists onto the base metadata config, respecting inheritance. +5. **Validation:** During release operations, `AssemblyIssue` objects are created for detected problems. `assembly_permits()` checks whether the assembly (or group lifecycle phase) has permits defined that allow specific issue codes to pass. +6. **Component Exclusion:** `assembly_excluded_components()` returns distgit keys marked with `exclude: true` in the assembly definition, respecting inheritance. + +### Key Functions + +- `assembly_type(releases_config, assembly)` -- Returns AssemblyTypes for the named assembly. +- `assembly_basis_event(releases_config, assembly, strict, build_system)` -- Returns the basis event (int for Brew, datetime for Konflux), recursing through inheritance. +- `assembly_group_config(releases_config, assembly, group_config)` -- Merges assembly group overrides onto the base group config. +- `assembly_metadata_config(releases_config, assembly, meta_type, distgit_key, meta_config)` -- Merges per-component assembly overrides onto metadata config. +- `assembly_permits(releases_config, group_config, assembly)` -- Returns permits list, respecting lifecycle phase (`pre-release` uses `prerelease_permits`). +- `assembly_streams_config(releases_config, assembly, streams_config)` -- Merges assembly stream overrides. +- `assembly_excluded_components(releases_config, assembly, meta_type)` -- Returns set of excluded distgit keys. + +## Related Concepts + +- [runtime](runtime.md) -- Runtime resolves the assembly type and basis event during initialization and stores them for use by all commands. +- [ocp-build-data](ocp-build-data.md) -- Assembly definitions live in `releases.yml` within ocp-build-data. +- [metadata](metadata.md) -- Per-component metadata config is overridden by assembly member definitions via `assembly_metadata_config()`. +- [brew-koji](brew-koji.md) -- Assembly basis events constrain Brew queries to a point-in-time snapshot. +- [model-missing](model-missing.md) -- Assembly configs are accessed as Model objects, relying on Missing for safe traversal of undefined keys. diff --git a/agentic/domain/concepts/brew-koji.md b/agentic/domain/concepts/brew-koji.md new file mode 100644 index 0000000000..66b54c53fc --- /dev/null +++ b/agentic/domain/concepts/brew-koji.md @@ -0,0 +1,59 @@ +--- +concept: Brew/Koji +type: ExternalSystem +related: + - runtime + - metadata + - assembly + - distgit + - plashet + - konflux +--- + +## Definition + +Brew is Red Hat's internal instance of the Koji build system, used to build and manage RPM and container image artifacts for OpenShift Container Platform releases. Koji is the upstream open-source build system; Brew adds Red Hat-specific extensions and integrations. art-tools interact with Brew through the `koji` Python client library, using Kerberos (GSSAPI) authentication to submit builds, query build history, manage tags, and retrieve build artifacts. + +## Purpose + +Brew/Koji serves as the authoritative build and artifact management system for OCP releases. It provides the build infrastructure (OSBS for container images, mock for RPMs), the tagging system that controls which builds are candidates for inclusion in a release, and the historical record of all builds. art-tools rely on Brew for triggering builds, finding latest builds for assemblies, verifying build provenance via tags, and assembling RPM repositories (plashets). As Konflux is introduced as a replacement, both systems coexist, with the `--build-system` flag controlling which is used. + +## Location in Code + +- **Doozer Brew utilities:** `doozer/doozerlib/brew.py` -- Contains `watch_task()`, `watch_tasks()`, `get_build_objects()`, `list_archives_by_builds()`, `get_builds_tags()`, and build-related utility functions. Manages the `watch_task_info` dict for tracking active build tasks. +- **Elliott Brew utilities:** `elliott/elliottlib/brew.py` -- Contains `get_tagged_builds()`, `get_latest_builds()`, and brew query utilities focused on advisory operations. +- **Shared Brew types:** `artcommon/artcommonlib/brew.py` -- Contains `BuildStates` enum (BUILDING=0, COMPLETE=1, DELETED=2, FAILED=3, CANCELED=4). +- **Runtime Koji sessions:** Both `doozer/doozerlib/runtime.py` and `elliott/elliottlib/runtime.py` provide: + - `shared_koji_client_session()` -- Returns a context-managed, authenticated `koji.ClientSession`. Uses GSSAPI login. + - `pooled_koji_client_session()` -- Provides session pooling for concurrent access with thread safety via `koji_lock`. + - `session_pool` / `session_pool_available` -- Session pool management dicts. +- **Build status detection:** `doozer/doozerlib/build_status_detector.py` -- Uses Koji sessions to detect build freshness and determine if rebuilds are needed. + +## Lifecycle + +1. **Authentication:** When `shared_koji_client_session()` is first called, it creates a `koji.ClientSession` pointing to Brew's hub URL and authenticates via `session.gssapi_login()`. This requires valid Kerberos credentials. The `--disable-gssapi` flag skips authentication for read-only operations. +2. **Event Constraining:** If an assembly has a basis event, `runtime.brew_event` is set to that event ID. All subsequent Koji queries (e.g., `listTagged`, `listBuilds`) include this event to get a consistent point-in-time view. +3. **Build Queries:** `MetadataBase.get_latest_brew_build()` uses the Koji API to find the latest build: + - Constructs NVR patterns based on component name, version prefix, and assembly suffix. + - Queries `koji_api.listBuilds()` with pattern matching, package ID, and optional `completeBefore` timestamp. + - Respects assembly pinning via `config['is']` which specifies an exact NVR. + - Verifies builds are tagged with expected candidate tags (e.g., `rhaos-4.17-rhel-9-candidate`). +4. **Build Triggering (Doozer):** `DistGitRepo` uses `rhpkg container-build` (for images via OSBS) or `rhpkg build` (for RPMs) to trigger Brew builds. `watch_task()` monitors the Koji task until completion, cancellation, or timeout (controlled by `BREW_BUILD_TIMEOUT`). +5. **Tagging:** Builds are tagged into candidate tags automatically after successful builds. Build tags are verified during latest-build lookups to ensure consistency. + +### Key Concepts + +- **NVR (Name-Version-Release):** The unique identifier for a build, e.g., `openshift-clients-4.17.0-202312151200.p0.g1234567.assembly.stream.el9`. +- **Candidate Tags:** Brew tags like `rhaos-4.17-rhel-9-candidate` that mark builds as potential release candidates. +- **Brew Events:** Monotonically increasing integer IDs representing points in time. Used to query "what was the latest build as of event X?" for reproducible assembly composition. +- **Package:** A Koji package corresponds to a component name. Each package has a history of builds. +- **Task:** A Koji task represents an in-progress build operation. Tasks have states (OPEN, CLOSED, CANCELED, FAILED). + +## Related Concepts + +- [runtime](runtime.md) -- Runtime provides shared Koji client sessions and stores the brew_event constraint. +- [metadata](metadata.md) -- Each Metadata object queries Brew for its latest build via `get_latest_brew_build()`. +- [assembly](assembly.md) -- Assembly basis events constrain Brew queries; assembly `is` config pins specific NVRs. +- [distgit](distgit.md) -- Builds are triggered from distgit repositories via rhpkg, which submits Koji tasks. +- [plashet](plashet.md) -- PlashetBuilder queries Brew tags to assemble RPM repositories. +- [konflux](konflux.md) -- Coexists with Brew as a build system; `--build-system` flag controls which is used. diff --git a/agentic/domain/concepts/distgit.md b/agentic/domain/concepts/distgit.md new file mode 100644 index 0000000000..1860df57ef --- /dev/null +++ b/agentic/domain/concepts/distgit.md @@ -0,0 +1,84 @@ +--- +concept: Distgit +type: Pattern +related: + - runtime + - metadata + - brew-koji + - ocp-build-data + - konflux +--- + +## Definition + +Distgit (distribution git) is the pattern of using internal Git repositories to store the buildable source for RPM and container image components. Each component has a downstream distgit repository that contains the Dockerfile (for images) or spec file (for RPMs) plus any patches, sources, or build configuration needed to produce a build in Brew. Doozer manages the lifecycle of these repositories: rebasing upstream source into the distgit format, pushing changes, and triggering builds. + +## Purpose + +Distgit exists as the bridge between upstream open-source projects and Red Hat's internal build system (Brew/OSBS). Upstream source code cannot be built directly in Brew; it must be transformed into a specific format in a distgit repository that Brew understands. Doozer automates this transformation through the rebase-push-build workflow, ensuring that downstream builds stay synchronized with upstream changes while applying Red Hat-specific patches, configurations, and dependency pinning. + +## Location in Code + +- **Core module:** `doozer/doozerlib/distgit.py` -- One of the largest modules (~3062 lines). Contains: + - `DistGitRepo` -- Base class for distgit repository management. Handles cloning, pushing, branching, and common operations. + - `ImageDistGitRepo` -- Image-specific distgit operations. Handles Dockerfile manipulation, source injection, `content_sets.yml` generation, `container.yaml` updates, and OSBS build triggering. + - `RPMDistGitRepo` -- RPM-specific distgit operations. Handles spec file updates, tarball generation, and RPM build triggering. +- **Helper functions:** `recursive_overwrite()` (rsync-based file copy), `pull_image()` (podman pull with retries), `map_image_name()` (image name remapping). +- **Distgit type registry:** `doozer/doozerlib/metadata.py` defines `DISTGIT_TYPES = {'image': ImageDistGitRepo, 'rpm': RPMDistGitRepo}` mapping meta_type to the correct distgit class. +- **Runtime integration:** `runtime.distgits_dir` is set during `Runtime.initialize()` to `{working_dir}/distgits/`. This is where all distgit repos are cloned. + +## Lifecycle + +### Clone Phase + +1. `DistGitRepo.__init__()` is called with a `Metadata` object and `autoclone=True` (default). +2. `clone()` creates a namespace subdirectory (e.g., `distgits/containers/`) and clones the distgit repo using `rhpkg clone` or direct git commands. +3. The distgit branch is determined from `metadata.branch()` (which may come from config, group config, or the runtime). +4. If the directory already exists and `--upcycle` is set, it does a `git fetch --all && git reset --hard @{upstream}` instead of recloning. + +### Rebase Phase (images:rebase command) + +1. Source is resolved from the upstream repository via `SourceResolver`. +2. `ImageDistGitRepo._run_modifications()` applies source modifications defined in the metadata config. +3. The Dockerfile is updated with correct FROM lines (parent image references), labels, and environment variables. +4. `content_sets.yml` and `container.yaml` are generated/updated for OSBS. +5. Dependencies (RPMs, other images) are pinned to specific versions. +6. Changes are committed to the local distgit clone. + +### Push Phase (images:push command) + +1. `DistGitRepo.push()` pushes the rebased changes to the remote distgit repository. +2. Uses `rhpkg` for authenticated push operations. + +### Build Phase (images:build command) + +1. `ImageDistGitRepo._trigger_build()` uses `rhpkg container-build` to submit an OSBS build task to Brew. +2. `DistGitRepo._watch_tasks()` monitors the Koji task via `brew.watch_task()`. +3. Build logs are captured in `runtime.brew_logs_dir`. +4. Results are recorded in `runtime.record_logger`. + +### Key Attributes (DistGitRepo) + +- `metadata` -- Reference to the parent Metadata object. +- `config` -- Model wrapping the component's config (same as `metadata.config`). +- `distgit_dir` -- Absolute path to the cloned distgit directory. +- `dg_path` -- `pathlib.Path` version of `distgit_dir`. +- `branch` -- The distgit branch (e.g., `rhaos-4.17-rhel-9`). +- `sha` -- Current commit SHA of the distgit repo. +- `source_sha` -- Short SHA of the upstream source commit used in rebase. +- `source_full_sha` -- Full SHA of the upstream source commit. +- `build_status` -- Boolean indicating if the build succeeded. +- `push_status` -- Boolean indicating if the push succeeded. + +### Constants + +- `BASE_IGNORE` -- Files always excluded from rebase: `[".git", ".oit"]`. +- Distgit interaction uses `rhpkg` (Red Hat's enhanced version of `fedpkg`). + +## Related Concepts + +- [runtime](runtime.md) -- Runtime manages `distgits_dir` and provides the working directory structure for distgit clones. +- [metadata](metadata.md) -- Each Metadata object creates a DistGitRepo. The metadata's config drives distgit branch selection, source resolution, and build configuration. +- [brew-koji](brew-koji.md) -- Builds are triggered from distgit via rhpkg, which submits Koji/Brew tasks. Build results are tracked as Koji builds. +- [ocp-build-data](ocp-build-data.md) -- Component YAML files in ocp-build-data define the distgit configuration: branch, namespace, component name, content sets, and build targets. +- [konflux](konflux.md) -- Konflux builds use a different mechanism (build repos and Tekton pipelines) but still reference distgit-like source management through BuildRepo. diff --git a/agentic/domain/concepts/errata-advisories.md b/agentic/domain/concepts/errata-advisories.md new file mode 100644 index 0000000000..8d6c4339b9 --- /dev/null +++ b/agentic/domain/concepts/errata-advisories.md @@ -0,0 +1,74 @@ +--- +concept: Errata/Advisories +type: ExternalSystem +related: + - runtime + - metadata + - brew-koji + - assembly + - ocp-build-data +--- + +## Definition + +The Errata Tool is Red Hat's internal system for managing release advisories (errata). An advisory groups one or more erratum together with associated metadata to track the lifecycle of a software update from creation through QE verification to publication on the CDN. Elliott is the art-tools CLI that automates advisory management, providing commands to create advisories, attach Brew builds, attach bugs, verify advisory contents, and manage advisory state transitions. + +## Purpose + +Errata/advisories exist to formalize the release process for OCP updates. Every shipped OCP release must have properly configured advisories that specify exactly which builds are included, which bugs are fixed, and which security vulnerabilities are addressed. Elliott automates what would otherwise be tedious manual work in the Errata Tool web UI, enabling the ART team to manage hundreds of advisories across multiple OCP versions efficiently. The system ensures that all shipped content goes through proper QE verification and approval workflows. + +## Location in Code + +- **Synchronous API wrapper:** `elliott/elliottlib/errata.py` -- Contains the `Advisory` class (wrapping `errata_tool.Erratum`) and utility functions for interacting with the Errata Tool REST API. Key components: + - `Advisory` class -- Provides `ensure_state()`, `attach_builds()`, `set_cdn_repos()`, `remove_builds()`. + - `get_raw_erratum()` -- Fetches raw erratum data by advisory ID. + - Various query functions for finding advisories, checking states, and manipulating builds. +- **Async API wrapper:** `elliott/elliottlib/errata_async.py` -- `AsyncErrataAPI` class providing async HTTP operations using `aiohttp` with GSSAPI (Kerberos) authentication. Used for high-concurrency advisory operations. +- **Bug tracking:** `elliott/elliottlib/bzutil.py` -- `BugTracker`, `BugzillaBugTracker`, `JIRABugTracker` classes for finding and attaching bugs to advisories. +- **Elliott CLI commands:** `elliott/elliottlib/cli/` -- Each advisory operation has its own CLI module: + - `create_cli.py` -- Create new advisories. + - `find_builds_cli.py` -- Find Brew builds for attachment. + - `attach_bugs_cli.py` -- Attach bugs to advisories. + - `find_bugs_cli.py` -- Find bugs for attachment. + - `attach_cve_flaws_cli.py` -- Attach CVE flaws to security advisories. + - `change_state_cli.py` -- Transition advisory state. + - `verify_cvp_cli.py` -- Verify Container Verification Pipeline results. +- **Constants:** `elliott/elliottlib/constants.py` -- Defines `errata_url`, `errata_states`, advisory type constants, and product version mappings. +- **Errata tool config:** Loaded from `erratatool.yml` in ocp-build-data via `Runtime.get_errata_config()`. + +## Lifecycle + +### Advisory Lifecycle + +1. **Creation:** `elliott create` creates a new advisory in the Errata Tool. Advisory types: + - **RHBA** (Red Hat Bug Advisory) -- Bug fix updates. + - **RHSA** (Red Hat Security Advisory) -- Security updates. Requires CVE information. + - **RHEA** (Red Hat Enhancement Advisory) -- New features/enhancements. +2. **Build Attachment:** `elliott find-builds` and `elliott attach-builds` find qualifying Brew builds and attach them to the advisory. Builds must be from the correct candidate tags and match the assembly's constraints. +3. **Bug Attachment:** `elliott find-bugs` discovers JIRA/Bugzilla bugs that should be associated with the advisory. Bugs are linked to the advisory to document what fixes are included. +4. **CDN Configuration:** CDN repositories are configured to specify where the advisory's content will be published. +5. **QE Verification:** The advisory moves to QE state for testing. Automated checks (CVP - Container Verification Pipeline) verify container images. +6. **State Transitions:** Advisories progress through states (NEW_FILES -> QE -> REL_PREP -> PUSH_READY -> IN_PUSH -> SHIPPED_LIVE). `elliott change-state` automates these transitions. +7. **Publication:** Final state transition pushes content to the CDN, making it available to customers. + +### Authentication + +All Errata Tool API interactions require Kerberos (GSSAPI) authentication: +- Synchronous: Uses `requests_gssapi.HTTPSPNEGOAuth` or `requests_kerberos.HTTPKerberosAuth`. +- Async: Uses `gssapi.SecurityContext` to generate Negotiate tokens manually. + +### Advisory Configuration in ocp-build-data + +The `erratatool.yml` file in ocp-build-data and the `advisories` section of `group.yml` define: +- Product and product version mappings. +- Default advisory IDs for the group. +- Errata tool URL and API endpoints. +- Per-advisory-type configuration. + +## Related Concepts + +- [runtime](runtime.md) -- Elliott's Runtime provides `get_errata_config()` and `get_default_advisories()` from ocp-build-data configuration. +- [metadata](metadata.md) -- Metadata objects are used to find builds for advisory attachment by resolving component names and querying Brew. +- [brew-koji](brew-koji.md) -- Builds attached to advisories come from Brew. Advisory operations query Brew to find qualifying builds by tag and NVR pattern. +- [assembly](assembly.md) -- Assembly configuration determines which builds are eligible for a specific release advisory. +- [ocp-build-data](ocp-build-data.md) -- Advisory configuration, product versions, and default advisory IDs are defined in ocp-build-data. diff --git a/agentic/domain/concepts/konflux.md b/agentic/domain/concepts/konflux.md new file mode 100644 index 0000000000..1ee2029045 --- /dev/null +++ b/agentic/domain/concepts/konflux.md @@ -0,0 +1,89 @@ +--- +concept: Konflux +type: ExternalSystem +related: + - runtime + - metadata + - brew-koji + - distgit + - assembly +--- + +## Definition + +Konflux is the next-generation build system for OpenShift, based on Tekton pipelines running on Kubernetes. It is actively replacing the legacy OSBS/Brew build pipeline for container image builds. Konflux provides a cloud-native CI/CD approach where builds are defined as PipelineRun resources, build records are stored in BigQuery, and enterprise contract (EC) verification ensures supply chain security. Both Brew and Konflux coexist during the migration period, with the `--build-system` flag controlling which system art-tools use. + +## Purpose + +Konflux exists to modernize the OCP build infrastructure. The legacy OSBS/Brew pipeline has scaling and maintenance challenges, and Konflux addresses these by providing Tekton-based pipelines, improved supply chain security via enterprise contracts, better build reproducibility, and a more cloud-native architecture. For art-tools, Konflux introduces a parallel build path that can eventually fully replace Brew for container image builds while maintaining compatibility with existing assembly and metadata patterns. + +## Location in Code + +### Doozer (Build Orchestration) + +- **KonfluxClient:** `doozer/doozerlib/backend/konflux_client.py` (~67KB) -- The main client for interacting with the Konflux Kubernetes API. Handles: + - PipelineRun creation and management via the Kubernetes dynamic client. + - Application and Component resource management in Konflux namespaces. + - Git authentication secret management (transient secrets per invocation). + - Enterprise Contract (EC) verification pipeline triggering. + - Label-based PipelineRun tracking for the current doozer invocation. +- **KonfluxImageBuilder:** `doozer/doozerlib/backend/konflux_image_builder.py` -- Orchestrates the image build process through Konflux: + - Creates BuildRepo instances with rebased source. + - Submits PipelineRun resources to Konflux. + - Monitors build progress via KonfluxWatcher. + - Records build results to KonfluxDb (BigQuery). + - Handles NVR computation, architecture-specific builds, and build record creation. +- **KonfluxFbcBuilder:** `doozer/doozerlib/backend/konflux_fbc.py` -- Handles file-based catalog (FBC) builds in Konflux for OLM operator bundles and catalogs. +- **BuildRepo:** `doozer/doozerlib/backend/build_repo.py` -- Manages the Git repository that Konflux builds from. Unlike traditional distgit, BuildRepo is a transient repo created per build with the rebased source. +- **KonfluxRebaser:** `doozer/doozerlib/backend/rebaser.py` -- Prepares source for Konflux builds by rebasing into the BuildRepo format. +- **KonfluxWatcher:** `doozer/doozerlib/backend/konflux_watcher.py` -- Watches PipelineRun resources via Kubernetes watch API for completion. +- **PipelineRunInfo:** `doozer/doozerlib/backend/pipelinerun_utils.py` -- Utilities for parsing PipelineRun status and results. + +### Artcommon (Shared Infrastructure) + +- **KonfluxDb:** `artcommon/artcommonlib/konflux/konflux_db.py` (~60KB) -- BigQuery-backed database for build records. Provides: + - `get_latest_build()` -- Find latest build by name, group, assembly, outcome. + - `get_build_record_by_nvr()` -- Look up specific build by NVR. + - `add_build()` -- Insert a new build record. + - `BuildCache` -- In-memory cache with per-group indexing and exponential search window expansion. + - Two-tier caching: small columns (no installed_rpms/installed_packages) and all columns. +- **KonfluxBuildRecord:** `artcommon/artcommonlib/konflux/konflux_build_record.py` -- Data model for Konflux build records. Key fields: name, group, version, release, assembly, el_target, arches, art_job_url, pipeline_url, installed_rpms, etc. +- **Enums:** + - `KonfluxBuildOutcome` -- `SUCCESS`, `FAILURE`, `PENDING`, `TIMEOUT`, `CANCELLED`. + - `KonfluxECStatus` -- `PASSED`, `FAILED`, `NOT_APPLICABLE`. + - `ArtifactType` -- `RPM`, `IMAGE`. + - `Engine` -- `KONFLUX`, `BREW`. +- **KonfluxRecord (base):** Base class for all Konflux records with common fields and build ID generation. + +### Runtime Integration + +- `GroupRuntime.initialize_konflux_db()` in `artcommon/artcommonlib/runtime.py` initializes the KonfluxDb connection. +- `runtime.build_system` (`"brew"` or `"konflux"`) controls which build system is queried. +- `runtime.konflux_db` provides access to the BigQuery-backed build record database. +- `MetadataBase.get_latest_konflux_build()` in `artcommon/artcommonlib/metadata.py` queries Konflux DB for the latest build matching assembly, outcome, and el_target constraints. + +## Lifecycle + +1. **Initialization:** `GroupRuntime.initialize()` creates a `KonfluxDb` instance connected to BigQuery. The `--build-system` flag sets `runtime.build_system` to `"konflux"`. +2. **Source Preparation:** `KonfluxRebaser` prepares a `BuildRepo` with the rebased source. Unlike distgit, this is a transient Git repository created per build. +3. **Build Submission:** `KonfluxImageBuilder` creates a PipelineRun resource in the Konflux Kubernetes namespace via `KonfluxClient`. The PipelineRun references the BuildRepo source and build configuration. +4. **Build Monitoring:** `KonfluxWatcher` monitors the PipelineRun for completion using the Kubernetes watch API. Progress is tracked via labels including a unique `doozer-watch-id` per invocation. +5. **EC Verification:** After build completion, `KonfluxClient` can trigger enterprise contract verification to check supply chain compliance. +6. **Record Storage:** Build results are stored as `KonfluxBuildRecord` entries in BigQuery via `KonfluxDb.add_build()`. +7. **Build Queries:** `MetadataBase.get_latest_konflux_build()` queries BigQuery for builds, using assembly-aware logic parallel to Brew queries: checking pinned builds (`is` config), falling back from assembly-specific to stream to true latest. + +### Key Differences from Brew + +- **Build infrastructure:** Tekton pipelines on Kubernetes vs. OSBS/mock on Brew workers. +- **Build records:** BigQuery vs. Koji database. +- **Authentication:** Kubernetes service accounts and GitHub App tokens vs. Kerberos. +- **Basis events:** Timestamps (`basis.time`) instead of Brew event IDs (`basis.brew_event`). +- **Source management:** Transient BuildRepo vs. persistent distgit repositories. + +## Related Concepts + +- [runtime](runtime.md) -- Runtime initializes KonfluxDb and stores the `build_system` flag that controls Brew vs. Konflux usage. +- [metadata](metadata.md) -- MetadataBase provides `get_latest_konflux_build()` for querying Konflux build records. +- [brew-koji](brew-koji.md) -- Coexists with Konflux; both build systems are supported simultaneously during migration. +- [distgit](distgit.md) -- Konflux uses BuildRepo instead of traditional distgit, but the rebase concept is similar. +- [assembly](assembly.md) -- Assembly basis events for Konflux use timestamps (`basis.time`) instead of Brew event IDs. diff --git a/agentic/domain/concepts/metadata.md b/agentic/domain/concepts/metadata.md new file mode 100644 index 0000000000..73d37cf8f2 --- /dev/null +++ b/agentic/domain/concepts/metadata.md @@ -0,0 +1,80 @@ +--- +concept: Metadata +type: Class +related: + - runtime + - ocp-build-data + - assembly + - distgit + - brew-koji + - konflux + - model-missing +--- + +## Definition + +Metadata is the class hierarchy that represents a single buildable component (container image or RPM) in the OCP release process. Each Metadata instance is loaded from a YAML file in ocp-build-data and encapsulates the component's configuration, distgit information, build targets, and build history. The hierarchy spans artcommon, doozer, and elliott with specialized subclasses for images and RPMs in each tool. + +## Purpose + +Metadata exists to provide a unified interface for querying and manipulating individual build components. It abstracts the details of how component configuration is loaded (from YAML files with assembly overrides), how builds are discovered (from Brew or Konflux), and how distgit repositories are managed. Every operation that touches a specific component -- rebasing, building, finding latest builds, checking scan results -- goes through its Metadata object. + +## Location in Code + +### Class Hierarchy + +``` +MetadataBase (artcommon/artcommonlib/metadata.py) +| ++-- Metadata (doozer/doozerlib/metadata.py) +| |-- ImageMetadata (doozer/doozerlib/image.py) +| |-- RPMMetadata (doozer/doozerlib/rpmcfg.py) +| ++-- Metadata (elliott/elliottlib/metadata.py) + |-- ImageMetadata (elliott/elliottlib/imagecfg.py) + |-- RPMMetadata (elliott/elliottlib/rpmcfg.py) +``` + +- **MetadataBase:** `artcommon/artcommonlib/metadata.py` -- The shared base class. Handles config loading from `DataObj`, assembly metadata config merging via `assembly_metadata_config()`, component name extraction, branch determination, build target resolution, and latest build queries for both Brew and Konflux. +- **Doozer Metadata:** `doozer/doozerlib/metadata.py` -- Extends MetadataBase with distgit management (`DistGitRepo`), source resolution, rebuild hint detection, scan-sources logic, Cgit feed parsing, and upstream commit tracking. +- **Doozer ImageMetadata:** `doozer/doozerlib/image.py` -- Image-specific operations: Dockerfile management, parent image resolution, golang builder version extraction, build ordering dependencies, and payload/release classification. +- **Doozer RPMMetadata:** `doozer/doozerlib/rpmcfg.py` -- RPM-specific operations: spec file management, source cloning, version/release tracking, and kube environment variable extraction. +- **Elliott Metadata:** `elliott/elliottlib/metadata.py` -- Thin wrapper that simply inherits from MetadataBase (12 lines). +- **Elliott ImageMetadata:** `elliott/elliottlib/imagecfg.py` -- Provides image properties: `image_name`, `is_release`, `is_payload`, `is_olm_operator`, `base_only`. +- **Elliott RPMMetadata:** `elliott/elliottlib/rpmcfg.py` -- Provides `rpm_name` from config (7 lines). + +## Lifecycle + +1. **Loading:** During `Runtime.initialize()`, YAML files from `ocp-build-data/images/` and `ocp-build-data/rpms/` are loaded via `GitData.load_data()` into `DataObj` instances. Each `DataObj` is passed to the appropriate Metadata constructor. +2. **Config Assembly:** In `MetadataBase.__init__()`, the raw YAML config is wrapped in a `Model` (`self.raw_config`), then merged with assembly overrides via `assembly_metadata_config()` to produce `self.config`. This means `self.config` reflects both the base YAML and any assembly-specific overrides. +3. **Registration:** The constructed Metadata is added to `runtime.image_map` or `runtime.rpm_map`, keyed by `distgit_key`. The component name is also registered in `runtime.component_map`. +4. **Usage:** Commands access metadata through the runtime maps. Common operations: + - `meta.get_latest_build()` -- Find latest build in Brew or Konflux (delegates based on `runtime.build_system`). + - `meta.get_component_name()` -- Get the Brew/Konflux component name (e.g., `"openshift-enterprise-cli-container"`). + - `meta.branch()` -- Get the distgit branch for the component. + - `meta.determine_targets()` -- Get Brew build targets. + - `meta.config.some.nested.key` -- Access any config value safely via Model. +5. **Distgit Operations (Doozer):** Doozer's Metadata creates a `DistGitRepo` for cloning and managing the downstream distribution git repository. +6. **Destruction:** Metadata objects are not explicitly destroyed; they live as long as the Runtime. + +### Key Attributes + +- `distgit_key` -- Unique identifier derived from the YAML filename (e.g., `"openshift-enterprise-cli"`). +- `name` -- Base name without differentiator suffixes (split on `.`). +- `config` -- `Model` object with assembly-merged configuration. +- `raw_config` -- `Model` object with unmerged base configuration. +- `meta_type` -- `"image"` or `"rpm"`. +- `namespace` -- `"containers"`, `"rpms"`, or `"apbs"`. +- `mode` -- `"enabled"`, `"disabled"`, or `"wip"`. +- `enabled` -- Boolean, True when mode is `"enabled"`. +- `qualified_key` -- `"{namespace}/{distgit_key}"` (e.g., `"containers/openshift-enterprise-cli"`). + +## Related Concepts + +- [runtime](runtime.md) -- Runtime creates Metadata objects during initialization and stores them in image_map/rpm_map. +- [ocp-build-data](ocp-build-data.md) -- Each Metadata instance is loaded from a YAML file in ocp-build-data. +- [assembly](assembly.md) -- Assembly member overrides are merged into metadata config via `assembly_metadata_config()`. +- [distgit](distgit.md) -- Doozer's Metadata creates DistGitRepo instances for downstream repository management. +- [brew-koji](brew-koji.md) -- `get_latest_brew_build()` queries Koji for the component's latest build using the component name and assembly. +- [konflux](konflux.md) -- `get_latest_konflux_build()` queries Konflux DB for the component's latest build. +- [model-missing](model-missing.md) -- `self.config` is a Model object, enabling safe traversal of arbitrarily nested configuration. diff --git a/agentic/domain/concepts/model-missing.md b/agentic/domain/concepts/model-missing.md new file mode 100644 index 0000000000..8b20dd2af2 --- /dev/null +++ b/agentic/domain/concepts/model-missing.md @@ -0,0 +1,90 @@ +--- +concept: Model/Missing +type: Pattern +related: + - runtime + - metadata + - ocp-build-data + - assembly +--- + +## Definition + +Model and Missing are the safe YAML configuration traversal pattern used throughout art-tools. `Model` is a dict subclass that wraps Python dictionaries loaded from YAML files, returning a `Missing` singleton instead of raising `KeyError` when accessing undefined keys. `Missing` (an instance of `MissingModel`) is falsy and returns itself for any further attribute access, enabling arbitrarily deep safe traversal of configuration trees without try/except blocks or explicit existence checks at each level. + +## Purpose + +Model/Missing exists to eliminate defensive programming boilerplate when accessing deeply nested YAML configuration. Since ocp-build-data YAML files have varying structures (some components define certain keys, others do not), every config access would otherwise require nested `if key in dict` or try/except chains. With Model, code can write `if config.some.deeply.nested.optional.key:` and it will safely evaluate to False if any level in the chain is undefined, without throwing exceptions. This pattern is used pervasively across doozer, elliott, and artcommon. + +## Location in Code + +- **Module:** `artcommon/artcommonlib/model.py` -- Contains all four classes: + - `Model(dict)` -- Dict subclass. `__getattr__` returns `Missing` for undefined keys, or wraps nested dicts/lists in Model/ListModel on access. `primitive()` recursively converts back to raw Python dicts. + - `MissingModel(dict)` -- Empty dict subclass that is the Missing sentinel. `__bool__` returns False. `__getattr__` and `__getitem__` return `self` (enabling chained access). `__setattr__`, `__setitem__`, `__delattr__`, `__delitem__` all raise `ModelException` to prevent accidental mutation. + - `Missing` -- The singleton `MissingModel()` instance, imported throughout the codebase as `from artcommonlib.model import Missing`. + - `ListModel(list)` -- List subclass that wraps list elements in Model/ListModel on access. `primitive()` recursively converts back to raw Python lists. + - `ModelException` -- Exception class for invalid operations on models. + - `to_model_or_val(v)` -- Helper that wraps dicts as Model, lists as ListModel, and returns scalars unchanged. + +## Lifecycle + +1. **Creation:** Model objects are created whenever YAML data is loaded from ocp-build-data: + - `MetadataBase.__init__()`: `self.raw_config = Model(data_obj.data)` and `self.config = assembly_metadata_config(...)` (which returns a Model). + - `Runtime`: `self.group_config = Model(group_config_dict)`. + - `Assembly functions`: `assembly_config_struct()` returns Model for dict defaults, ListModel for list defaults. +2. **Traversal:** Throughout tool code, config values are accessed via attribute syntax: + ```python + # Safe -- never throws, even if any intermediate key is missing + if self.config.distgit.branch is not Missing: + branch = self.config.distgit.branch + + # Boolean check -- Missing is falsy + if self.config.content.source: + source = self.config.content.source + + # Deep access with chaining + network_mode = self.config.konflux.get("network_mode") + ``` +3. **Comparison:** `Missing` is compared using `is not Missing` (identity check) or truthiness (`if config.key:`). Since Missing is a singleton, identity checks are reliable. +4. **Conversion back:** `model.primitive()` recursively converts Model/ListModel back to plain dict/list, used when data needs to be serialized, merged, or passed to APIs that expect plain dicts. +5. **Mutation:** Model supports `__setattr__` and `__setitem__` for modification. MissingModel (Missing) raises `ModelException` on any mutation attempt, preventing accidental writes to undefined branches. + +### Behavior Summary + +| Operation | Model (key exists) | Model (key missing) | Missing | +|---|---|---|---| +| `obj.key` | Returns value (wrapped) | Returns `Missing` | Returns `Missing` | +| `obj[key]` | Returns value (wrapped) | Returns `Missing` | Returns `Missing` | +| `bool(obj)` | True (non-empty dict) | True (non-empty dict) | **False** | +| `obj.key = val` | Sets value | Sets value | **Raises ModelException** | +| `str(obj)` | Dict repr | Dict repr | `"(MissingModel)"` | +| `obj.primitive()` | Returns raw dict | Returns raw dict | N/A (dict methods) | + +### Common Patterns in Codebase + +```python +# Pattern 1: Check before use +if self.config.content.source is not Missing: + # use self.config.content.source + +# Pattern 2: Truthiness check (works because Missing is falsy) +if self.config.distgit.branch: + branch = self.config.distgit.branch + +# Pattern 3: Fallback with or +mode = self.config.get('mode', 'enabled') + +# Pattern 4: Iteration over ListModel +for entry in self.config.targets: + process(entry) + +# Pattern 5: Convert to dict for serialization +raw = self.config.primitive() +``` + +## Related Concepts + +- [runtime](runtime.md) -- Runtime's `group_config` and `releases_config` are Model objects, and Missing is used extensively in config access throughout initialization. +- [metadata](metadata.md) -- Every Metadata's `config` and `raw_config` are Model objects. All component configuration access uses the Model/Missing pattern. +- [ocp-build-data](ocp-build-data.md) -- All YAML data loaded from ocp-build-data is wrapped in Model objects for safe traversal. +- [assembly](assembly.md) -- Assembly merging functions (`_merger`, `assembly_config_struct`, etc.) operate on and return Model objects. Missing is used to detect undefined assembly fields. diff --git a/agentic/domain/concepts/ocp-build-data.md b/agentic/domain/concepts/ocp-build-data.md new file mode 100644 index 0000000000..84b0d23d9e --- /dev/null +++ b/agentic/domain/concepts/ocp-build-data.md @@ -0,0 +1,73 @@ +--- +concept: ocp-build-data +type: ExternalSystem +related: + - runtime + - assembly + - metadata + - model-missing + - plashet +--- + +## Definition + +ocp-build-data is an external Git repository (`github.com/openshift-eng/ocp-build-data`) that serves as the single source of truth for all build and release configuration in the OpenShift Container Platform release process. It contains YAML configuration files organized by version groups (e.g., `openshift-4.17`, `openshift-4.18`), including per-component image and RPM metadata, group-level configuration, streams definitions, assembly/release definitions, and errata tool configuration. All art-tools (doozer, elliott, pyartcd) clone and read from this repository at runtime. + +## Purpose + +ocp-build-data exists to decouple build/release configuration from tool logic. By storing all component definitions, version constraints, assembly configurations, and release parameters in a separate data repository, the ART team can modify build behavior without changing tool code. This also enables per-version configuration (different groups for different OCP versions), reproducible builds (assembly basis events pin configuration to a point in time), and collaborative editing of release parameters through standard Git workflows. + +## Location in Code + +- **GitData class:** `artcommon/artcommonlib/gitdata.py` -- Handles cloning the data repo, checking out the correct branch/commitish, and loading YAML files. The `GitData.clone_data()` method handles both remote Git URLs and local directory paths. `GitData.load_data()` reads YAML files from a subdirectory, supports filtering, exclusion, key-based selection, and variable substitution. +- **DataObj class:** `artcommon/artcommonlib/gitdata.py` -- Wraps a single loaded YAML file with its key, path, and parsed data. Supports reload and save operations using `ruamel.yaml` to preserve formatting. +- **BuildDataLoader:** `artcommon/artcommonlib/config/` -- Higher-level loader that uses GitData to load group config, releases config, and other configuration files with assembly merging support. +- **Runtime integration:** Both `doozer/doozerlib/runtime.py` and `elliott/elliottlib/runtime.py` call `resolve_metadata()` during initialization, which clones ocp-build-data and populates metadata maps. +- **CLI flags:** `--data-path` overrides the default repo URL. Environment variables `DOOZER_DATA_PATH` and `ELLIOTT_DATA_PATH` also work. `--group` specifies the branch to check out (e.g., `openshift-4.17`). Append `@commitish` to pin a specific commit. + +## Lifecycle + +1. **Clone:** When a Runtime initializes, it clones ocp-build-data into the working directory using `GitData.clone_data()`. The branch is determined by the `--group` parameter. If a local clone already exists and is up to date, cloning is skipped. +2. **Load Group Config:** `group.yml` is loaded from the group's root directory, parsed into a `Model` object, and optionally merged with assembly-level group overrides via `assembly_group_config()`. +3. **Load Releases Config:** `releases.yml` is loaded to resolve assembly definitions, basis events, and member overrides. +4. **Load Metadata:** YAML files from the `images/` and `rpms/` subdirectories are loaded via `GitData.load_data()`. Each file becomes a `DataObj` whose data is wrapped in a `Model` and passed to `ImageMetadata` or `RPMMetadata` constructors. Files can be filtered by mode (`enabled`, `disabled`, `wip`) and by explicit include/exclude lists. +5. **Load Additional Config:** Streams config (`streams.yml`), errata tool config (`erratatool.yml`), and other files are loaded as needed, with variable substitution (e.g., `{MAJOR}`, `{MINOR}`, `{runtime_assembly}`). +6. **Read-only Usage:** Throughout a tool run, the cloned data is read but never modified by the tools (except rare admin operations). Changes to ocp-build-data are made through separate PRs to the repository. + +### Repository Structure + +``` +openshift-4.17/ # Group branch + group.yml # Group-level configuration (arches, branch, vars, repos, etc.) + streams.yml # Stream image definitions (builders, base images) + releases.yml # Assembly/release definitions + erratatool.yml # Errata tool configuration + images/ # One YAML file per container image component + openshift-enterprise-cli.yml + ose-node.yml + ... + rpms/ # One YAML file per RPM component + openshift-clients.yml + cri-o.yml + ... +``` + +### Key Configuration Fields (group.yml) + +- `name` -- Group name (must match branch name) +- `vars` -- Template variables (MAJOR, MINOR, etc.) +- `arches` -- Supported architectures +- `branch` -- Default distgit branch +- `advisories` -- Default advisory IDs +- `freeze_automation` -- Build freeze state +- `software_lifecycle.phase` -- Release phase (e.g., `pre-release`) +- `repos` / `all_repos` -- RPM repository definitions +- `plashet` -- Plashet RPM composition configuration + +## Related Concepts + +- [runtime](runtime.md) -- Runtime clones ocp-build-data during initialization and loads all configuration from it. +- [assembly](assembly.md) -- Assembly definitions live in `releases.yml` within ocp-build-data. +- [metadata](metadata.md) -- Each image/RPM YAML file in ocp-build-data becomes a Metadata object. +- [model-missing](model-missing.md) -- All loaded YAML data is wrapped in Model objects for safe traversal. +- [plashet](plashet.md) -- Plashet configuration references Brew tags and RPM repos defined in ocp-build-data. diff --git a/agentic/domain/concepts/plashet.md b/agentic/domain/concepts/plashet.md new file mode 100644 index 0000000000..6d8f9e5aa0 --- /dev/null +++ b/agentic/domain/concepts/plashet.md @@ -0,0 +1,62 @@ +--- +concept: Plashet +type: Pattern +related: + - brew-koji + - runtime + - metadata + - assembly + - ocp-build-data +--- + +## Definition + +A plashet is a composed RPM repository assembled from builds in specific Brew tags, used to provide a consistent set of RPM dependencies for container image builds. The PlashetBuilder queries Brew for the latest (or assembly-specific) RPM builds from configured tags, downloads the RPMs, and creates a local yum/dnf repository. This ensures that all container images in a release are built against the same RPM versions, preventing dependency skew. + +## Purpose + +Plashets exist to solve the RPM consistency problem in container image builds. Without plashets, container image builds would pull RPMs from live repositories where package versions change constantly, leading to non-reproducible builds and potential version mismatches between sibling images. By composing a fixed RPM repository from specific Brew-tagged builds, plashets ensure that every image in a release sees the same RPM versions, which is critical for release consistency and assembly validation. + +## Location in Code + +- **PlashetBuilder:** `doozer/doozerlib/plashet.py` -- Core logic for assembling RPM repositories from Brew tags. Key methods: + - `from_tag(tag, inherit, assembly, event, only)` -- Returns RPM builds from a specified Brew tag. When assembly is set, uses `find_latest_builds()` to find assembly-appropriate builds rather than just the tag-latest. Caches build lookups internally. + - `_get_builds(ids_or_nvrs)` -- Batch-fetches build dicts from Brew with internal caching to avoid redundant queries. + - `_cache_build(build)` -- Saves build dicts indexed by both build_id and NVR. +- **CLI interface:** `doozer/doozerlib/cli/config_plashet.py` (~64KB) -- The `config:plashet` CLI command that orchestrates plashet creation. Handles: + - Signed/unsigned RPM selection based on signing key configuration. + - RPM repository layout with `createrepo`. + - Signing status verification and retry logic. + - Multi-arch repository composition. + - Concerns tracking for plashet viability (stored in `plashet.yml`). + - OpenShift-aware NVR comparison via `compare_nvr_openshift_aware()`. +- **PlashetConfig:** `artcommon/artcommonlib/config/plashet.py` -- Pydantic model for plashet configuration from ocp-build-data's group config. +- **Runtime integration:** `Runtime.get_plashet_config()` in `doozer/doozerlib/runtime.py` loads plashet configuration from `group_config.plashet`. +- **Repos integration:** `doozer/doozerlib/repos.py` uses plashet configuration along with repo definitions to set up the complete RPM repository landscape for builds. + +## Lifecycle + +1. **Configuration:** Plashet parameters are defined in ocp-build-data's `group.yml` under the `plashet` key and in repo definitions. This includes which Brew tags to pull from, signing key requirements, and RPM inclusion/exclusion rules. +2. **Tag Querying:** `PlashetBuilder.from_tag()` queries Brew for RPM builds in the specified tag: + - Without assembly: Uses `koji_api.listTagged(tag, latest=True)` to get the tag-latest builds. + - With assembly: Uses `koji_api.listTagged(tag, latest=False)` to get ALL tagged builds, then `find_latest_builds()` filters for the assembly-appropriate subset. + - Results are keyed by component name. +3. **Build Selection:** Multiple tags may contribute builds. When the same component appears in multiple tags, the plashet logic resolves conflicts using version comparison, with OpenShift-version-aware sorting when applicable. +4. **Signing Verification:** For production plashets, RPMs must be signed with an approved key (e.g., `fd431d51`). The CLI checks signing status and can wait/retry for unsigned RPMs to become signed. +5. **Repository Creation:** Downloaded RPMs are organized into a directory structure and `createrepo` is run to generate yum/dnf repository metadata. +6. **Consumption:** The resulting plashet repository URL/path is injected into container image builds (via repo files in the distgit) so that `yum/dnf install` commands resolve against the composed repository. + +### Key Concepts + +- **Brew Tags:** Named labels in Koji that group builds together (e.g., `rhaos-4.17-rhel-9-candidate`). The `inherit` flag controls whether parent tags are also searched. +- **Signing Keys:** RPMs must be signed with Red Hat's GPG key before shipping. Plashets can enforce this requirement. +- **Assembly-aware selection:** When an assembly is specified, plashet uses `find_latest_builds()` which understands assembly naming conventions in the NVR release field. +- **Plashet concerns:** During assembly, potential issues (e.g., unsigned RPMs, version conflicts) are collected in `plashet_concerns` and written to `plashet.yml` for audit. + +## Related Concepts + +- [brew-koji](brew-koji.md) -- PlashetBuilder queries Brew tags and retrieves builds from Koji to compose repositories. +- [runtime](runtime.md) -- Runtime loads plashet configuration and provides Koji sessions for PlashetBuilder. +- [metadata](metadata.md) -- RPMMetadata objects correspond to the RPM components whose builds appear in plashets. +- [assembly](assembly.md) -- Assembly configuration controls which builds are selected for plashet composition. +- [ocp-build-data](ocp-build-data.md) -- Plashet configuration (tags, signing keys, repo definitions) is defined in ocp-build-data. diff --git a/agentic/domain/concepts/runtime.md b/agentic/domain/concepts/runtime.md new file mode 100644 index 0000000000..51318faef1 --- /dev/null +++ b/agentic/domain/concepts/runtime.md @@ -0,0 +1,66 @@ +--- +concept: Runtime +type: Class +related: + - assembly + - ocp-build-data + - metadata + - brew-koji + - distgit + - model-missing +--- + +## Definition + +Runtime is the central orchestration object in art-tools that initializes and manages the entire context for a tool invocation. It is responsible for cloning ocp-build-data, loading group configuration, creating Metadata objects, and providing shared resources such as Koji client sessions, working directories, and assembly configuration. Three distinct Runtime implementations exist across the codebase: doozer's full-featured Runtime, elliott's advisory-focused Runtime, and pyartcd's lightweight pipeline Runtime. + +## Purpose + +Runtime exists to serve as the single entry point for all configuration, state, and resource management during a tool run. Every CLI command in doozer and elliott receives an initialized Runtime, which guarantees that ocp-build-data has been cloned, group config has been loaded, metadata has been parsed, and build system connections are available. Without Runtime, each command would need to independently handle data loading, authentication, and state management. + +## Location in Code + +- **GroupRuntime (ABC):** `artcommon/artcommonlib/runtime.py` -- Abstract base class defining the interface. Initializes logging and Konflux DB connection. Declares the abstract `group_config` property. +- **Doozer Runtime:** `doozer/doozerlib/runtime.py` -- Full implementation (~1400 lines). Inherits from GroupRuntime. Manages image_map, rpm_map, component_map, source_resolver, distgits_dir, repos, streams, build ordering, and freeze automation state. +- **Elliott Runtime:** `elliott/elliottlib/runtime.py` -- Advisory-focused implementation (~575 lines). Inherits from GroupRuntime. Manages image_map, rpm_map, bug trackers, shipment metadata, and assembly basis events. +- **pyartcd Runtime:** `pyartcd/pyartcd/runtime.py` -- Lightweight pipeline runtime (~60 lines). Does NOT inherit from GroupRuntime. Holds config dict, working_dir, dry_run flag, and factory methods for Jira/Slack/Mail clients. +- **pyartcd GroupRuntime:** `pyartcd/pyartcd/runtime.py` -- A separate class in the same file that DOES inherit from `artcommonlib.runtime.GroupRuntime`. Used when pipeline code needs group configuration. + +## Lifecycle + +1. **Construction:** Created by Click CLI decorators (`@click.pass_context`) with keyword arguments from CLI options (`--group`, `--working-dir`, `--assembly`, `--data-path`, etc.). +2. **Initialization:** `runtime.initialize()` is called at the start of each command. This: + - Creates/validates the working directory (or a temp directory). + - Calls `super().initialize()` to set up logging and Konflux DB. + - Clones ocp-build-data via `resolve_metadata()` which uses `BuildDataLoader` and `GitData`. + - Loads `group.yml` into `self.group_config` (a `Model` object). + - Loads `releases.yml` into `self.releases_config`. + - Determines `assembly_type` and `assembly_basis_event`. + - Loads image and RPM metadata YAML files, creating `ImageMetadata` and `RPMMetadata` objects populating `self.image_map` and `self.rpm_map`. + - (Doozer only) Sets up source_resolver, repos, streams, distgits_dir, record_logger, arches, and freeze_automation. +3. **Usage:** Commands access runtime attributes throughout their execution -- `runtime.image_map`, `runtime.shared_koji_client_session()`, `runtime.group_config`, `runtime.assembly`, `runtime.brew_event`, etc. +4. **Destruction:** On exit, if a temp working directory was created, it is cleaned up via `atexit.register(remove_tmp_working_dir, self)`. + +### Key Attributes (Doozer Runtime) + +- `group` -- Group name string (e.g., `"openshift-4.17"`) +- `assembly` -- Assembly name string (e.g., `"stream"`, `"4.17.3"`) +- `group_config` -- `Model` wrapping the loaded `group.yml` +- `releases_config` -- `Model` wrapping the loaded `releases.yml` +- `image_map` -- `Dict[str, ImageMetadata]` keyed by distgit_key +- `rpm_map` -- `Dict[str, RPMMetadata]` keyed by distgit_key +- `brew_event` -- Optional Koji event ID constraining all Brew queries +- `assembly_basis_event` -- Event derived from assembly's basis config +- `distgits_dir` -- Path to the directory where distgit repos are cloned +- `working_dir` -- Root working directory for the invocation +- `build_system` -- `"brew"` or `"konflux"` +- `konflux_db` -- `KonfluxDb` instance (from GroupRuntime) + +## Related Concepts + +- [assembly](assembly.md) -- Runtime determines and stores the assembly type and basis event during initialization. +- [ocp-build-data](ocp-build-data.md) -- Runtime clones this repository and loads all configuration from it. +- [metadata](metadata.md) -- Runtime creates and holds all ImageMetadata and RPMMetadata objects. +- [brew-koji](brew-koji.md) -- Runtime provides shared Koji client sessions via `shared_koji_client_session()`. +- [distgit](distgit.md) -- Runtime manages `distgits_dir` where distgit repositories are cloned. +- [model-missing](model-missing.md) -- Runtime's `group_config` and `releases_config` are Model objects enabling safe traversal. diff --git a/agentic/domain/glossary.md b/agentic/domain/glossary.md new file mode 100644 index 0000000000..f6601bea08 --- /dev/null +++ b/agentic/domain/glossary.md @@ -0,0 +1,184 @@ +# Glossary - art-tools + +> Canonical definitions for domain terminology. Alphabetical order. Link to detailed docs where available. + +## A + +### Advisory +**Definition**: An errata advisory (RHSA, RHBA, or RHEA) that ships bug fixes, security patches, or enhancements to customers via the Red Hat CDN. +**Type**: ExternalSystem concept +**Related**: [Errata/Advisories](./concepts/errata-advisories.md), [Elliott](../design-docs/components/elliott.md) + +### Assembly +**Definition**: A named release or checkpoint that controls how builds are pinned and validated. Five types: STREAM, STANDARD, CANDIDATE, CUSTOM, PREVIEW. +**Type**: Pattern +**Related**: [Assembly concept](./concepts/assembly.md), [Runtime](./concepts/runtime.md) +**Details**: [./concepts/assembly.md](./concepts/assembly.md) + +## B + +### Brew +**Definition**: Red Hat's instance of the Koji build system. Used to build RPMs and container images for OCP. +**Type**: ExternalSystem +**Related**: [Brew/Koji](./concepts/brew-koji.md), [Konflux](./concepts/konflux.md) +**Details**: [./concepts/brew-koji.md](./concepts/brew-koji.md) + +### Build Sync +**Definition**: The process of synchronizing builds for an assembly, ensuring all required images and RPMs are built and tagged. Implemented in `pyartcd/pipelines/build_sync.py`. +**Type**: Workflow step +**Related**: [Release Preparation](./workflows/release-preparation.md) + +## C + +### Candidate Tag +**Definition**: A Brew tag (e.g. `rhaos-4.17-rhel-9-candidate`) used to collect builds that are candidates for inclusion in a release. +**Type**: Brew concept +**Related**: [Brew/Koji](./concepts/brew-koji.md) + +## D + +### Distgit +**Definition**: Distribution git repositories -- internal source repos where doozer pushes rebased sources before triggering builds in Brew. +**Type**: Pattern +**Related**: [Distgit concept](./concepts/distgit.md), [Doozer](../design-docs/components/doozer.md) +**Details**: [./concepts/distgit.md](./concepts/distgit.md) + +## E + +### Embargo +**Definition**: A restriction on disclosing security fix details before a coordinated release date. Embargoed builds are marked with PRIVATE visibility. +**Type**: Security concept +**Related**: [SECURITY.md](../SECURITY.md), `artcommonlib/build_visibility.py` + +### Errata Tool +**Definition**: Red Hat's internal system for managing release advisories. Elliott interacts with it to create, populate, and ship advisories. +**Type**: ExternalSystem +**Related**: [Errata/Advisories](./concepts/errata-advisories.md) + +## F + +### FBC (File-Based Catalog) +**Definition**: A file-based operator catalog format used for OLM (Operator Lifecycle Manager) operators in OCP. Replaces the deprecated SQLite-based catalog. +**Type**: DataFormat +**Related**: `doozerlib/backend/konflux_fbc.py`, `doozerlib/cli/fbc.py` + +## G + +### Group +**Definition**: An OCP version target (e.g. `openshift-4.17`) that maps to a branch in ocp-build-data. Specified via `--group` on all CLI commands. +**Type**: Configuration concept +**Related**: [Runtime](./concepts/runtime.md), [ocp-build-data](./concepts/ocp-build-data.md) + +## I + +### ImageMetadata +**Definition**: A metadata object representing a single container image component, loaded from ocp-build-data YAML by Runtime. +**Type**: Class +**Related**: [Metadata](./concepts/metadata.md), `doozerlib/image.py` + +## K + +### Konflux +**Definition**: A Tekton-based build system replacing OSBS/Brew for OCP builds. Active migration in progress. +**Type**: ExternalSystem +**Related**: [Konflux concept](./concepts/konflux.md), [ADR-0003](../decisions/adr-0003-dual-build-system.md) +**Details**: [./concepts/konflux.md](./concepts/konflux.md) + +## M + +### Metadata +**Definition**: The base abstraction for a buildable component (image or RPM). Wraps ocp-build-data YAML config and provides build lifecycle operations. +**Type**: Class +**Related**: [Metadata concept](./concepts/metadata.md), ImageMetadata, RPMMetadata + +### Model/Missing +**Definition**: Safe YAML config traversal pattern. `Model` wraps dicts; accessing undefined keys returns the `Missing` singleton (falsy) instead of raising KeyError. +**Type**: Pattern +**Related**: [Model/Missing concept](./concepts/model-missing.md), `artcommonlib/model.py` +**Details**: [./concepts/model-missing.md](./concepts/model-missing.md) + +## N + +### NVR +**Definition**: Name-Version-Release -- the standard identifier for a Brew/Koji build (e.g. `openshift-clients-4.17.0-202401151205.p0.g1234567.assembly.stream.el9`). +**Type**: Identifier format +**Related**: [Brew/Koji](./concepts/brew-koji.md) + +## O + +### ocp-build-data +**Definition**: External git repository containing YAML configuration for all OCP build components -- group configs, image metadata, RPM metadata, assembly definitions, and streams. +**Type**: ExternalSystem +**Related**: [ocp-build-data concept](./concepts/ocp-build-data.md), [Runtime](./concepts/runtime.md) +**Details**: [./concepts/ocp-build-data.md](./concepts/ocp-build-data.md) + +### OLM Bundle +**Definition**: An operator bundle for the Operator Lifecycle Manager, packaging operator metadata and manifests for distribution. +**Type**: DataFormat +**Related**: `doozerlib/cli/olm_bundle.py` + +## P + +### Payload +**Definition**: A release payload containing all container images for an OCP release, used for installation and upgrades. +**Type**: Release artifact +**Related**: `doozerlib/cli/release_gen_payload.py`, [Release Preparation](./workflows/release-preparation.md) + +### Pipeline +**Definition**: An automated workflow in pyartcd that orchestrates doozer and elliott commands for release operations (builds, scans, promotions). +**Type**: Module +**Related**: [pyartcd](../design-docs/components/pyartcd.md), `pyartcd/pipelines/` + +### Plashet +**Definition**: RPM repository composition tool. Assembles RPM repos from Brew tags to provide consistent dependency sets for container image builds. +**Type**: Pattern +**Related**: [Plashet concept](./concepts/plashet.md), `doozerlib/plashet.py` +**Details**: [./concepts/plashet.md](./concepts/plashet.md) + +## R + +### Rebase +**Definition**: The process of updating a distgit repository with new upstream source, modifying Dockerfiles, and updating dependencies before triggering a build. +**Type**: Workflow step +**Related**: [Distgit](./concepts/distgit.md), [Image Build Lifecycle](./workflows/image-build-lifecycle.md) + +### RHCOS +**Definition**: Red Hat CoreOS -- the immutable operating system used by OCP nodes. Managed separately but tracked by art-tools for release coordination. +**Type**: ExternalSystem concept +**Related**: `doozerlib/rhcos.py`, `elliottlib/rhcos.py` + +### RPMMetadata +**Definition**: A metadata object representing a single RPM component, loaded from ocp-build-data YAML by Runtime. +**Type**: Class +**Related**: [Metadata](./concepts/metadata.md), `doozerlib/rpmcfg.py` + +### Runtime +**Definition**: Central orchestration object for CLI sessions. Initialized with `--group`, clones ocp-build-data, loads config, creates Metadata objects. Holds Koji sessions and working directory. +**Type**: Class +**Related**: [Runtime concept](./concepts/runtime.md), [ADR-0002](../decisions/adr-0002-runtime-pattern.md) +**Details**: [./concepts/runtime.md](./concepts/runtime.md) + +## S + +### Shipment +**Definition**: A structured representation of a release delivery, modeled in `elliottlib/shipment_model.py` using Pydantic. +**Type**: DataFormat +**Related**: `elliottlib/shipment_model.py`, `elliottlib/shipment_utils.py` + +### Source Resolution +**Definition**: The process of determining the upstream source repository and commit for a component, performed by doozer during rebase. +**Type**: Workflow step +**Related**: `doozerlib/source_resolver.py`, [Distgit](./concepts/distgit.md) + +### Stream +**Definition**: The default assembly type representing continuous development builds with no pinned constraints. +**Type**: Assembly type +**Related**: [Assembly](./concepts/assembly.md) + +--- + +## See Also + +- [Domain concepts](./concepts/) -- Detailed explanations +- [Workflows](./workflows/) -- How concepts interact +- [ARCHITECTURE.md](../../ARCHITECTURE.md) -- System structure diff --git a/agentic/domain/index.md b/agentic/domain/index.md new file mode 100644 index 0000000000..454d65c0ad --- /dev/null +++ b/agentic/domain/index.md @@ -0,0 +1,30 @@ +# Domain Documentation + +Domain documentation captures the specialized knowledge needed to work effectively with OCP release tooling. + +## Glossary + +- [Glossary](./glossary.md) -- Definitions of domain-specific terms used across art-tools + +## Concepts + +Explanations of key domain concepts: + +- [Assemblies](./concepts/assemblies.md) +- [Advisories and Errata](./concepts/advisories-errata.md) +- [Brew and Koji](./concepts/brew-koji.md) +- [Distgit](./concepts/distgit.md) +- [Groups and Releases](./concepts/groups-releases.md) +- [Konflux](./concepts/konflux.md) +- [Metadata System](./concepts/metadata-system.md) +- [OCP Build Data](./concepts/ocp-build-data.md) +- [RPM and Image Builds](./concepts/rpm-image-builds.md) +- [Streams](./concepts/streams.md) + +## Workflows + +End-to-end descriptions of common operational workflows: + +- [Release Workflow](./workflows/release-workflow.md) +- [Build Workflow](./workflows/build-workflow.md) +- [Advisory Workflow](./workflows/advisory-workflow.md) diff --git a/agentic/domain/workflows/advisory-management.md b/agentic/domain/workflows/advisory-management.md new file mode 100644 index 0000000000..3d4a8fb500 --- /dev/null +++ b/agentic/domain/workflows/advisory-management.md @@ -0,0 +1,175 @@ +--- +workflow: AdvisoryManagement +components: [elliott] +related_concepts: [Errata/Advisories, Brew/Koji, Assembly] +--- + +# Advisory Management Workflow + +## Overview + +How release advisories are created, populated with bugs and builds, verified, and shipped using elliott. Advisories are managed through the Red Hat Errata Tool and follow a defined state machine from creation to CDN push. + +## Steps + +### 1. Create Advisory + +Elliott provides three creation commands: + +#### `elliott create` (`elliott/elliottlib/cli/create_cli.py`) + +Creates a new advisory with full boilerplate. Options: +- `--type` (required): `RHBA` (bug fix) or `RHEA` (enhancement). RHSA (security) advisories are handled separately. +- `--art-advisory-key` (required): Key into `erratatool.yml` in ocp-build-data for boilerplate text. +- `--date`: Release date (format: YYYY-Mon-DD). +- `--assigned-to`, `--manager`, `--package-owner`: Email addresses for advisory ownership. +- `--batch-id`: Batch ID for grouping advisories. +- `--with-placeholder`: Creates a placeholder bug and attaches it. +- `--with-liveid / --no-liveid`: Whether to request a Live ID. +- `--yes`: Non-interactive mode. + +#### `elliott create-placeholder` (`elliott/elliottlib/cli/create_placeholder_cli.py`) + +Creates a placeholder bug for an advisory when no real bugs are ready to attach. + +#### `elliott create-textonly` (`elliott/elliottlib/cli/create_textonly_cli.py`) + +Creates a text-only advisory (no builds, just text content). + +### 2. Find and Attach Bugs + +#### `elliott find-bugs sweep` (`elliott/elliottlib/cli/find_bugs_sweep_cli.py`) + +Sweeps qualified bugs into advisories: +- Default policy: sweeps only `VERIFIED` status bugs (per ART policy document). +- `--include-status`: Add additional statuses to sweep. +- `--exclude-status`: Remove statuses from sweep. +- `--add` / `-a`: Attach found bugs to a specific advisory ID. +- `--cve-only`: Only sweep CVE tracker bugs. +- Uses `FindBugsSweep(FindBugsMode)` class which delegates to `BugTracker.search()`. + +#### `elliott find-bugs blocker` + +Finds blocker bugs that could block a release. Used in pre-release checks. + +#### `elliott attach-bugs` (`elliott/elliottlib/cli/attach_bugs_cli.py`) + +Manually attach specific bugs to an advisory. + +### 3. Find and Attach Builds + +#### `elliott find-builds` (`elliott/elliottlib/cli/find_builds_cli.py`) + +Finds builds and optionally attaches them to an advisory: +- `--attach` / `-a`: Attach builds to ADVISORY. +- `--build` / `-b`: Add specific NVR or build ID. +- `--builds-file` / `-f`: Read builds from file or STDIN. +- `--use-default-advisory`: Use the default advisory configured in group.yml. +- Supports both Brew builds and Konflux builds (via `KonfluxBuildRecord`). +- Uses `BuildFinder` (`elliott/elliottlib/build_finder.py`) for build discovery. +- Validates builds against assembly constraints (pinned NVRs, excluded components). + +### 4. Attach CVE Flaws + +#### `elliott attach-cve-flaws` (`elliott/elliottlib/cli/attach_cve_flaws_cli.py`) + +For security advisories (RHSA): +- Finds CVE flaw bugs associated with the release. +- Attaches them to the advisory. +- Required for security advisories to pass errata validation. + +### 5. Verify + +Multiple verification commands ensure advisory correctness before shipping: + +#### `elliott verify-attached-bugs` (`elliott/elliottlib/cli/verify_attached_bugs_cli.py`) + +Validates that all attached bugs meet release criteria (correct status, target release, etc.). + +#### `elliott verify-attached-operators` (`elliott/elliottlib/cli/verify_attached_operators_cli.py`) + +Validates operator bundle builds attached to the advisory. + +#### `elliott verify-cvp` (`elliott/elliottlib/cli/verify_cvp_cli.py`) + +Checks Container Verification Pipeline results for attached builds. + +#### `elliott verify-payload` (`elliott/elliottlib/cli/verify_payload.py`) + +Validates that the release payload matches expected content. + +#### `elliott validate-rhsa` (`elliott/elliottlib/cli/validate_rhsa.py`) + +Validates RHSA advisory content (CVE descriptions, CVSS scores, etc.). + +### 6. Change State + +#### `elliott change-state` (`elliott/elliottlib/cli/change_state_cli.py`) + +Moves an advisory through the state machine: +- `--state`: Target state (`NEW_FILES`, `QE`, `REL_PREP`). +- `--from`: Only change state if currently in this state (guard). +- `--advisory` / `-a`: Specific advisory ID. +- `--default-advisories`: Change state of all group default advisories. +- `--noop` / `--dry-run`: Check without changing. + +Advisory state machine: +``` +NEW_FILES --> QE --> REL_PREP --> PUSH_READY --> IN_PUSH --> SHIPPED_LIVE +``` + +State transition requirements: +- `NEW_FILES -> QE`: Bugs or JIRA issues must be attached. +- `QE -> REL_PREP`: QE testing must be complete. +- `REL_PREP -> PUSH_READY`: All verification checks pass. + +### 7. Ship + +Advisory is pushed to CDN by the Errata Tool. This is triggered externally (not by elliott directly). The `push_cdn_stage` function in `elliott/elliottlib/errata.py` can push to CDN staging for testing. + +## Advisory Types + +| Type | Description | Use Case | +|------|-------------|----------| +| RHBA | Bug Fix Advisory | Standard OCP releases with bug fixes | +| RHEA | Enhancement Advisory | OCP releases with new features | +| RHSA | Security Advisory | Releases containing CVE fixes | + +## State Diagram + +``` + +------------+ +---------+ +----------+ +------------+ +---------+ +-------------+ + | NEW_FILES |---->| QE |---->| REL_PREP |---->| PUSH_READY |---->| IN_PUSH |---->| SHIPPED_LIVE| + +------------+ +---------+ +----------+ +------------+ +---------+ +-------------+ + | | | + v v v + (attach (QE tests (final + bugs & complete) verification) + builds) +``` + +## Integration with Assemblies + +When using `--assembly`, elliott reads the assembly definition from `releases.yml`: +- Default advisories are stored in the assembly's group config under `advisories`. +- `--use-default-advisory` resolves to the advisory ID from group config. +- Assembly basis events constrain Brew queries for build discovery. +- Assembly-specific bug inclusion/exclusion rules apply during sweep. + +## Key Source Files + +- `elliott/elliottlib/cli/create_cli.py` -- advisory creation +- `elliott/elliottlib/cli/find_builds_cli.py` -- build discovery and attachment +- `elliott/elliottlib/cli/find_bugs_sweep_cli.py` -- bug sweep +- `elliott/elliottlib/cli/attach_bugs_cli.py` -- manual bug attachment +- `elliott/elliottlib/cli/attach_cve_flaws_cli.py` -- CVE flaw attachment +- `elliott/elliottlib/cli/change_state_cli.py` -- state transitions +- `elliott/elliottlib/cli/verify_attached_bugs_cli.py` -- bug verification +- `elliott/elliottlib/cli/verify_attached_operators_cli.py` -- operator verification +- `elliott/elliottlib/cli/verify_cvp_cli.py` -- CVP verification +- `elliott/elliottlib/cli/verify_payload.py` -- payload verification +- `elliott/elliottlib/errata.py` -- Errata Tool API (sync) +- `elliott/elliottlib/errata_async.py` -- Errata Tool API (async) +- `elliott/elliottlib/build_finder.py` -- build discovery logic +- `elliott/elliottlib/bzutil.py` -- Bugzilla/JIRA bug tracking +- `elliott/elliottlib/runtime.py` -- Elliott Runtime diff --git a/agentic/domain/workflows/image-build-lifecycle.md b/agentic/domain/workflows/image-build-lifecycle.md new file mode 100644 index 0000000000..d296377a80 --- /dev/null +++ b/agentic/domain/workflows/image-build-lifecycle.md @@ -0,0 +1,140 @@ +--- +workflow: ImageBuildLifecycle +components: [doozer, artcommon] +related_concepts: [Distgit, Brew/Koji, Konflux, Metadata, Runtime] +--- + +# Image Build Lifecycle Workflow + +## Overview + +How an OCP container image goes from source change to tagged build. The lifecycle differs depending on the build system (Brew/OSBS or Konflux), but both follow the same logical steps: source resolution, rebase, push, build, tag, inclusion. + +## Steps + +### 1. Source Resolution + +Identify the upstream source repository and commit for each image. + +- `ImageMetadata` (defined in `doozer/doozerlib/image.py`) reads its config from ocp-build-data (e.g. `images/openshift-apiserver.yml`). +- The config specifies the upstream source repo alias and branch. +- `SourceResolver` (`doozer/doozerlib/source_resolver.py`) clones or updates the upstream source to the correct commit. +- For assemblies with a basis event, source commits are pinned to the state at that event. + +### 2. Rebase + +Update the downstream build repository with new source and Dockerfile modifications. + +#### Brew/OSBS Path (distgit) + +`DistGitRepo` (`doozer/doozerlib/distgit.py`): +- Clones the distgit repo for the image (e.g. `containers/openshift-apiserver`). +- Copies upstream source into distgit, applying ignore rules (`BASE_IGNORE = [".git", ".oit"]`). +- Modifies the Dockerfile: updates FROM lines, injects labels, applies source modifications from `SourceModifierFactory`. +- Sets the release field with visibility suffix (p0/p1) and assembly info. + +#### Konflux Path (build repo) + +`KonfluxRebaser` (`doozer/doozerlib/backend/rebaser.py`): +- Manages a build source repository (`BuildRepo` in `doozer/doozerlib/backend/build_repo.py`). +- Performs similar Dockerfile modifications as distgit: updates FROM lines, injects labels, applies modifications. +- Generates lockfiles for RPM dependencies (`ArtifactLockfileGenerator`, `RPMLockfileGenerator`). +- Detects package managers (cachito-enabled builds). +- Sets visibility suffix (p2/p3) for Konflux builds. +- Handles CPE labels for product identification. + +### 3. Push + +Commit and push the rebased content to the build repository. + +#### Brew/OSBS Path + +- `DistGitRepo.push()` commits and pushes to the distgit branch (e.g. `rhaos-4.17-rhel-9`). +- Uses `rhpkg` commands for distgit interaction. + +#### Konflux Path + +- `BuildRepo` pushes to a branch in the Konflux build source repository on GitHub. +- Uses GitHub App token authentication for push access. + +### 4. Build + +Trigger the actual container build. + +#### Brew/OSBS Path + +- `OSBS2Builder` (`doozer/doozerlib/osbs2_builder.py`) triggers a build via `rhpkg container-build`. +- Build runs in Brew (Koji) using OSBS. +- Build is tracked by Brew task ID. + +#### Konflux Path + +- `KonfluxImageBuilder` (`doozer/doozerlib/backend/konflux_image_builder.py`) creates a Tekton PipelineRun via the Kubernetes API. +- `KonfluxClient` (`doozer/doozerlib/backend/konflux_client.py`) manages the PipelineRun lifecycle: + - Creates transient git-auth secrets (prefixed `art-transient-pipeline-auth-`) for source access. + - Labels PipelineRuns with `doozer-watch-id` for tracking. + - `KonfluxWatcher` (`doozer/doozerlib/backend/konflux_watcher.py`) monitors pipeline completion. +- Build records are written to BigQuery via `KonfluxDb`. +- NVR comparison against existing builds determines whether a new build is needed. + +### 5. Tag + +Built artifacts are tagged for release candidacy. + +#### Brew/OSBS Path + +- Successful builds are automatically tagged with the candidate tag (e.g. `rhaos-4.17-rhel-9-candidate`). +- For hotfix assemblies (non-stream), builds are also tagged with a hotfix tag (e.g. `rhaos-4.17-rhel-9-hotfix`). + +#### Konflux Path + +- Build records in BigQuery serve as the equivalent of Brew tags. +- Konflux Snapshots capture the set of built images for a release. + +### 6. Inclusion + +Built images are included in the release payload. + +- `doozer release:gen-payload` generates the release imagestream from candidate builds. +- Payload images must follow naming conventions (prefix `ose-`). +- For Konflux, Snapshots and Release resources are created to track the payload. +- Elliott attaches builds to advisories for errata-based delivery. + +## Role of ImageMetadata + +`ImageMetadata` (`doozer/doozerlib/image.py`, inherits from `Metadata`) is the central object for each image: + +- Loaded from ocp-build-data YAML config during `Runtime.initialize()`. +- Stores: image name, distgit key, component name, parent/child relationships, architecture config. +- `config` attribute: a `Model` object wrapping the raw YAML config. +- `is_payload`: whether this image is destined for the OCP release payload. +- `for_release`: whether this image is released via errata. +- `distgit_repo()`: returns the `DistGitRepo` instance for Brew builds. +- `resolve_parent()`: establishes the parent image dependency chain. +- `get_component_name()`: returns the Brew component name. + +Images are stored in `Runtime.image_map` (Dict[str, ImageMetadata]) keyed by distgit key, and ordered by dependency in `Runtime.image_order`. + +## Build System Comparison + +| Aspect | Brew/OSBS | Konflux | +|--------|-----------|---------| +| Source repo | distgit (pkgs.devel.redhat.com) | GitHub build repo | +| Build trigger | `rhpkg container-build` | Tekton PipelineRun | +| Auth | Kerberos (GSSAPI) | GitHub App + kubeconfig | +| Build tracking | Brew task ID, koji session | BigQuery build records | +| Tagging | Brew candidate tag | BigQuery + Snapshot | +| Visibility | p0 (public), p1 (private) | p2 (public), p3 (private) | +| Rebaser | `DistGitRepo` | `KonfluxRebaser` | + +## Key Source Files + +- `doozer/doozerlib/image.py` -- `ImageMetadata` class +- `doozer/doozerlib/distgit.py` -- `DistGitRepo` for Brew distgit operations +- `doozer/doozerlib/backend/rebaser.py` -- `KonfluxRebaser` for Konflux rebase +- `doozer/doozerlib/backend/konflux_image_builder.py` -- Konflux build orchestration +- `doozer/doozerlib/backend/konflux_client.py` -- Kubernetes API client for Konflux +- `doozer/doozerlib/backend/build_repo.py` -- Build source repo management +- `doozer/doozerlib/source_resolver.py` -- Upstream source resolution +- `doozer/doozerlib/rpmcfg.py` -- `RPMMetadata` class +- `doozer/doozerlib/osbs2_builder.py` -- Brew/OSBS build triggering diff --git a/agentic/domain/workflows/release-preparation.md b/agentic/domain/workflows/release-preparation.md new file mode 100644 index 0000000000..64933a411e --- /dev/null +++ b/agentic/domain/workflows/release-preparation.md @@ -0,0 +1,134 @@ +--- +workflow: ReleasePreparation +components: [pyartcd, doozer, elliott] +related_concepts: [Assembly, Runtime, Brew/Koji, Errata/Advisories, Konflux] +--- + +# Release Preparation Workflow + +## Overview + +The end-to-end flow for preparing an OCP release, from assembly creation to promotion. The primary implementation is `PrepareReleaseKonfluxPipeline` in `pyartcd/pyartcd/pipelines/prepare_release_konflux.py`, which orchestrates doozer and elliott commands to create advisories, attach builds and bugs, generate payloads, and promote to release channels. + +## High-Level Steps + +### 1. Assembly Definition + +An assembly is defined in `releases.yml` within ocp-build-data. Each assembly specifies: +- Basis event (Brew event ID or Konflux timestamp) that pins the build state +- Assembly type: `standard` (named release like 4.17.1), `candidate`, `preview`, or `custom` +- Release date +- Component-specific overrides (pinned NVRs, excluded components) + +### 2. Pipeline Initialization + +`PrepareReleaseKonfluxPipeline.run()` performs: +1. `initialize()` -- sets up working directories, clones ocp-build-data and shipment-data repos, validates assembly config, reads `releases.yml` and `group.yml`. +2. `check_advisory_stage_policy()` -- validates advisory stage constraints for the assembly type. +3. `check_blockers()` -- runs `elliott find-bugs:blocker` to check for unresolved blocker bugs. + +### 3. Advisory Creation + +`prepare_et_advisories()` orchestrates: +- Creates RHBA/RHEA advisories via `elliott create` with appropriate boilerplate from `erratatool.yml`. +- For security releases (RHSA), creates security advisories with CVE metadata. +- Runs `elliott find-bugs sweep` to find VERIFIED bugs and attach them to advisories. +- Runs `elliott find-builds` to find and attach builds (images, RPMs) to advisories. +- Runs `elliott attach-cve-flaws` for security advisories. +- Runs verification commands: `elliott verify-attached-bugs`, `elliott verify-attached-operators`. + +### 4. Shipment Preparation + +`prepare_shipment()`: +- Creates or updates a shipment configuration in the shipment-data repo. +- Generates Konflux Snapshot and Release resources. +- Creates a merge request (MR) in GitLab for the shipment config. + +### 5. Jira Ticket Management + +`handle_jira_ticket()`: +- Creates or updates the release Jira ticket. +- Attaches advisory links, shipment MR URL, and build information. + +### 6. Build Data PR + +`create_update_build_data_pr()`: +- Updates the assembly definition in `releases.yml` with advisory numbers. +- Creates a pull request in ocp-build-data via GitHub. + +### 7. Payload Verification + +`verify_payload()`: +- Runs `doozer release:gen-payload` to generate release imagestream. +- Verifies payload content matches expected components. + +### 8. Promotion (separate pipeline) + +`PromotePipeline` in `pyartcd/pyartcd/pipelines/promote.py`: +- Verifies all advisories are in the correct state. +- Signs container images. +- Promotes the payload to release channels (candidate, fast, stable). +- Updates Cincinnati graph data. +- Sends notifications via Slack. + +## Sequence Diagram + +``` + Assembly Author pyartcd elliott doozer Brew/Konflux + | | | | | + | Define assembly | | | | + | in releases.yml | | | | + |------------------>| | | | + | | | | | + | initialize() | | | + | |--- clone ocp-build-data, shipment-data | + | | | | | + | check_blockers() | | | + | |--- find-bugs:blocker ----------------->| | + | |<-- blocker count --| | | + | | | | | + | prepare_et_advisories() | | | + | |--- create -------->| | | + | |<-- advisory IDs ---| | | + | |--- find-bugs sweep>| | | + | |<-- attached bugs --| | | + | |--- find-builds --->| | | + | |<-- attached builds-| | | + | |--- attach-cve-flaws> | | + | |--- verify-* ------>| | | + | | | | | + | prepare_shipment() | | | + | |--- create shipment MR (GitLab) -------> | + | | | | | + | verify_payload() | | | + | |--- release:gen-payload --------------->| | + | |<-- imagestream ----|-------------------| | + | | | | | + | [Promotion - separate pipeline] | | | + | |--- sign images --->|-------------------|---> registries | + | |--- promote ------->|-------------------|---> channels | + | | | | | +``` + +## Error Handling + +| Step | Failure Mode | Behavior | +|------|-------------|----------| +| Assembly validation | Assembly not found in releases.yml | Pipeline exits with ValueError | +| Blocker check | Blocker bugs found | Warning logged; pipeline continues but warns | +| Advisory creation | Errata Tool API failure | Exception raised; pipeline attempts to save partial progress via `create_update_build_data_pr()` in `finally` block | +| Bug sweep | No bugs found | Warning; advisory may be empty | +| Build attachment | Missing builds | ElliottFatalError; logged and re-raised | +| Payload verification | Payload mismatch | Verification failure raised | +| Bundle/FBC builds | Build errors during prep | Deferred: errors collected, pipeline exits with code 2 (UNSTABLE) after completing other steps | +| Promotion | Signing failure | Retried with tenacity; eventually raises | + +## Key Source Files + +- `pyartcd/pyartcd/pipelines/prepare_release_konflux.py` -- main pipeline +- `pyartcd/pyartcd/pipelines/promote.py` -- promotion pipeline +- `pyartcd/pyartcd/pipelines/build_sync.py` -- build sync pipeline +- `elliott/elliottlib/cli/create_cli.py` -- advisory creation +- `elliott/elliottlib/cli/find_builds_cli.py` -- build attachment +- `elliott/elliottlib/cli/find_bugs_sweep_cli.py` -- bug sweep +- `doozer/doozerlib/cli/release_gen_payload.py` -- payload generation diff --git a/agentic/exec-plans/active/implement-agentic-docs.md b/agentic/exec-plans/active/implement-agentic-docs.md new file mode 100644 index 0000000000..e9993e69fa --- /dev/null +++ b/agentic/exec-plans/active/implement-agentic-docs.md @@ -0,0 +1,135 @@ +--- +status: active +owner: "@art-team" +created: 2026-04-16 +target: 2026-04-30 +related_issues: [] +related_prs: [] +--- + +# Implement Agentic Documentation Framework + +## Goal + +Implement an agentic documentation framework for art-tools to provide AI-agent-friendly navigation, domain knowledge, and exec-plan review workflow. + +## Success Criteria + +- [ ] 38 new files created under `agentic/` +- [ ] AGENTS.md is under 150 lines and serves as the primary entry point +- [ ] CI validation added for documentation structure +- [ ] All concept docs in `agentic/domain/concepts/` populated +- [ ] All component docs in `agentic/design-docs/components/` populated +- [ ] Exec-plan templates and tracker in place +- [ ] ADR template and initial ADRs written +- [ ] Developer reference docs populated + +## Context + +AI agents (Claude Code, Copilot, etc.) are increasingly used for development in art-tools. The existing `CLAUDE.md` provides project context, but a structured documentation framework will: + +- Give agents deterministic navigation paths to find relevant context +- Capture domain knowledge that is otherwise tribal or scattered across wikis +- Replace ad-hoc PR descriptions with reviewable exec-plans +- Document architectural decisions in a discoverable format + +This work is foundational and does not depend on external system changes. + +## Technical Approach + +### Architecture Changes + +No code changes. This is a documentation-only addition under `agentic/`. + +### New Abstractions + +- **Exec-plan workflow:** Structured documents that replace lengthy PR descriptions for complex changes. Teammates review the plan before or alongside code. +- **AGENTS.md:** A concise entry point (<150 lines) that directs agents to the right documentation area. + +### Dependencies + +None. Pure documentation. + +## Implementation Phases + +### Phase 1: Directory Structure and Exec-Plans + +- [x] Create `agentic/` directory tree +- [ ] Create exec-plan template (`agentic/exec-plans/template.md`) +- [ ] Create tech-debt tracker (`agentic/exec-plans/tech-debt-tracker.md`) +- [ ] Create this exec-plan (`agentic/exec-plans/active/implement-agentic-docs.md`) + +### Phase 2: Navigation and Entry Points + +- [ ] Create `AGENTS.md` at repo root (<150 lines) +- [ ] Create index files for each documentation area + - [ ] `agentic/design-docs/index.md` + - [ ] `agentic/domain/index.md` + - [ ] `agentic/decisions/index.md` + - [ ] `agentic/references/index.md` + - [ ] `agentic/generated/README.md` + +### Phase 3: Domain Documentation + +- [ ] Create glossary (`agentic/domain/glossary.md`) +- [ ] Create concept docs in `agentic/domain/concepts/` (10 docs) +- [ ] Create workflow docs in `agentic/domain/workflows/` (3 docs) + +### Phase 4: Design and Component Documentation + +- [ ] Create core beliefs (`agentic/design-docs/core-beliefs.md`) +- [ ] Create component docs in `agentic/design-docs/components/` (5 docs) + +### Phase 5: ADRs + +- [ ] Create ADR template (`agentic/decisions/adr-template.md`) +- [ ] Write ADR-0001: Monorepo Structure for Release Tools +- [ ] Write ADR-0002: Runtime Pattern for CLI Initialization +- [ ] Write ADR-0003: Dual Build System Support (Brew and Konflux) + +### Phase 6: Developer Reference Documentation + +- [ ] Create developer reference docs in `agentic/references/` + +### Phase 7: CI Validation + +- [ ] Add CI check for documentation structure integrity +- [ ] Validate all index links resolve to existing files + +## Testing Strategy + +### Unit Tests + +Not applicable (documentation only). + +### Integration Tests + +Not applicable. + +### End-to-End Tests + +- Manual review: verify all links in index files resolve +- CI validation script checks file existence and structure + +## Decision Log + +| Date | Decision | Rationale | +|------|----------|-----------| +| 2026-04-16 | Use `agentic/` as the top-level directory | Separates agent-oriented docs from existing project docs; avoids cluttering repo root | +| 2026-04-16 | Keep AGENTS.md under 150 lines | Agents work better with concise entry points that link deeper rather than monolithic files | +| 2026-04-16 | Use exec-plans instead of RFCs | Exec-plans are lighter weight and action-oriented, better suited to this team's workflow | + +## Progress Notes + +| Date | Status Update | +|------|---------------| +| 2026-04-16 | Started implementation. Directory structure created. Working on exec-plan templates, index files, and ADR template. | + +## Completion Checklist + +- [ ] All implementation phases completed +- [ ] All index file links verified +- [ ] AGENTS.md under 150 lines +- [ ] PR(s) reviewed and merged +- [ ] Success criteria verified +- [ ] Exec plan status updated to `completed` diff --git a/agentic/exec-plans/tech-debt-tracker.md b/agentic/exec-plans/tech-debt-tracker.md new file mode 100644 index 0000000000..aa75a60d33 --- /dev/null +++ b/agentic/exec-plans/tech-debt-tracker.md @@ -0,0 +1,63 @@ +# Tech Debt Tracker + +Central registry of known technical debt in the art-tools repository. Items are organized by priority and tracked through resolution. + +## Item Template + +When adding a new item, copy this template: + +``` +### [DEBT-NNNN] Short description + +- **Status:** open | in-progress | resolved +- **Owner:** @username (or unassigned) +- **Created:** YYYY-MM-DD +- **Impact:** Description of how this affects development, reliability, or performance +- **Workaround:** Current mitigation, if any +- **Fix:** Description of the intended fix +- **Effort:** S | M | L +- **Related Issues:** #issue-number, #issue-number +``` + +--- + +## High Priority + +_Items that actively impede development, cause production issues, or block planned work._ + +(No items currently tracked.) + +--- + +## Medium Priority + +_Items that increase maintenance burden or slow development but have acceptable workarounds._ + +(No items currently tracked.) + +--- + +## Low Priority + +_Items that would improve code quality or developer experience but are not urgent._ + +(No items currently tracked.) + +--- + +## Resolved (Recent) + +_Recently resolved items, kept here for reference. Move to git history after 3 months._ + +(No items currently tracked.) + +--- + +## How to Use + +1. **Adding debt:** Copy the item template above into the appropriate priority section. Assign the next sequential `DEBT-NNNN` ID. +2. **Claiming work:** Set the owner to your handle and change status to `in-progress`. +3. **Resolving debt:** Move the item to the "Resolved (Recent)" section, update status to `resolved`, and add the date and PR link. +4. **Priority changes:** Re-evaluate priority during planning. Move items between sections as impact becomes clearer. +5. **Linking from exec plans:** When an exec plan defers cleanup, add a debt item here and reference it from the exec plan's completion checklist. +6. **Cleanup:** Items in "Resolved (Recent)" older than 3 months can be removed; git history preserves them. diff --git a/agentic/exec-plans/template.md b/agentic/exec-plans/template.md new file mode 100644 index 0000000000..301c514d60 --- /dev/null +++ b/agentic/exec-plans/template.md @@ -0,0 +1,99 @@ +--- +status: active # active | completed | abandoned +owner: "@username" +created: YYYY-MM-DD +target: YYYY-MM-DD +related_issues: [] +related_prs: [] +--- + +# [Exec Plan Title] + +## Goal + +_One sentence describing what this execution plan achieves._ + +## Success Criteria + +- [ ] Criterion 1 +- [ ] Criterion 2 +- [ ] Criterion 3 + +## Context + +_Why is this work happening now? What prompted it?_ + +**Links:** +- Spec/RFC: (link) +- ADR: (link) +- Upstream docs: (link) + +## Technical Approach + +### Architecture Changes + +_Describe any structural changes to the codebase._ + +### New Abstractions + +_List any new classes, modules, or patterns being introduced._ + +### Dependencies + +_New dependencies, version bumps, or cross-package changes._ + +## Implementation Phases + +### Phase 1: [Name] + +- [ ] Task 1.1 +- [ ] Task 1.2 +- [ ] Task 1.3 + +### Phase 2: [Name] + +- [ ] Task 2.1 +- [ ] Task 2.2 +- [ ] Task 2.3 + +### Phase 3: [Name] + +- [ ] Task 3.1 +- [ ] Task 3.2 +- [ ] Task 3.3 + +## Testing Strategy + +### Unit Tests + +_Describe unit test coverage targets and approach._ + +### Integration Tests + +_Describe integration test coverage, including any external system mocks._ + +### End-to-End Tests + +_Describe any e2e validation, manual or automated._ + +## Decision Log + +| Date | Decision | Rationale | +|------|----------|-----------| +| YYYY-MM-DD | Decision description | Why this was chosen | + +## Progress Notes + +| Date | Status Update | +|------|---------------| +| YYYY-MM-DD | Started work | + +## Completion Checklist + +- [ ] All implementation phases completed +- [ ] Tests passing (unit, integration, e2e as applicable) +- [ ] Documentation updated +- [ ] PR(s) reviewed and merged +- [ ] Success criteria verified +- [ ] Tech debt items logged (if any deferred) +- [ ] Exec plan status updated to `completed` diff --git a/agentic/generated/README.md b/agentic/generated/README.md new file mode 100644 index 0000000000..745cc70dca --- /dev/null +++ b/agentic/generated/README.md @@ -0,0 +1,20 @@ +# Generated Documentation + +This directory contains auto-generated documentation. **Do not manually edit files in this directory.** They will be overwritten by automated processes. + +## Purpose + +Generated docs provide derived or computed views of the codebase that are useful for navigation but expensive to maintain by hand. Examples of what could live here: + +- **Dependency graph** -- Visual or textual representation of cross-package dependencies within the monorepo +- **API reference** -- Auto-generated CLI command documentation from docstrings and click decorators +- **Build metadata index** -- Summary of supported OCP versions and their build configurations + +## Regeneration + +Generated files should include a header comment indicating: +- The script or command that produced them +- The date of last generation +- Instructions to regenerate + +If a generated file is out of date, re-run the generating command rather than editing the file directly. diff --git a/agentic/references/index.md b/agentic/references/index.md new file mode 100644 index 0000000000..11d1bea90a --- /dev/null +++ b/agentic/references/index.md @@ -0,0 +1,25 @@ +# References + +This section holds reference documentation for external systems and APIs that art-tools integrates with. + +## External Systems + +Art-tools interacts with several Red Hat internal systems. Reference docs for these can be added as needed: + +- **Brew / Koji** -- Red Hat's build system. API documentation and common query patterns. +- **Errata Tool** -- Advisory management system. API endpoints and advisory lifecycle. +- **Bugzilla** -- Bug tracking. Query patterns and bug state transitions relevant to releases. +- **Jira** -- Issue tracking for ART team workflows and release planning. +- **Distgit (Dist-Git)** -- RPM and container source repositories managed via `rhpkg`. +- **UMB (Unified Message Bus)** -- Messaging system for build and release event notifications. +- **Konflux** -- Next-generation build system. Build pipeline integration points. + +## Adding References + +Add a reference document here when: + +- You need to document API patterns, endpoints, or query templates for an external system +- A new external system integration is added to art-tools +- Common "how to query X" patterns emerge that should be shared across the team + +Keep reference docs factual and concise. For "why we integrate this way," use an [ADR](../decisions/index.md). diff --git a/agentic/scripts/generate-metrics-dashboard.py b/agentic/scripts/generate-metrics-dashboard.py new file mode 100644 index 0000000000..28c6be4852 --- /dev/null +++ b/agentic/scripts/generate-metrics-dashboard.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 +""" +Generate HTML dashboard for agentic documentation metrics. + +Usage: + python3 agentic/scripts/generate-metrics-dashboard.py + python3 agentic/scripts/generate-metrics-dashboard.py --output agentic/metrics-dashboard.html + python3 agentic/scripts/generate-metrics-dashboard.py --open +""" + +import argparse +import re +import subprocess +import sys +import webbrowser +from datetime import datetime +from pathlib import Path + + +def run_metric_script(script_path: Path, *args) -> dict: + """Run a metric script and return parsed output.""" + try: + result = subprocess.run( + ['python3', str(script_path)] + list(args), + capture_output=True, + text=True, + cwd=Path.cwd() + ) + return { + 'success': result.returncode == 0, + 'output': result.stdout, + 'error': result.stderr + } + except Exception as e: + return { + 'success': False, + 'output': '', + 'error': str(e) + } + + +def parse_navigation_metrics(output: str) -> dict: + """Parse navigation depth script output.""" + metrics = { + 'max_depth': 0, + 'avg_depth': 0.0, + 'total_docs': 0, + 'reachable_docs': 0, + 'unreachable_count': 0, + 'over_limit_count': 0, + 'status': 'unknown' + } + + for line in output.split('\n'): + if 'Max observed depth:' in line: + metrics['max_depth'] = int(line.split(':')[1].strip().split()[0]) + elif 'Average depth:' in line: + metrics['avg_depth'] = float(line.split(':')[1].strip().split()[0]) + elif 'Total documents found:' in line: + metrics['total_docs'] = int(line.split(':')[1].strip()) + elif 'Reachable documents:' in line: + metrics['reachable_docs'] = int(line.split(':')[1].strip()) + elif 'Unreachable documents:' in line: + metrics['unreachable_count'] = int(line.split(':')[1].strip()) + elif 'Docs exceeding limit:' in line: + metrics['over_limit_count'] = int(line.split(':')[1].strip()) + elif 'PASSED' in line: + metrics['status'] = 'pass' + elif 'FAILED' in line: + metrics['status'] = 'fail' + + return metrics + + +def parse_context_budget(output: str) -> dict: + """Parse context budget script output.""" + metrics = { + 'workflows': [], + 'max_observed': 0, + 'avg_observed': 0, + 'passing': 0, + 'failing': 0, + 'status': 'unknown' + } + + current_workflow = None + for line in output.split('\n'): + if line.strip() and not line.startswith(('=', '-', 'CONTEXT', 'Budget', 'Recommendations')): + if 'Status:' in line: + if current_workflow: + if 'OK' in line: + status = 'pass' + metrics['passing'] += 1 + else: + status = 'fail' + metrics['failing'] += 1 + + match = re.search(r'\((\d+)/(\d+) lines', line) + if match: + current_workflow['actual'] = int(match.group(1)) + current_workflow['limit'] = int(match.group(2)) + current_workflow['status'] = status + metrics['workflows'].append(current_workflow) + current_workflow = None + elif line[0].isupper() and not line.startswith(('SUMMARY', 'Workflows', 'PASSED', 'FAILED')): + current_workflow = {'name': line.strip()} + + if 'Max observed:' in line: + try: + metrics['max_observed'] = int(line.split(':')[1].strip().split()[0]) + except (ValueError, IndexError): + pass + elif 'Average observed:' in line: + try: + metrics['avg_observed'] = int(line.split(':')[1].strip().split()[0]) + except (ValueError, IndexError): + pass + elif 'PASSED' in line and 'workflows' in line.lower(): + metrics['status'] = 'pass' + elif 'FAILED' in line and 'workflows' in line.lower(): + metrics['status'] = 'fail' + + return metrics + + +def generate_html_dashboard(nav_metrics: dict, budget_metrics: dict, output_path: Path): + """Generate HTML dashboard.""" + nav_score = 100 if nav_metrics['status'] == 'pass' else 50 + budget_score = 100 if budget_metrics['status'] == 'pass' else 75 + structure_score = 100 + coverage_score = 100 + + overall_score = (nav_score + budget_score + structure_score + coverage_score) // 4 + + if overall_score >= 90: + overall_label = 'EXCELLENT' + overall_color = '#10b981' + elif overall_score >= 80: + overall_label = 'GOOD' + overall_color = '#3b82f6' + elif overall_score >= 70: + overall_label = 'FAIR' + overall_color = '#f59e0b' + else: + overall_label = 'POOR' + overall_color = '#ef4444' + + workflow_html = "" + for workflow in budget_metrics.get('workflows', []): + status_class = 'pass' if workflow.get('status') == 'pass' else 'fail' + status_text = 'OK' if workflow.get('status') == 'pass' else 'OVER' + actual = workflow.get('actual', 0) + limit = workflow.get('limit', 700) + + workflow_html += f""" +
+
+
{workflow['name']}
+ {actual}/{limit} lines +
+
{status_text}
+
+""" + + html = f""" + + + + + art-tools Documentation Metrics Dashboard + + + +
+
+

Documentation Metrics Dashboard

+

art-tools

+
+
+
+
+
{overall_score}
+
/100
+
+

{overall_label}

+

Overall Documentation Quality

+
+
+
+

Navigation Depth

+
{nav_metrics['max_depth']} hops
+
+
+
+
+ Score: {nav_score}/100 + {'PASSED' if nav_metrics['status'] == 'pass' else 'FAILED'} +
+
+ Average: {nav_metrics['avg_depth']:.1f} hops | + Reachable: {nav_metrics['reachable_docs']}/{nav_metrics['total_docs']} docs +
+
+
+

Context Budget

+
{budget_metrics['max_observed']} lines
+
+
+
+
+ Score: {budget_score}/100 + {'PASSED' if budget_metrics['status'] == 'pass' else 'OVER BUDGET'} +
+
+ Average: {budget_metrics['avg_observed']} lines | + Passing: {budget_metrics['passing']}/{budget_metrics['passing'] + budget_metrics['failing']} workflows +
+
+
+
+

Workflow Analysis

+
+{workflow_html} +
+
+
+

Quick Stats

+
+
+
Total Documents
+
{nav_metrics['total_docs']}
+
+
+
Reachable
+
{nav_metrics['reachable_docs']}
+
+
+
Max Depth
+
{nav_metrics['max_depth']} hops
+
+
+
Avg Context
+
{budget_metrics['avg_observed']} lines
+
+
+
+
+ Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} by + agentic/scripts/generate-metrics-dashboard.py +
+
+
+ + +""" + + output_path.write_text(html) + return output_path + + +def main(): + parser = argparse.ArgumentParser(description='Generate HTML metrics dashboard') + parser.add_argument('--output', '-o', default='agentic/metrics-dashboard.html', + help='Output HTML file path') + parser.add_argument('--open', action='store_true', + help='Open dashboard in browser after generation') + + args = parser.parse_args() + + base_dir = Path.cwd() + scripts_dir = base_dir / 'agentic' / 'scripts' + + if not scripts_dir.exists(): + scripts_dir = Path(__file__).parent + + print("Running navigation depth analysis...") + nav_result = run_metric_script(scripts_dir / 'measure-navigation-depth.py', '--max-depth', '3') + + print("Running context budget analysis...") + budget_result = run_metric_script(scripts_dir / 'measure-context-budget.py', '--max-budget', '700') + + if not nav_result['success'] or not budget_result['success']: + print("Error running metric scripts", file=sys.stderr) + if not nav_result['success']: + print(f"Navigation error: {nav_result['error']}", file=sys.stderr) + if not budget_result['success']: + print(f"Budget error: {budget_result['error']}", file=sys.stderr) + sys.exit(1) + + print("Parsing metrics...") + nav_metrics = parse_navigation_metrics(nav_result['output']) + budget_metrics = parse_context_budget(budget_result['output']) + + print("Generating HTML dashboard...") + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + + dashboard_path = generate_html_dashboard(nav_metrics, budget_metrics, output_path) + + print(f"Dashboard generated: {dashboard_path}") + + if args.open: + print("Opening in browser...") + webbrowser.open(f'file://{dashboard_path.absolute()}') + + print(f"\nTo view: open {dashboard_path}") + + +if __name__ == '__main__': + main() diff --git a/agentic/scripts/measure-all-metrics.sh b/agentic/scripts/measure-all-metrics.sh new file mode 100755 index 0000000000..bc1f5ee9c3 --- /dev/null +++ b/agentic/scripts/measure-all-metrics.sh @@ -0,0 +1,379 @@ +#!/bin/bash +# Comprehensive agentic documentation metrics dashboard +# +# Measures: +# 1. Navigation depth (link graph analysis) +# 2. Context budget (typical workflows) +# 3. Structure compliance (validation) +# 4. Quality score calculation +# +# Usage: +# ./agentic/scripts/measure-all-metrics.sh # Display metrics only +# ./agentic/scripts/measure-all-metrics.sh --generate-reports # Save to files +# ./agentic/scripts/measure-all-metrics.sh --html # Generate HTML dashboard + +if [ -z "$BASH_VERSION" ]; then + echo "ERROR: This is a Bash script, not a Python script" + echo "" + echo "Correct usage:" + echo " ./agentic/scripts/measure-all-metrics.sh" + echo " bash agentic/scripts/measure-all-metrics.sh" + exit 1 +fi + +set -e + +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) +cd "$REPO_ROOT" + +SCRIPT_DIR="agentic/scripts" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +GENERATE_REPORTS=false +GENERATE_HTML=false + +while [[ $# -gt 0 ]]; do + case $1 in + --generate-reports) + GENERATE_REPORTS=true + shift + ;; + --html) + GENERATE_HTML=true + shift + ;; + --update-quality-score) + echo -e "${YELLOW}--update-quality-score is deprecated, use --generate-reports${NC}" + GENERATE_REPORTS=true + shift + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Measures agentic documentation quality metrics." + echo "" + echo "Options:" + echo " --generate-reports Generate METRICS_REPORT.md and update QUALITY_SCORE.md" + echo " --html Generate HTML dashboard (agentic/metrics-dashboard.html)" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--generate-reports] [--html]" + exit 1 + ;; + esac +done + +echo -e "${BLUE}================================================================${NC}" +echo -e "${BLUE} AGENTIC DOCUMENTATION METRICS DASHBOARD ${NC}" +echo -e "${BLUE}================================================================${NC}" +echo "" + +if ! command -v python3 &> /dev/null; then + echo -e "${RED}Python 3 is required but not found${NC}" + exit 1 +fi + +# Metric 1: Navigation Depth +echo -e "${BLUE}----------------------------------------------------------------${NC}" +echo -e "${BLUE}1. NAVIGATION DEPTH ANALYSIS${NC}" +echo -e "${BLUE}----------------------------------------------------------------${NC}" + +if [ -f "$SCRIPT_DIR/measure-navigation-depth.py" ]; then + NAV_OUTPUT=$(python3 $SCRIPT_DIR/measure-navigation-depth.py --max-depth 3 2>&1) + echo "$NAV_OUTPUT" + + if echo "$NAV_OUTPUT" | grep -q "PASSED"; then + NAVIGATION_STATUS="PASSED" + NAVIGATION_SCORE=100 + elif echo "$NAV_OUTPUT" | grep -q "FAILED"; then + NAVIGATION_STATUS="FAILED" + NAVIGATION_SCORE=50 + else + NAVIGATION_STATUS="UNKNOWN" + NAVIGATION_SCORE=0 + fi +else + echo -e "${YELLOW}Navigation depth script not found${NC}" + NAVIGATION_STATUS="SKIPPED" + NAVIGATION_SCORE=0 +fi + +# Metric 2: Context Budget +echo -e "${BLUE}----------------------------------------------------------------${NC}" +echo -e "${BLUE}2. CONTEXT BUDGET ANALYSIS${NC}" +echo -e "${BLUE}----------------------------------------------------------------${NC}" + +if [ -f "$SCRIPT_DIR/measure-context-budget.py" ]; then + BUDGET_OUTPUT=$(python3 $SCRIPT_DIR/measure-context-budget.py --max-budget 700 2>&1) + echo "$BUDGET_OUTPUT" + echo "" + + if echo "$BUDGET_OUTPUT" | grep -q "PASSED"; then + BUDGET_STATUS="PASSED" + BUDGET_SCORE=100 + elif echo "$BUDGET_OUTPUT" | grep -q "FAILED"; then + BUDGET_STATUS="FAILED" + BUDGET_SCORE=75 + else + BUDGET_STATUS="UNKNOWN" + BUDGET_SCORE=0 + fi +else + echo -e "${YELLOW}Context budget script not found${NC}" + BUDGET_STATUS="SKIPPED" + BUDGET_SCORE=0 +fi + +# Metric 3: Structure Validation +echo -e "${BLUE}----------------------------------------------------------------${NC}" +echo -e "${BLUE}3. STRUCTURE VALIDATION${NC}" +echo -e "${BLUE}----------------------------------------------------------------${NC}" + +STRUCTURE_SCORE=0 +STRUCTURE_CHECKS=0 +STRUCTURE_PASSED=0 + +# Check AGENTS.md length +if [ -f "AGENTS.md" ]; then + STRUCTURE_CHECKS=$((STRUCTURE_CHECKS + 1)) + AGENTS_LINES=$(wc -l < AGENTS.md) + if [ "$AGENTS_LINES" -le 150 ]; then + echo -e "${GREEN}AGENTS.md length OK ($AGENTS_LINES/150 lines)${NC}" + STRUCTURE_PASSED=$((STRUCTURE_PASSED + 1)) + else + echo -e "${RED}AGENTS.md too long ($AGENTS_LINES/150 lines)${NC}" + fi +fi + +# Check required directories +REQUIRED_DIRS="agentic/design-docs agentic/domain agentic/exec-plans agentic/decisions agentic/references agentic/generated" +for dir in $REQUIRED_DIRS; do + STRUCTURE_CHECKS=$((STRUCTURE_CHECKS + 1)) + if [ -d "$dir" ]; then + STRUCTURE_PASSED=$((STRUCTURE_PASSED + 1)) + else + echo -e "${RED}Missing directory: $dir${NC}" + fi +done + +# Check required files +REQUIRED_FILES="agentic/DESIGN.md agentic/DEVELOPMENT.md agentic/TESTING.md agentic/SECURITY.md" +for file in $REQUIRED_FILES; do + STRUCTURE_CHECKS=$((STRUCTURE_CHECKS + 1)) + if [ -f "$file" ]; then + STRUCTURE_PASSED=$((STRUCTURE_PASSED + 1)) + else + echo -e "${RED}Missing file: $file${NC}" + fi +done + +if [ $STRUCTURE_CHECKS -gt 0 ]; then + STRUCTURE_SCORE=$(( STRUCTURE_PASSED * 100 / STRUCTURE_CHECKS )) + if [ $STRUCTURE_SCORE -eq 100 ]; then + STRUCTURE_STATUS="PASSED" + echo -e "${GREEN}Structure compliance: $STRUCTURE_PASSED/$STRUCTURE_CHECKS checks passed${NC}" + elif [ $STRUCTURE_SCORE -ge 80 ]; then + STRUCTURE_STATUS="PARTIAL" + echo -e "${YELLOW}Structure compliance: $STRUCTURE_PASSED/$STRUCTURE_CHECKS checks passed${NC}" + else + STRUCTURE_STATUS="FAILED" + echo -e "${RED}Structure compliance: $STRUCTURE_PASSED/$STRUCTURE_CHECKS checks passed${NC}" + fi +else + STRUCTURE_STATUS="SKIPPED" +fi + +# Metric 4: Documentation Coverage +echo -e "${BLUE}----------------------------------------------------------------${NC}" +echo -e "${BLUE}4. DOCUMENTATION COVERAGE${NC}" +echo -e "${BLUE}----------------------------------------------------------------${NC}" + +ADR_COUNT=$(find agentic/decisions -name "adr-*.md" -not -name "*template*" 2>/dev/null | wc -l | tr -d ' ') +echo " ADRs documented: $ADR_COUNT" + +CONCEPT_COUNT=$(find agentic/domain/concepts -name "*.md" 2>/dev/null | wc -l | tr -d ' ') +echo " Domain concepts: $CONCEPT_COUNT" + +ACTIVE_PLANS=$(find agentic/exec-plans/active -name "*.md" -not -name "template*" 2>/dev/null | wc -l | tr -d ' ') +COMPLETED_PLANS=$(find agentic/exec-plans/completed -name "*.md" 2>/dev/null | wc -l | tr -d ' ') +echo " Execution plans: $ACTIVE_PLANS active, $COMPLETED_PLANS completed" + +COVERAGE_SCORE=0 +if [ "$ADR_COUNT" -ge 3 ]; then COVERAGE_SCORE=$((COVERAGE_SCORE + 40)); fi +if [ "$CONCEPT_COUNT" -ge 2 ]; then COVERAGE_SCORE=$((COVERAGE_SCORE + 30)); fi +if [ $((ACTIVE_PLANS + COMPLETED_PLANS)) -ge 1 ]; then COVERAGE_SCORE=$((COVERAGE_SCORE + 30)); fi + +if [ $COVERAGE_SCORE -ge 80 ]; then + COVERAGE_STATUS="GOOD" +elif [ $COVERAGE_SCORE -ge 50 ]; then + COVERAGE_STATUS="FAIR" +else + COVERAGE_STATUS="POOR" +fi + +echo " Coverage score: $COVERAGE_SCORE/100 $COVERAGE_STATUS" + +# Overall Summary +echo "" +echo -e "${BLUE}================================================================${NC}" +echo -e "${BLUE} OVERALL SUMMARY ${NC}" +echo -e "${BLUE}================================================================${NC}" +echo "" + +printf " %-30s %10s %10s\n" "Metric" "Score" "Status" +echo " ----------------------------------------------------------------" +printf " %-30s %10s %10s\n" "Navigation Depth" "$NAVIGATION_SCORE/100" "$NAVIGATION_STATUS" +printf " %-30s %10s %10s\n" "Context Budget" "$BUDGET_SCORE/100" "$BUDGET_STATUS" +printf " %-30s %10s %10s\n" "Structure Compliance" "$STRUCTURE_SCORE/100" "$STRUCTURE_STATUS" +printf " %-30s %10s %10s\n" "Documentation Coverage" "$COVERAGE_SCORE/100" "$COVERAGE_STATUS" +echo " ----------------------------------------------------------------" + +TOTAL_SCORE=$(( (NAVIGATION_SCORE + BUDGET_SCORE + STRUCTURE_SCORE + COVERAGE_SCORE) / 4 )) +printf " %-30s %10s\n" "OVERALL QUALITY SCORE" "$TOTAL_SCORE/100" + +echo "" + +if [ $TOTAL_SCORE -ge 80 ]; then + echo -e "${GREEN}EXCELLENT - Documentation is in great shape${NC}" + EXIT_CODE=0 +elif [ $TOTAL_SCORE -ge 60 ]; then + echo -e "${YELLOW}GOOD - Some improvements recommended${NC}" + EXIT_CODE=0 +elif [ $TOTAL_SCORE -ge 40 ]; then + echo -e "${YELLOW}FAIR - Significant improvements needed${NC}" + EXIT_CODE=1 +else + echo -e "${RED}POOR - Documentation needs major work${NC}" + EXIT_CODE=1 +fi + +echo "" + +# Generate report files if requested +if [ "$GENERATE_REPORTS" = true ]; then + echo -e "${BLUE}Updating agentic/METRICS_REPORT.md...${NC}" + + cat > agentic/METRICS_REPORT.md < **Last Updated**: $(date +"%Y-%m-%d %H:%M:%S") +> **Overall Score**: $TOTAL_SCORE/100 + +## Summary + +| Metric | Score | Status | +|--------|-------|--------| +| Navigation Depth | $NAVIGATION_SCORE/100 | $NAVIGATION_STATUS | +| Context Budget | $BUDGET_SCORE/100 | $BUDGET_STATUS | +| Structure Compliance | $STRUCTURE_SCORE/100 | $STRUCTURE_STATUS | +| Documentation Coverage | $COVERAGE_SCORE/100 | $COVERAGE_STATUS | +| **OVERALL** | **$TOTAL_SCORE/100** | | + +## Metrics Explained + +### Navigation Depth ($NAVIGATION_SCORE/100) + +Measures how many "hops" (link clicks) are required to reach any documentation from AGENTS.md. + +- **Target**: All docs reachable in 3 hops or fewer +- **Why**: Keeps context loading efficient, prevents navigation dead-ends + +### Context Budget ($BUDGET_SCORE/100) + +Measures total documentation lines loaded for typical agent workflows. + +- **Target**: 700 lines or fewer per workflow +- **Why**: Prevents context window overflow, improves agent performance + +### Structure Compliance ($STRUCTURE_SCORE/100) + +Validates required directory structure and files exist. + +- **Target**: 100% compliance +- **Why**: Ensures consistent structure for tooling and navigation + +### Documentation Coverage ($COVERAGE_SCORE/100) + +Measures completeness of documentation. + +- **Metrics**: + - ADRs: $ADR_COUNT (target: 3 or more) + - Concepts: $CONCEPT_COUNT (target: 2 or more) + - Exec Plans: $((ACTIVE_PLANS + COMPLETED_PLANS)) (target: 1 or more) + +## How to Improve + +\`\`\`bash +# Check navigation depth +python3 agentic/scripts/measure-navigation-depth.py --verbose + +# Check context budget +python3 agentic/scripts/measure-context-budget.py + +# Run all metrics +make check-docs + +# Generate HTML dashboard +make docs-dashboard +\`\`\` + +--- + +*This report is automatically generated by \`agentic/scripts/measure-all-metrics.sh --generate-reports\`* +EOF + + echo -e "${GREEN}Updated agentic/METRICS_REPORT.md${NC}" + + # Append automated metrics to QUALITY_SCORE.md if it exists + if [ -f "agentic/QUALITY_SCORE.md" ]; then + echo -e "${BLUE}Appending automated metrics to QUALITY_SCORE.md...${NC}" + + if ! grep -q "## Automated Metrics" agentic/QUALITY_SCORE.md 2>/dev/null; then + cat >> agentic/QUALITY_SCORE.md < **Last Run**: $(date +"%Y-%m-%d %H:%M:%S") +> **Source**: Generated by \`agentic/scripts/measure-all-metrics.sh\` + +| Metric | Score | Status | +|--------|-------|--------| +| Navigation Depth | $NAVIGATION_SCORE/100 | $NAVIGATION_STATUS | +| Context Budget | $BUDGET_SCORE/100 | $BUDGET_STATUS | +| Structure Compliance | $STRUCTURE_SCORE/100 | $STRUCTURE_STATUS | +| Documentation Coverage | $COVERAGE_SCORE/100 | $COVERAGE_STATUS | + +**Overall Automated Score**: $TOTAL_SCORE/100 + +See [METRICS_REPORT.md](./METRICS_REPORT.md) for detailed automated metrics. +EOF + echo -e "${GREEN}Appended automated metrics section to QUALITY_SCORE.md${NC}" + else + echo -e "${YELLOW}Automated metrics section already exists in QUALITY_SCORE.md${NC}" + fi + fi +fi + +# Generate HTML dashboard if requested +if [ "$GENERATE_HTML" = true ]; then + echo "" + echo -e "${BLUE}Generating HTML dashboard...${NC}" + if [ -f "$SCRIPT_DIR/generate-metrics-dashboard.py" ]; then + python3 $SCRIPT_DIR/generate-metrics-dashboard.py + echo -e "${GREEN}HTML dashboard available at: agentic/metrics-dashboard.html${NC}" + else + echo -e "${YELLOW}HTML dashboard generator not found${NC}" + fi +fi + +exit $EXIT_CODE diff --git a/agentic/scripts/measure-context-budget.py b/agentic/scripts/measure-context-budget.py new file mode 100644 index 0000000000..7309bd3be4 --- /dev/null +++ b/agentic/scripts/measure-context-budget.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +""" +Measure documentation context budget for typical navigation paths. + +Simulates agent workflows and measures how much documentation gets loaded. + +Metrics: +- Total lines loaded per workflow +- Files accessed per workflow +- Context budget compliance + +Usage: + python3 agentic/scripts/measure-context-budget.py + python3 agentic/scripts/measure-context-budget.py --max-budget 700 +""" + +import argparse +import sys +from pathlib import Path +from typing import List, Dict + + +def count_lines(file_path: Path) -> int: + """Count non-empty lines in a file.""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + lines = [line.strip() for line in f if line.strip()] + if lines and lines[0] == '---': + try: + end_idx = lines[1:].index('---') + 2 + lines = lines[end_idx:] + except ValueError: + pass + return len(lines) + except Exception as e: + print(f"Warning: Could not read {file_path}: {e}", file=sys.stderr) + return 0 + + +class Workflow: + """Represents a typical agent workflow.""" + + def __init__(self, name: str, description: str, files: List[str]): + self.name = name + self.description = description + self.files = files + + def measure(self, base_dir: Path) -> Dict: + """Measure context budget for this workflow.""" + total_lines = 0 + file_details = [] + missing_files = [] + + for file_pattern in self.files: + file_path = base_dir / file_pattern + + if not file_path.exists(): + missing_files.append(file_pattern) + continue + + lines = count_lines(file_path) + total_lines += lines + file_details.append({ + 'path': file_pattern, + 'lines': lines + }) + + return { + 'name': self.name, + 'description': self.description, + 'total_lines': total_lines, + 'file_count': len(file_details), + 'files': file_details, + 'missing_files': missing_files + } + + +# art-tools-specific workflows +WORKFLOWS = [ + Workflow( + name="Bug Fix (Simple)", + description="Find and fix a bug in existing code", + files=[ + 'AGENTS.md', + 'ARCHITECTURE.md', + 'agentic/DEVELOPMENT.md' + ] + ), + Workflow( + name="Bug Fix (Complex)", + description="Debug an issue requiring domain knowledge", + files=[ + 'AGENTS.md', + 'ARCHITECTURE.md', + 'agentic/domain/glossary.md', + 'agentic/DEVELOPMENT.md', + 'agentic/TESTING.md' + ] + ), + Workflow( + name="Feature Implementation", + description="Implement a new feature with exec-plan", + files=[ + 'AGENTS.md', + 'ARCHITECTURE.md', + 'agentic/design-docs/core-beliefs.md', + 'agentic/domain/glossary.md', + 'agentic/DESIGN.md', + 'agentic/DEVELOPMENT.md', + 'agentic/TESTING.md' + ] + ), + Workflow( + name="Understanding System", + description="Learn how art-tools works", + files=[ + 'AGENTS.md', + 'ARCHITECTURE.md', + 'agentic/design-docs/core-beliefs.md', + 'agentic/domain/glossary.md' + ] + ), + Workflow( + name="Security Review", + description="Review security implications of a change", + files=[ + 'AGENTS.md', + 'agentic/SECURITY.md', + 'agentic/design-docs/core-beliefs.md' + ] + ), + Workflow( + name="Advisory Management", + description="Work on elliott advisory commands", + files=[ + 'AGENTS.md', + 'agentic/design-docs/components/elliott.md', + 'agentic/domain/concepts/errata-advisories.md', + 'agentic/domain/workflows/advisory-management.md' + ] + ), + Workflow( + name="Image Build Work", + description="Work on doozer image build commands", + files=[ + 'AGENTS.md', + 'agentic/design-docs/components/doozer.md', + 'agentic/domain/concepts/distgit.md', + 'agentic/domain/workflows/image-build-lifecycle.md' + ] + ), +] + + +def print_workflow_report(result: Dict, max_budget: int): + """Print report for a single workflow.""" + total = result['total_lines'] + over_budget = total > max_budget + status = "OVER" if over_budget else "OK" + + print(f"\n{result['name']}") + print(f" {result['description']}") + print(f" Status: {status} ({total}/{max_budget} lines, {result['file_count']} files)") + + if result['missing_files']: + print(f" Missing files: {', '.join(result['missing_files'])}") + + if over_budget: + print(" Files loaded:") + for file in result['files']: + print(f" - {file['lines']:4d} lines: {file['path']}") + + +def print_summary(results: List[Dict], max_budget: int): + """Print summary report.""" + print("\n" + "=" * 70) + print("CONTEXT BUDGET ANALYSIS") + print("=" * 70) + print(f"Budget Limit: {max_budget} lines per workflow\n") + + passing = 0 + failing = 0 + + for result in results: + print_workflow_report(result, max_budget) + if result['total_lines'] <= max_budget: + passing += 1 + else: + failing += 1 + + print("\n" + "=" * 70) + print("SUMMARY") + print("-" * 70) + print(f" Workflows tested: {len(results)}") + print(f" Passing (<={max_budget} lines): {passing}") + print(f" Failing (>{max_budget} lines): {failing}") + + if results: + max_observed = max(r['total_lines'] for r in results) + avg_observed = sum(r['total_lines'] for r in results) / len(results) + print(f"\n Max observed: {max_observed} lines") + print(f" Average observed: {avg_observed:.0f} lines") + + print("\n" + "=" * 70) + + if failing == 0: + print("PASSED: All workflows within budget") + return True + else: + print(f"FAILED: {failing} workflows exceed budget") + print("\nRecommendations:") + print(" 1. Split large files into smaller, focused documents") + print(" 2. Increase budget limit if justified by benchmarking") + print(" 3. Review if all linked docs are necessary for each workflow") + return False + + +def main(): + parser = argparse.ArgumentParser(description='Measure context budget for workflows') + parser.add_argument('--max-budget', type=int, default=700, + help='Maximum context budget in lines (default: 700)') + parser.add_argument('--fail-on-violation', action='store_true', + help='Exit with error code if budget exceeded') + + args = parser.parse_args() + + base_dir = Path.cwd() + + results = [] + for workflow in WORKFLOWS: + results.append(workflow.measure(base_dir)) + + passed = print_summary(results, args.max_budget) + + if args.fail_on_violation and not passed: + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/agentic/scripts/measure-navigation-depth.py b/agentic/scripts/measure-navigation-depth.py new file mode 100644 index 0000000000..48753504b5 --- /dev/null +++ b/agentic/scripts/measure-navigation-depth.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +""" +Measure navigation depth from AGENTS.md to all documentation. + +Metrics: +- Maximum hop count from AGENTS.md +- Average hop count +- Unreachable documents +- Per-document depth distribution + +Usage: + python3 agentic/scripts/measure-navigation-depth.py + python3 agentic/scripts/measure-navigation-depth.py --max-depth 3 --fail-on-violation +""" + +import re +import sys +import argparse +from pathlib import Path +from collections import defaultdict, deque +from typing import Dict, Set, List + + +def extract_markdown_links(file_path: Path, base_dir: Path) -> Set[Path]: + """Extract all relative markdown links from a file.""" + links = set() + + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + link_pattern = r'\[([^\]]+)\]\(([^\)]+)\)|(?:^|\s)((?:\.{1,2}/|\./)[^\s\)]+\.md)' + + for match in re.finditer(link_pattern, content, re.MULTILINE): + link = match.group(2) if match.group(2) else match.group(3) + + if not link: + continue + + if link.startswith(('http://', 'https://', '#', 'mailto:')): + continue + + link = link.split('#')[0] + + if not link or not link.endswith('.md'): + continue + + link_path = (file_path.parent / link).resolve() + + try: + link_path.relative_to(base_dir) + if link_path.exists(): + links.add(link_path) + except ValueError: + pass + + except Exception as e: + print(f"Warning: Could not parse {file_path}: {e}", file=sys.stderr) + + return links + + +def build_link_graph(base_dir: Path, entry_point: Path) -> Dict[Path, Set[Path]]: + """Build a directed graph of markdown links.""" + graph = defaultdict(set) + visited = set() + to_visit = {entry_point} + + while to_visit: + current = to_visit.pop() + if current in visited: + continue + + visited.add(current) + links = extract_markdown_links(current, base_dir) + graph[current] = links + + to_visit.update(links - visited) + + return dict(graph) + + +def calculate_depths(graph: Dict[Path, Set[Path]], entry_point: Path) -> Dict[Path, int]: + """Calculate shortest path distance from entry_point to all nodes using BFS.""" + depths = {entry_point: 0} + queue = deque([entry_point]) + + while queue: + current = queue.popleft() + current_depth = depths[current] + + for neighbor in graph.get(current, set()): + if neighbor not in depths: + depths[neighbor] = current_depth + 1 + queue.append(neighbor) + + return depths + + +def find_all_docs(base_dir: Path, patterns: List[str]) -> Set[Path]: + """Find all documentation files that should be reachable.""" + docs = set() + for pattern in patterns: + docs.update(base_dir.glob(pattern)) + return docs + + +def analyze_navigation(base_dir: Path, entry_point: Path, max_depth: int = 3) -> Dict: + """Analyze navigation structure and return metrics.""" + print("Building link graph...") + graph = build_link_graph(base_dir, entry_point) + + print("Calculating navigation depths...") + depths = calculate_depths(graph, entry_point) + + expected_docs = find_all_docs(base_dir, [ + 'agentic/**/*.md', + 'AGENTS.md', + 'ARCHITECTURE.md', + ]) + + reachable_all = set(depths.keys()) + reachable_expected = expected_docs & reachable_all + unreachable = expected_docs - reachable_all + + all_docs = expected_docs + over_limit = {doc: depth for doc, depth in depths.items() if depth > max_depth} + + if depths: + max_observed_depth = max(depths.values()) + avg_depth = sum(depths.values()) / len(depths) + depth_distribution = defaultdict(int) + for depth in depths.values(): + depth_distribution[depth] += 1 + else: + max_observed_depth = 0 + avg_depth = 0 + depth_distribution = {} + + return { + 'entry_point': entry_point, + 'max_depth_limit': max_depth, + 'max_observed_depth': max_observed_depth, + 'avg_depth': avg_depth, + 'total_docs': len(all_docs), + 'reachable_docs': len(reachable_expected), + 'unreachable_docs': unreachable, + 'over_limit_docs': over_limit, + 'depth_distribution': dict(depth_distribution), + 'all_depths': depths + } + + +def print_report(analysis: Dict, verbose: bool = False): + """Print analysis report.""" + print("\n" + "=" * 70) + print("NAVIGATION DEPTH ANALYSIS") + print("=" * 70) + print(f"Entry Point: {analysis['entry_point'].name}") + print(f"Max Depth Limit: {analysis['max_depth_limit']} hops") + print() + + print("SUMMARY") + print("-" * 70) + print(f" Total documents found: {analysis['total_docs']}") + print(f" Reachable documents: {analysis['reachable_docs']}") + print(f" Unreachable documents: {len(analysis['unreachable_docs'])}") + print(f" Max observed depth: {analysis['max_observed_depth']} hops") + print(f" Average depth: {analysis['avg_depth']:.2f} hops") + print(f" Docs exceeding limit: {len(analysis['over_limit_docs'])}") + print() + + print("DEPTH DISTRIBUTION") + print("-" * 70) + for depth in sorted(analysis['depth_distribution'].keys()): + count = analysis['depth_distribution'][depth] + bar = "=" * min(count, 50) + print(f" {depth} hops: {count:3d} docs {bar}") + print() + + if analysis['over_limit_docs']: + print(f"DOCS EXCEEDING {analysis['max_depth_limit']} HOPS") + print("-" * 70) + for doc, depth in sorted(analysis['over_limit_docs'].items(), key=lambda x: x[1], reverse=True): + rel_path = doc.relative_to(Path.cwd()) + print(f" {depth} hops: {rel_path}") + print() + + if analysis['unreachable_docs']: + print("UNREACHABLE DOCUMENTS") + print("-" * 70) + for doc in sorted(analysis['unreachable_docs']): + rel_path = doc.relative_to(Path.cwd()) + print(f" {rel_path}") + print() + + print("RESULT") + print("-" * 70) + + issues = [] + if analysis['over_limit_docs']: + issues.append(f"{len(analysis['over_limit_docs'])} docs exceed max depth") + if analysis['unreachable_docs']: + issues.append(f"{len(analysis['unreachable_docs'])} docs unreachable") + + if issues: + print(f"FAILED: {', '.join(issues)}") + print() + return False + else: + print(f"PASSED: All docs reachable within {analysis['max_depth_limit']} hops") + print() + return True + + +def main(): + parser = argparse.ArgumentParser(description='Measure navigation depth in agentic docs') + parser.add_argument('--entry-point', default='AGENTS.md', help='Entry point file (default: AGENTS.md)') + parser.add_argument('--max-depth', type=int, default=3, help='Maximum allowed hop count (default: 3)') + parser.add_argument('--fail-on-violation', action='store_true', help='Exit with error code if violations found') + parser.add_argument('--verbose', '-v', action='store_true', help='Show all document depths') + + args = parser.parse_args() + + base_dir = Path.cwd() + entry_point = base_dir / args.entry_point + + if not entry_point.exists(): + print(f"Error: Entry point not found: {entry_point}", file=sys.stderr) + sys.exit(1) + + analysis = analyze_navigation(base_dir, entry_point, args.max_depth) + passed = print_report(analysis, verbose=args.verbose) + + if args.fail_on_violation and not passed: + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/agentic/scripts/test-metrics.sh b/agentic/scripts/test-metrics.sh new file mode 100755 index 0000000000..d03cd251a7 --- /dev/null +++ b/agentic/scripts/test-metrics.sh @@ -0,0 +1,147 @@ +#!/bin/bash +# Validation tests for metrics scripts +# Run this to verify metrics calculations are correct + +if [ -z "$BASH_VERSION" ]; then + echo "ERROR: This is a Bash script, not a Python script" + echo "" + echo "Correct usage:" + echo " ./agentic/scripts/test-metrics.sh" + echo " bash agentic/scripts/test-metrics.sh" + exit 1 +fi + +set -e + +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) +cd "$REPO_ROOT" + +SCRIPT_DIR="agentic/scripts" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo "================================================================" +echo " METRICS VALIDATION TESTS " +echo "================================================================" +echo "" + +PASS=0 +FAIL=0 + +# Test 1: Navigation metrics math +echo "Test 1: Navigation metrics math (total = reachable + unreachable)" +OUTPUT=$(python3 "$SCRIPT_DIR/measure-navigation-depth.py" 2>&1) + +TOTAL=$(echo "$OUTPUT" | grep "Total documents found:" | awk '{print $4}') +REACHABLE=$(echo "$OUTPUT" | grep "Reachable documents:" | awk '{print $3}') +UNREACHABLE=$(echo "$OUTPUT" | grep "Unreachable documents:" | awk '{print $3}') + +SUM=$((REACHABLE + UNREACHABLE)) + +if [ "$TOTAL" -eq "$SUM" ]; then + echo -e "${GREEN}PASS${NC}: Total ($TOTAL) = Reachable ($REACHABLE) + Unreachable ($UNREACHABLE)" + PASS=$((PASS + 1)) +else + echo -e "${RED}FAIL${NC}: Total ($TOTAL) != Reachable ($REACHABLE) + Unreachable ($UNREACHABLE) = $SUM" + FAIL=$((FAIL + 1)) +fi + +# Test 2: Navigation depth is reasonable +echo "Test 2: Max navigation depth is reasonable (<=10 hops)" +MAX_DEPTH=$(echo "$OUTPUT" | grep "Max observed depth:" | awk '{print $4}') + +if [ "$MAX_DEPTH" -le 10 ]; then + echo -e "${GREEN}PASS${NC}: Max depth ($MAX_DEPTH) is reasonable" + PASS=$((PASS + 1)) +else + echo -e "${RED}FAIL${NC}: Max depth ($MAX_DEPTH) seems too high" + FAIL=$((FAIL + 1)) +fi + +# Test 3: Context budget workflows count +echo "Test 3: Context budget has workflows defined" +BUDGET_OUTPUT=$(python3 "$SCRIPT_DIR/measure-context-budget.py" 2>&1) + +WORKFLOW_COUNT=$(echo "$BUDGET_OUTPUT" | grep -c "Status:" || echo 0) + +if [ "$WORKFLOW_COUNT" -ge 3 ]; then + echo -e "${GREEN}PASS${NC}: Found $WORKFLOW_COUNT workflows" + PASS=$((PASS + 1)) +else + echo -e "${YELLOW}WARN${NC}: Only found $WORKFLOW_COUNT workflows (expected >=3)" + PASS=$((PASS + 1)) +fi + +# Test 4: AGENTS.md exists and is entry point +echo "Test 4: AGENTS.md exists and is readable" +if [ -f "AGENTS.md" ] && [ -r "AGENTS.md" ]; then + AGENTS_LINES=$(wc -l < AGENTS.md) + if [ "$AGENTS_LINES" -le 150 ]; then + echo -e "${GREEN}PASS${NC}: AGENTS.md exists and is $AGENTS_LINES lines (<=150)" + PASS=$((PASS + 1)) + else + echo -e "${RED}FAIL${NC}: AGENTS.md is $AGENTS_LINES lines (should be <=150)" + FAIL=$((FAIL + 1)) + fi +else + echo -e "${RED}FAIL${NC}: AGENTS.md not found or not readable" + FAIL=$((FAIL + 1)) +fi + +# Test 5: All scripts exist +echo "Test 5: Required scripts exist" +REQUIRED_SCRIPTS=( + "$SCRIPT_DIR/measure-navigation-depth.py" + "$SCRIPT_DIR/measure-context-budget.py" + "$SCRIPT_DIR/measure-all-metrics.sh" + "$SCRIPT_DIR/generate-metrics-dashboard.py" +) + +SCRIPT_PASS=true +for script in "${REQUIRED_SCRIPTS[@]}"; do + if [ -f "$script" ] && [ -r "$script" ]; then + : # Script exists + else + echo -e "${RED} Missing: $script${NC}" + SCRIPT_PASS=false + fi +done + +if [ "$SCRIPT_PASS" = true ]; then + echo -e "${GREEN}PASS${NC}: All required scripts found" + PASS=$((PASS + 1)) +else + echo -e "${RED}FAIL${NC}: Some scripts missing" + FAIL=$((FAIL + 1)) +fi + +# Test 6: Dashboard generation doesn't error +echo "Test 6: HTML dashboard can be generated" +if python3 "$SCRIPT_DIR/generate-metrics-dashboard.py" --output /tmp/test-dashboard.html 2>&1 | grep -q "Dashboard generated"; then + echo -e "${GREEN}PASS${NC}: Dashboard generated successfully" + PASS=$((PASS + 1)) + rm -f /tmp/test-dashboard.html +else + echo -e "${RED}FAIL${NC}: Dashboard generation failed" + FAIL=$((FAIL + 1)) +fi + +# Summary +echo "" +echo "================================================================" +echo "RESULTS" +echo "================================================================" +echo -e " Passed: ${GREEN}$PASS${NC}" +echo -e " Failed: ${RED}$FAIL${NC}" +echo "" + +if [ "$FAIL" -eq 0 ]; then + echo -e "${GREEN}ALL TESTS PASSED${NC}" + exit 0 +else + echo -e "${RED}SOME TESTS FAILED${NC}" + exit 1 +fi