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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bootc-dev-infra-commit.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
56e4f615d38cc4a923f6a7e2a174a0c05a962451
ec12516f14c24e8bf3a86ccc7c478e57e03009f7
1 change: 1 addition & 0 deletions .cursorrules
49 changes: 42 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,57 @@ and the DCO check fails, tell the human to review
the code and give them instructions on how to add
a signoff.

### Attribution
### Attribution and AI disclosure

When generating substantial amounts of code, you SHOULD
include an `Assisted-by: TOOLNAME (MODELNAME)`. For example,
`Assisted-by: Goose (Sonnet 4.5)`.
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 organization, etc. If you're
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 you think a task is complete
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 you are performing a review of other's code, the same
If the agent is performing a review of other's code, the same
principles apply.

## Follow other guidelines
Expand Down
117 changes: 81 additions & 36 deletions REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ 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 a `&str`, then have a separate function that
reads from disk and calls the parser. This makes unit testing straightforward
without filesystem dependencies.
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

Expand All @@ -48,9 +49,7 @@ or `sed`.

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.) and 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.
level ways to structure things (Justfile e.g.).

### Constants and Magic Values

Expand All @@ -64,10 +63,10 @@ value was chosen.

### Don't ignore (swallow) errors

Avoid the `if let Ok(v) = ... { }` in Rust, or `foo 2>/dev/null || true`
pattern in shell script by default. Most errors should be propagated by
default. If not, it's usually appropriate to at least log error messages
at a `tracing::debug!` or equivalent level.
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.
Expand All @@ -90,24 +89,68 @@ 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. Commit messages should
explain the "why" not just the "what," and use imperative mood ("Add feature"
not "Added feature").
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

PRs should link to the issues they address using `Closes:` or `Fixes:` with
full URLs. One reviewer noted: "I edited this issue just now to have
`Closes: <URL>` but let's try to be sure we're doing that kind of thing in
general in the future."
Generally, just restate the commit message.

Document known limitations and caveats explicitly. When approaches have tradeoffs
or don't fully solve a problem, say so. For complex investigations, use collapsible
`<details>` sections to include debugging notes without cluttering the main
description.
Where it makes sense, it is OK to include additional details though.

Think about broader implications: "But we'll have this problem across all repos
right?" Consider how your change affects the wider ecosystem.
### 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 <file>" 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

Expand All @@ -123,7 +166,6 @@ 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
Expand All @@ -149,25 +191,28 @@ functionality, ensure equivalent coverage exists.

When multiple contributors co-author a PR, bring in an independent reviewer.

## Rust-Specific Guidance

Prefer rustix over `libc`. All `unsafe` code must be very carefully
justified.
## Dependencies

### Dependencies
New dependencies should be justified. Consider alternatives: "I'm curious if
you did any comparative analysis at all with alternatives?"

New dependencies should be justified. Glance at existing reverse dependencies
on crates.io to see if a crate is widely used. 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).

Prefer well-maintained crates with active communities. Consider `cargo deny`
policies when adding dependencies.

### API Design
## 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
130 changes: 130 additions & 0 deletions REVIEW_GOLANG.md
Original file line number Diff line number Diff line change
@@ -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"))
```
Loading