diff --git a/.bootc-dev-infra-commit.txt b/.bootc-dev-infra-commit.txt new file mode 100644 index 0000000..4a51c41 --- /dev/null +++ b/.bootc-dev-infra-commit.txt @@ -0,0 +1 @@ +0c34c5b394926053b2fbe1106320f37bba500afa diff --git a/.cursorrules b/.cursorrules new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/.cursorrules @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/.gemini/config.yaml b/.gemini/config.yaml new file mode 100644 index 0000000..080ba11 --- /dev/null +++ b/.gemini/config.yaml @@ -0,0 +1,19 @@ +# NOTE: This file is canonically maintained in +# +# DO NOT EDIT +# +# This config mainly overrides `summary: false` by default +# as it's really noisy. +have_fun: true +code_review: + disable: false + # Even medium level can be quite noisy, I don't think + # we need LOW. Anyone who wants that type of stuff should + # be able to get it locally or before review. + comment_severity_threshold: MEDIUM + max_review_comments: -1 + pull_request_opened: + help: false + summary: false # turned off by default + code_review: true +ignore_patterns: [] diff --git a/AGENTS.md b/AGENTS.md deleted file mode 120000 index 681311e..0000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -CLAUDE.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6126130 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,76 @@ + + +# Instructions for AI agents + +## CRITICAL instructions for generating commits + +### Signed-off-by + +Human review is required for all code that is generated +or assisted by a large language model. If you +are a LLM, you MUST NOT include a `Signed-off-by` +on any automatically generated git commits. Only explicit +human action or request should include a Signed-off-by. +If for example you automatically create a pull request +and the DCO check fails, tell the human to review +the code and give them instructions on how to add +a signoff. + +### Attribution and AI disclosure + +You SHOULD insert an `Assisted-by: AI` tag when the commit contains +substantial assistance, and `Generated-by: AI` when the commit is +effectively entirely generated. + +Do NOT add `Co-developed-by`, and do NOT reference specific +model names or tools because these can be considered a form of advertising. + +For new contributors, when using AI you SHOULD include in at least the pull +request description a rough outline of the human's level of review and +knowledge: + +> Assisted-by: AI +> Unit tests are LLM generated. + +> Generated-by: AI +> I am knowledgeable in this problem domain and reviewed it carefully. + +> Generated-by: AI +> I don't know Rust|Go|... well, but I did test this and it fixed the problem. + +### Large changes + +If the generated code is more than ~500 lines of substantial (non-whitespace) code, +encourage the human to file a design issue first to be reviewed by other maintainers. + +### Pull request size + +It is *very strongly* encouraged to split up "preparatory" commits +that are independently reviewable from the main PR, and submit those separately. + +### Commit messages and text + +Software can be machine checked (via compilation and unit/integration tests) +but natural languages like English cannot. Encourage the human to review +the commit message text. + +## Code guidelines + +The [REVIEW.md](REVIEW.md) file describes expectations around +testing, code quality, commit messages, commit organization, etc. +Language-specific guidelines are in +[REVIEW_RUST.md](REVIEW_RUST.md) and +[REVIEW_GOLANG.md](REVIEW_GOLANG.md). If you're +creating a change, it is strongly encouraged after each +commit and especially when the agent thinks a task is complete +to spawn a subagent to perform a review using guidelines (alongside +looking for any other issues). + +If the agent is performing a review of other's code, the same +principles apply. + +## Follow other guidelines + +Look at the project README.md and look for guidelines +related to contribution, such as a CONTRIBUTING.md +and follow those. diff --git a/REVIEW.md b/REVIEW.md new file mode 100644 index 0000000..3068dd5 --- /dev/null +++ b/REVIEW.md @@ -0,0 +1,218 @@ +# Code Review Guidelines + +These guidelines are derived from analysis of code reviews across the bootc-dev +organization (October–December 2024). They represent the collective expectations +and standards that have emerged from real review feedback. + +## Testing + +Tests are expected for all non-trivial changes - unit and integration by default. + +If there's something that's difficult to write a test for at the current time, +please do at least state if it was tested manually. + +### Choosing the Right Test Type + +Unit tests are appropriate for parsing logic, data transformations, and +self-contained functions. Use integration tests for anything that involves +running containers or VMs. + +Default to table-driven tests instead of having a separate unit test per +case. Especially LLMs like to generate the latter, but it can become +too verbose. Context windows matter to both humans and LLMs reading the +code later (this applies outside of unit tests too of course, but it's +easy to generate a *lot* of code for unit tests unnecessarily). + +### Separating Parsing from I/O + +A recurring theme is structuring code for testability. Split parsers from data +reading: have the parser accept the raw data (e.g. a string), then have a +separate function that reads from disk and calls the parser. This makes unit +testing straightforward without filesystem dependencies. See the +language-specific review guides for concrete examples. + +### Test Assertions + +Make assertions strict and specific. Don't just verify that code "didn't crash"— +check that outputs match expected values. When adding new commands or output +formats, tests should verify the actual content, not just that something was +produced. + +## Code Quality + +### Parsing Structured Data + +Never parse structured data formats (JSON, YAML, XML) with text tools like `grep` +or `sed`. + +### Shell Scripts + +Try to avoid having shell script longer than 50 lines. This commonly occurs +in build system and tests. For the build system, usually there's higher +level ways to structure things (Justfile e.g.). + +### Constants and Magic Values + +Extract magic numbers into named constants. Any literal number that isn't +immediately obvious—buffer sizes, queue lengths, retry counts, timeouts—should +be a constant with a descriptive name. The same applies to magic strings: +deduplicate repeated paths, configuration keys, and other string literals. + +When values aren't self-explanatory, add a comment explaining why that specific +value was chosen. + +### Don't ignore (swallow) errors + +Avoid swallowing errors (e.g. `foo 2>/dev/null || true` in shell script). +Most errors should be propagated by default. If not, it's usually appropriate +to at least log error messages at a debug level. See the language-specific +review guides for concrete anti-patterns. + +Handle edge cases explicitly: missing data, malformed input, offline systems. +Error messages should provide clear context for diagnosis. + +### Code Organization + +Separate concerns: I/O operations, parsing logic, and business logic belong in +different functions. Structure code so core logic can be unit tested without +external dependencies. + +It can be OK to duplicate a bit of code in a slightly different form twice, +but having it happen in 3 places asks for deduplication. + +## Commits and Pull Requests + +### Commit Organization + +Break changes into logical, atomic commits. Reviewers appreciate being able to +follow your reasoning: "Especially grateful for breaking it up into individual +commits so I can more easily follow your train of thought." + +Preparatory refactoring should be separate from behavioral changes. Each commit +should tell a clear story and be reviewable independently. Where applicable, +create "prep" commits that could be merged separately from the behavioral change. + +### Commit Messages + +Write clear and descriptive commit messages using a `component: Summary` +subject, such as `kernel: Add find API w/correct hyphen-dash equality, add docs`. +Use imperative mood: "Add integration with..." not "Adds integration with...". + +The body of the commit should start with at least one sentence (or paragraph) +describing **why** the change is being made, even for something apparently +trivial. For example a "refactor" commit might have a "why" rationale of just +"Prep for handling X later." A big commit introducing a feature may seem +self-explanatory, but there is often ambient context like "A large-scale Debian +user wanted this" that provides helpful grounding in the motivation. + +If there's a linked tracking issue, often that will contain a more extensive +rationale that doesn't need to be duplicated entirely in the commit message, +but do ensure the commit message has something useful on its own for a rationale. + +Keep it natural and concise. A few sentences of prose explaining the design +intent or the high-level data flow is often good enough. If there's a +non-obvious consequence of the change, call it out briefly (e.g. "Note the +manifest becomes part of the GC root") rather than explaining the full +mechanism. Think about what a reviewer needs to know that may not be obvious +from a skim of the code. + +Do not restate obvious parts of what is already visible in the commit diff: + +- "Changed function X to call Y" +- Generic `Changes:` sections with bulleted lists of implementation details +- "Files changed" sections — completely redundant with git + +Implementation details belong in the code documentation. The goal of the +commit message is like a "cover letter" for the change, with a primary +rationale of why the change is being made, alongside a concise summary of +its implementation. + +Another thing that can go in the commit message is brief descriptions +of alternative approaches that were considered and discarded. + +Closes: tags should generally come at the end of the commit message. + +### PR Descriptions + +Generally, just restate the commit message. + +Where it makes sense, it is OK to include additional details though. + +### Further changes on top of existing commits + +If you have followup fixes (whether that's part of a local loop or +as part of addressing PR review), it is generally encouraged to *squash* +the fixes into the prior commit. Do not create generically-named "Update " commits +or "Address review feedback" or "Fix cargo fmt" commits. + +This applies equally when an AI tool (e.g. Gemini, Copilot) suggests a +change via a review comment — applying the suggestion creates a new commit +with an auto-generated subject. That commit should be squashed before the +PR is merged. + +In other words either a commit "stands alone" with its own rationale or it doesn't. + +### Keeping PRs Current + +Keep PRs rebased on main. When CI failures are fixed in other PRs, rebase to +pick up the fixes. Reference the fixing PR when noting that a rebase is needed. + +### Before Merge + +Self-review your diff before requesting review. Catch obvious issues yourself +rather than burning reviewer cycles. + +Do not add `Signed-off-by` lines automatically—these require explicit human +action after review. If code was AI-assisted, include an `Assisted-by:` trailer +indicating the tool and model used. + +## Architecture and Design + +### Workarounds vs Proper Fixes + +When implementing a workaround, document where the proper fix belongs and link +to relevant upstream issues. Invest time investigating proper fixes before +settling on workarounds. + +### Cross-Project Considerations + +Prefer pushing fixes upstream when the root cause is in a dependency. Reduce +scope where possible; don't reimplement functionality that belongs elsewhere. + +When multiple systems interact (like Renovate and custom sync tooling), be +explicit about which system owns what and how they coordinate. + +### Avoiding Regressions + +Verify that new code paths handle all cases the old code handled. When rewriting +functionality, ensure equivalent coverage exists. + +### Review Requirements + +When multiple contributors co-author a PR, bring in an independent reviewer. + +## Dependencies + +New dependencies should be justified. Consider alternatives: "I'm curious if +you did any comparative analysis at all with alternatives?" + +Prefer well-maintained libraries with active communities. Glance at existing +reverse dependencies to gauge adoption (e.g. on crates.io for Rust, or +pkg.go.dev for Go). Consider project-level dependency policies (e.g. +`cargo deny` for Rust). + +## API Design + +When adding new commands or options, think about machine-readable output early. +JSON is generally preferred for that. + +Keep helper functions in appropriate modules. Move command output formatting +close to the CLI layer, keeping core logic functions focused on their primary +purpose. + +## Language-Specific Guidance + +The following guides cover language-specific review expectations: + +- [REVIEW_RUST.md](REVIEW_RUST.md) — Rust projects +- [REVIEW_GOLANG.md](REVIEW_GOLANG.md) — Go projects diff --git a/REVIEW_GOLANG.md b/REVIEW_GOLANG.md new file mode 100644 index 0000000..1199339 --- /dev/null +++ b/REVIEW_GOLANG.md @@ -0,0 +1,130 @@ +# Go-Specific Review Guidelines + +These guidelines supplement the general [REVIEW.md](REVIEW.md) with +Go-specific expectations. + +## Separating Parsing from I/O + +Have the parser accept a string or `io.Reader`, then have a separate function +that opens the file and calls the parser: + +```go +// ✅ Good: parser is a pure function, easy to unit test +func parseConfig(data string) (*Config, error) { ... } + +func loadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return parseConfig(string(data)) +} +``` + +## Don't Ignore (Swallow) Errors + +Avoid discarding errors with the blank identifier. Most errors should be +returned to the caller. If not, at least log the error: + +```go +// ❌ Avoid: error is silently swallowed +_ = doSomething() + +// ✅ Good: propagate +if err := doSomething(); err != nil { + return err +} + +// ✅ OK if the error is truly ignorable: log it +if err := doSomething(); err != nil { + log.Debug("ignoring error", "err", err) +} +``` + +## Gomega and Test Assertions + +We use [gomega](https://github.com/onsi/gomega) for test assertions. Follow +these conventions: + +### Use `g.Eventually` for Polling + +Gomega's `Eventually` handles polling, timeouts, and failure reporting. + +### Return `(T, error)` from `Eventually` Callbacks + +Return the specific field you care about and let gomega matchers describe the +expectation declaratively. This produces better failure messages because +gomega can show what the value actually was vs. what was expected. + +```go +// ✅ Good: return the field, match with gomega +g.Eventually(func() ([]metav1.Condition, error) { + var p bootcv1alpha1.BootcNodePool + err := k8sClient.Get(ctx, client.ObjectKey{Name: name}, &p) + return p.Status.Conditions, err +}).Should(ContainElement(And( + HaveField("Type", bootcv1alpha1.PoolDegraded), + HaveField("Status", metav1.ConditionTrue), + HaveField("Reason", bootcv1alpha1.PoolNodeDegraded), +))) + +// ❌ Avoid: assertions inside the callback with Succeed() +g.Eventually(func(g Gomega) { + var p bootcv1alpha1.BootcNodePool + g.Expect(k8sClient.Get(ctx, ...)).To(Succeed()) + cond := apimeta.FindStatusCondition(p.Status.Conditions, ...) + g.Expect(cond).NotTo(BeNil()) + g.Expect(cond.Status).To(Equal(...)) +}).Should(Succeed()) +``` + +### Return the Narrowest Type + +Extract exactly the field you want to assert on — labels, conditions, +ownerReference — rather than returning the whole object or a `bool`: + +```go +// Labels +g.Eventually(func() (map[string]string, error) { + var n corev1.Node + err := k8sClient.Get(ctx, client.ObjectKey{Name: name}, &n) + return n.Labels, err +}).Should(HaveKey(bootcv1alpha1.LabelManaged)) + +// OwnerReference +g.Eventually(func() (*metav1.OwnerReference, error) { + var bn bootcv1alpha1.BootcNode + err := k8sClient.Get(ctx, client.ObjectKey{Name: name}, &bn) + return metav1.GetControllerOf(&bn), err +}).Should(And(Not(BeNil()), HaveField("Name", pool.Name))) +``` + +### Use Composed Matchers for Struct Assertions + +Prefer `HaveField` and `ContainElement(And(...))` to match on struct fields +declaratively rather than manually extracting fields and asserting one by one: + +```go +// ✅ Good: declarative, one expression +g.Expect(conditions).To(ContainElement(And( + HaveField("Type", bootcv1alpha1.PoolDegraded), + HaveField("Status", metav1.ConditionTrue), + HaveField("Reason", bootcv1alpha1.PoolInvalidSpec), +))) + +// ❌ Avoid: manual lookup + sequential field assertions +cond := apimeta.FindStatusCondition(conditions, bootcv1alpha1.PoolDegraded) +g.Expect(cond).NotTo(BeNil()) +g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) +g.Expect(cond.Reason).To(Equal(bootcv1alpha1.PoolInvalidSpec)) +``` + +### Assert Specific Errors When Expected + +When a test expects a particular error, match on the concrete error type or +value: + +```go +// ✅ Good: we know the API server should reject this +g.Expect(err).To(MatchError(apierrors.IsInvalid, "IsInvalid")) +``` diff --git a/REVIEW_RUST.md b/REVIEW_RUST.md new file mode 100644 index 0000000..ac7983b --- /dev/null +++ b/REVIEW_RUST.md @@ -0,0 +1,60 @@ +# Rust-Specific Review Guidelines + +These guidelines supplement the general [REVIEW.md](REVIEW.md) with +Rust-specific expectations. + +## Separating Parsing from I/O + +Have the parser accept a `&str`, then have a separate function that reads from +disk and calls the parser: + +```rust +// ✅ Good: parser is a pure function, easy to unit test +fn parse_config(data: &str) -> Result { ... } + +fn load_config(path: &Path) -> Result { + let data = std::fs::read_to_string(path)?; + parse_config(&data) +} +``` + +## Don't Ignore (Swallow) Errors + +Avoid the `if let Ok(v) = ... { }` pattern which silently discards the error +branch. Most errors should be propagated with `?`. If not, at least log the +error: + +```rust +// ❌ Avoid: error is silently swallowed +if let Ok(v) = do_something() { + use_value(v); +} + +// ✅ Good: propagate +let v = do_something()?; + +// ✅ OK if the error is truly ignorable: log it +match do_something() { + Ok(v) => use_value(v), + Err(e) => tracing::debug!("ignoring error: {e}"), +} +``` + +## Shell Scripts + +Several of our projects use the `cargo xtask` pattern to put arbitrary "glue" +code in Rust using the `xshell` crate to keep it easy to run external commands. +This is preferred over long shell scripts. + +## Code Formatting + +After making changes to any .rs files, `cargo fmt` should be run. +There are CI jobs that lint and expect `cargo fmt` to be a no-op. +Failing to properly format rust code will result in wasted CI +resources. + +## General + +Prefer rustix over `libc`. All `unsafe` code must be very carefully +justified. +