diff --git a/.bootc-dev-infra-commit.txt b/.bootc-dev-infra-commit.txt new file mode 100644 index 0000000..e0954c6 --- /dev/null +++ b/.bootc-dev-infra-commit.txt @@ -0,0 +1 @@ +574cc5b9bf84f88a9cef3073adda95e4acfc7318 diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 120000 index 0000000..be77ac8 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file 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/.devcontainer/debian/devcontainer.json b/.devcontainer/debian/devcontainer.json new file mode 100644 index 0000000..f1b43a8 --- /dev/null +++ b/.devcontainer/debian/devcontainer.json @@ -0,0 +1,30 @@ +{ + "name": "bootc-devenv-debian", + "image": "ghcr.io/bootc-dev/devenv-debian", + "customizations": { + "vscode": { + // Arbitrary, but most of our code is in one of these two + "extensions": [ + "rust-lang.rust-analyzer", + "golang.Go" + ] + }, + "devaipod": { + // When running under devaipod, use minimal capabilities + // (SYS_ADMIN, NET_ADMIN, etc.) instead of full --privileged. + "nestedContainers": true + } + }, + "features": {}, + // Use privileged mode for broad compatibility (Codespaces, Docker, + // stock devcontainer CLI). devaipod overrides this with tighter + // security via the nestedContainers customization above. + "privileged": true, + "postCreateCommand": { + // Our init script + "devenv-init": "sudo /usr/local/bin/devenv-init.sh" + }, + "remoteEnv": { + "PATH": "${containerEnv:PATH}:/usr/local/cargo/bin" + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..f1b43a8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,30 @@ +{ + "name": "bootc-devenv-debian", + "image": "ghcr.io/bootc-dev/devenv-debian", + "customizations": { + "vscode": { + // Arbitrary, but most of our code is in one of these two + "extensions": [ + "rust-lang.rust-analyzer", + "golang.Go" + ] + }, + "devaipod": { + // When running under devaipod, use minimal capabilities + // (SYS_ADMIN, NET_ADMIN, etc.) instead of full --privileged. + "nestedContainers": true + } + }, + "features": {}, + // Use privileged mode for broad compatibility (Codespaces, Docker, + // stock devcontainer CLI). devaipod overrides this with tighter + // security via the nestedContainers customization above. + "privileged": true, + "postCreateCommand": { + // Our init script + "devenv-init": "sudo /usr/local/bin/devenv-init.sh" + }, + "remoteEnv": { + "PATH": "${containerEnv:PATH}:/usr/local/cargo/bin" + } +} diff --git a/.devcontainer/ubuntu/devcontainer.json b/.devcontainer/ubuntu/devcontainer.json new file mode 100644 index 0000000..8a6167a --- /dev/null +++ b/.devcontainer/ubuntu/devcontainer.json @@ -0,0 +1,30 @@ +{ + "name": "bootc-devenv-ubuntu", + "image": "ghcr.io/bootc-dev/devenv-ubuntu", + "customizations": { + "vscode": { + // Arbitrary, but most of our code is in one of these two + "extensions": [ + "rust-lang.rust-analyzer", + "golang.Go" + ] + }, + "devaipod": { + // When running under devaipod, use minimal capabilities + // (SYS_ADMIN, NET_ADMIN, etc.) instead of full --privileged. + "nestedContainers": true + } + }, + "features": {}, + // Use privileged mode for broad compatibility (Codespaces, Docker, + // stock devcontainer CLI). devaipod overrides this with tighter + // security via the nestedContainers customization above. + "privileged": true, + "postCreateCommand": { + // Our init script + "devenv-init": "sudo /usr/local/bin/devenv-init.sh" + }, + "remoteEnv": { + "PATH": "${containerEnv:PATH}:/usr/local/cargo/bin" + } +} 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 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 index 3c1d690..3068dd5 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -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 @@ -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 @@ -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. @@ -192,21 +191,17 @@ functionality, ensure equivalent coverage exists. When multiple contributors co-author a PR, bring in an independent reviewer. -## Rust-Specific Guidance +## Dependencies -Prefer rustix over `libc`. All `unsafe` code must be very carefully -justified. +New dependencies should be justified. Consider alternatives: "I'm curious if +you did any comparative analysis at all with alternatives?" -### Dependencies +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). -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 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. @@ -214,3 +209,10 @@ 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. +