diff --git a/internal/app/azldev/agentskill/agentskill.go b/internal/app/azldev/agentskill/agentskill.go index d6eb9c18..f46b109d 100644 --- a/internal/app/azldev/agentskill/agentskill.go +++ b/internal/app/azldev/agentskill/agentskill.go @@ -69,6 +69,81 @@ var skills = []Skill{ "component, distro config.", bodyTemplate: "azldev.md.tmpl", }, + { + Name: "azldev-mock", + Description: "Read this before testing or inspecting a built RPM; do not drive mock by hand from " + + "memory. Explains how to test and inspect built packages in a mock chroot with 'azldev adv " + + "mock shell', covering non-interactive (heredoc) and interactive chroot workflows, the " + + "-p/--add-package flag, and resetting stale chroot state. Triggers include test package, mock " + + "shell, inspect rpm, smoke test, chroot, verify build output.", + bodyTemplate: "mock.md.tmpl", + }, + { + Name: "azldev-update-component", + Description: "Read this before finalizing a component change, changing source resolution, or " + + "touching a lock file; lock edits are easy to get wrong. Explains how to refresh azldev " + + "component lock files with 'azldev comp update', covering when to run update versus render, the " + + "update/render/commit/re-render/amend workflow, and per-component versus -a refresh. Triggers " + + "include comp update, refresh lock, bump pin, change snapshot, upstream distro, lock drift, " + + "version bump, finalize component.", + bodyTemplate: "update-component.md.tmpl", + }, + { + Name: "azldev-remove-component", + Description: "Read this before deleting or dropping a component; there is no azldev remove " + + "command, so doing it wrong leaves dangling state. Explains the manual removal workflow for " + + "deleting component metadata, cleaning references, and validating any related output-affecting " + + "changes. Triggers include remove component, delete package, drop " + + "component, prune dependency.", + bodyTemplate: "remove-component.md.tmpl", + }, + { + Name: "azldev-overlays", + Description: "Read this before adding, changing, or diagnosing any overlay; never edit a spec or " + + "rendered file from memory. Explains how to modify a component's RPM spec or loose source files " + + "with azldev overlays (semantic patches applied at render time) instead of forking the spec, " + + "covering overlay types, the render-and-inspect loop, common failures, pitfalls, and metadata. " + + "Triggers include overlay, overlay failed, no match, spec-add-tag, spec-remove-tag, patch-add, " + + "fix spec, backport, disable test, prune subpackage, edit spec.", + bodyTemplate: "overlays.md.tmpl", + }, + { + Name: "azldev-comp-toml", + Description: "Read this before authoring, editing, or reviewing a *.comp.toml file; do not work " + + "from memory. Explains the azldev component definition format and review workflow, covering " + + "component structure, spec sources, build config, release calculation, render options, file " + + "organization, overlay hygiene, stale files, disabled tests, and testing verification. " + + "Triggers include comp.toml, component config, review component, component hygiene, spec " + + "source, upstream-distro, build defines, release calculation, includes.", + bodyTemplate: "comp-toml.md.tmpl", + }, + { + Name: "azldev-add-component", + Description: "Read this before adding or importing a component; follow the workflow instead of " + + "guessing. Explains how to add a new component to an azldev distro, covering inspecting the " + + "upstream spec, the inline-versus-dedicated-file decision, and validating with render, " + + "diff-sources, and build. Triggers include add component, new package, import package, create " + + "comp.toml, new component.", + bodyTemplate: "add-component.md.tmpl", + }, + { + Name: "azldev-build-component", + Description: "Read this before building a component or diagnosing a build failure; do not guess " + + "build flags or the inner loop. Explains how to build, iterate on, and debug an azldev " + + "component, covering comp build flags (local-repo, preserve-buildenv), the render/build/test " + + "inner loop, diff-sources, and disabling a failing %check via check.skip. Triggers include " + + "build component, build failed, build error, inner loop, preserve buildenv, local repo, disable " + + "check.", + bodyTemplate: "build-component.md.tmpl", + }, + { + Name: "azldev-image", + Description: "Read this before building, booting, or configuring an azldev image. Explains the " + + "azldev image commands (list, build, boot, test, customize) and the [images.] config " + + "(kiwi definition, capabilities, tests, publish); the kiwi XML format itself is upstream KIWI " + + "NG. Triggers include image build, image boot, kiwi, container image, VM image, images.toml.", + bodyTemplate: "image.md.tmpl", + }, } // Skills returns the registered skills in emission order. @@ -146,6 +221,40 @@ var instructions = []Instruction{ {Skill: SkillName, Purpose: "for how to use the azldev CLI"}, }, }, + { + Name: "comp-toml", + ApplyTo: "**/*.comp.toml,**/components.toml", + Description: "These are azldev component definition files (*.comp.toml). Before editing " + + "or reviewing one, load " + + "the azldev-comp-toml skill (and azldev-overlays for spec changes); do not hand-write " + + "component config from memory. Triggers include comp.toml, component config, spec source, " + + "build defines, release calculation, overlays, review component.", + Title: "Component definition files (`*.comp.toml`)", + Intro: "These files define a distro's components — each one's spec source and how azldev customizes it.", + Skills: []SkillPointer{ + {Skill: "azldev-comp-toml", Purpose: "for the component TOML format and review checklist"}, + {Skill: "azldev-add-component", Purpose: "to add a new component"}, + {Skill: "azldev-overlays", Purpose: "to add or change overlays"}, + {Skill: "azldev-update-component", Purpose: "to refresh a component's lock"}, + {Skill: "azldev-remove-component", Purpose: "to remove a component"}, + }, + }, + { + Name: "rendered-specs", + ApplyTo: "{{ .RenderedSpecsDir }}/**/*", + Description: "Rendered component files produced by 'azldev comp render'. They are build inputs and " + + "must not be hand-edited. Before changing one, load the azldev-overlays or azldev-comp-toml " + + "skill, edit the source, and re-render. Read this when viewing or tempted to edit generated " + + "output.", + Title: "Rendered component files", + Intro: "These files are generated by `azldev comp render` and are build inputs; do not edit them " + + "directly — change the component's `.comp.toml`, overlays, or source files and re-render.", + Skills: []SkillPointer{ + {Skill: "azldev-comp-toml", Purpose: "for the component TOML format"}, + {Skill: "azldev-overlays", Purpose: "to change generated output via overlays"}, + {Skill: "azldev-update-component", Purpose: "to refresh and finalize a component"}, + }, + }, } // Instructions returns the registered instruction files in emission order. diff --git a/internal/app/azldev/agentskill/agentskill_test.go b/internal/app/azldev/agentskill/agentskill_test.go index cd5ef941..964cf97a 100644 --- a/internal/app/azldev/agentskill/agentskill_test.go +++ b/internal/app/azldev/agentskill/agentskill_test.go @@ -5,10 +5,12 @@ package agentskill_test import ( "path" + "reflect" "strings" "testing" "github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/agentskill" + "github.com/microsoft/azure-linux-dev-tools/internal/projectconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" @@ -78,6 +80,29 @@ func TestSkillDocumentUnknown(t *testing.T) { require.Error(t, err) } +func TestSkillDocumentUsesBindings(t *testing.T) { + params := testParams() + params.Bindings = agentskill.Bindings{ + LockDir: "build/locks", + RenderedSpecsDir: "build/specs", + } + + doc, err := agentskill.SkillDocument("azldev-remove-component", params) + require.NoError(t, err) + + // The resolved binding values, not azldev's defaults, appear in the rendered body. + assert.Contains(t, doc, "build/locks/.lock") + assert.Contains(t, doc, "build/specs/") +} + +func TestUpdateComponentSkillStagesRenderedOutputBeforeAmend(t *testing.T) { + doc, err := agentskill.SkillDocument("azldev-update-component", testParams()) + require.NoError(t, err) + + assert.Equal(t, 2, strings.Count(doc, "git add specs///"), + "both amend workflows must stage the post-commit render") +} + func TestSkillFrontmatterInvariants(t *testing.T) { layout := agentskill.DefaultLayout() @@ -115,6 +140,15 @@ func fileByPath(t *testing.T, files []agentskill.EmittedFile, relPath string) ag return agentskill.EmittedFile{} } +func mockSkill(t *testing.T) agentskill.Skill { + t.Helper() + + skill, err := agentskill.FindSkill("azldev-mock") + require.NoError(t, err) + + return skill +} + // instructionByName returns the registered instruction with the given name. func instructionByName(t *testing.T, name string) agentskill.Instruction { t.Helper() @@ -137,6 +171,19 @@ func TestInstructionsRegistry(t *testing.T) { } assert.Contains(t, names, "azldev") + assert.Contains(t, names, "comp-toml") + assert.Contains(t, names, "rendered-specs") + + compToml := instructionByName(t, "comp-toml") + assert.Equal(t, "**/*.comp.toml,**/components.toml", compToml.ApplyTo) + + skillNames := make([]string, 0, len(compToml.Skills)) + for _, pointer := range compToml.Skills { + skillNames = append(skillNames, pointer.Skill) + } + + assert.Contains(t, skillNames, "azldev-comp-toml") + assert.Contains(t, skillNames, "azldev-overlays") } func TestSkillsRegistry(t *testing.T) { @@ -146,6 +193,14 @@ func TestSkillsRegistry(t *testing.T) { } assert.Contains(t, names, agentskill.SkillName) + assert.Contains(t, names, "azldev-mock") + assert.Contains(t, names, "azldev-update-component") + assert.Contains(t, names, "azldev-remove-component") + assert.Contains(t, names, "azldev-overlays") + assert.Contains(t, names, "azldev-comp-toml") + assert.Contains(t, names, "azldev-add-component") + assert.Contains(t, names, "azldev-build-component") + assert.Contains(t, names, "azldev-image") } func TestRegistryAccessorsReturnCopies(t *testing.T) { @@ -196,6 +251,11 @@ func TestFilesWrapper(t *testing.T) { assert.Contains(t, skill, agentskill.ShowSkillToolName) assert.NotContains(t, skill, "Golden rules") + // The mock wrapper also points at the tool but is not the full body. + mockWrapper := fileByPath(t, files, layout.SkillFile(mockSkill(t))).Content + assert.Contains(t, mockWrapper, agentskill.ShowSkillToolName) + assert.NotContains(t, mockWrapper, "Never install built RPMs") + // The azldev instruction wrapper applies to azldev.toml and points at the azldev skill by // name (never the CLI/MCP tool, which may be unavailable in --full installs). azldevInstruction := instructionByName(t, "azldev") @@ -205,6 +265,39 @@ func TestFilesWrapper(t *testing.T) { assert.Contains(t, instructions, "`"+agentskill.SkillName+"`") assert.NotContains(t, instructions, agentskill.ShowSkillToolName) assert.NotContains(t, instructions, "docs agent show") + + // The comp-toml instruction wrapper applies to *.comp.toml and points at its skills by + // name and purpose. + compTomlInstruction := instructionByName(t, "comp-toml") + compTomlWrapper := fileByPath(t, files, agentskill.InstructionFile(compTomlInstruction)).Content + assert.Contains(t, compTomlWrapper, `applyTo: "**/*.comp.toml,**/components.toml"`) + assert.Contains(t, compTomlWrapper, "read the `azldev-comp-toml` skill") + assert.Contains(t, compTomlWrapper, "Read the `azldev-overlays` skill to add or change overlays") + // The file-format skill is required; mutually exclusive workflow skills are conditional. + assert.Contains(t, compTomlWrapper, "You MUST read the `azldev-comp-toml` skill") + assert.NotContains(t, compTomlWrapper, "You MUST read the `azldev-overlays` skill") + assert.NotContains(t, compTomlWrapper, "docs agent show") + + // The rendered-specs wrapper carries the do-not-edit guardrail and a binding-resolved glob. + renderedSpecsInstruction := instructionByName(t, "rendered-specs") + renderedSpecsWrapper := fileByPath(t, files, agentskill.InstructionFile(renderedSpecsInstruction)).Content + assert.Contains(t, renderedSpecsWrapper, `applyTo: "specs/**/*"`) + assert.Contains(t, renderedSpecsWrapper, "do not edit them directly") +} + +func TestRenderedSpecsInstructionApplyToTracksBindings(t *testing.T) { + layout := agentskill.DefaultLayout() + params := testParams() + params.RenderedSpecsDir = "SPECS" + + files, err := agentskill.Files(layout, params, false) + require.NoError(t, err) + + inst := instructionByName(t, "rendered-specs") + wrapper := fileByPath(t, files, agentskill.InstructionFile(inst)).Content + + // The applyTo glob tracks the configured rendered-specs directory. + assert.Contains(t, wrapper, `applyTo: "SPECS/**/*"`) } func TestFilesFull(t *testing.T) { @@ -220,6 +313,7 @@ func TestFilesFull(t *testing.T) { // In full mode each on-disk SKILL.md inlines the complete skill document. assert.Contains(t, fileByPath(t, files, layout.SkillFile(primarySkill(t))).Content, "overlay system") + assert.Contains(t, fileByPath(t, files, layout.SkillFile(mockSkill(t))).Content, "azldev adv mock shell") } func TestFilesGitHubLayout(t *testing.T) { @@ -234,4 +328,51 @@ func TestFilesGitHubLayout(t *testing.T) { // The github layout places skills under .github/skills with their plain (namespaced) names. azldevSkill := fileByPath(t, files, ".github/skills/azldev/SKILL.md") assert.Contains(t, azldevSkill.Content, "name: azldev") + + mockFile := fileByPath(t, files, ".github/skills/azldev-mock/SKILL.md") + assert.Contains(t, mockFile.Content, "name: azldev-mock") +} + +// schemaEnum extracts values from an authoritative jsonschema enum tag. +func schemaEnum(t *testing.T, structType reflect.Type, fieldName string) []string { + t.Helper() + + field, ok := structType.FieldByName(fieldName) + require.Truef(t, ok, "%s must have a %s field", structType.Name(), fieldName) + + var values []string + + for _, part := range strings.Split(field.Tag.Get("jsonschema"), ",") { + if value, found := strings.CutPrefix(part, "enum="); found { + values = append(values, value) + } + } + + require.NotEmptyf(t, values, "expected enum values in %s.%s jsonschema tag", structType.Name(), fieldName) + + return values +} + +// TestOverlaysSkillCoversSchemaEnums is a drift guard: the azldev-overlays skill must +// document every overlay type, metadata category, and upstream status defined in code. +func TestOverlaysSkillCoversSchemaEnums(t *testing.T) { + doc, err := agentskill.SkillDocument("azldev-overlays", agentskill.Params{}) + require.NoError(t, err) + + schemaFields := []struct { + structType reflect.Type + fieldName string + }{ + {reflect.TypeOf(projectconfig.ComponentOverlay{}), "Type"}, + {reflect.TypeOf(projectconfig.OverlayMetadata{}), "Category"}, + {reflect.TypeOf(projectconfig.OverlayMetadata{}), "UpstreamStatus"}, + } + + for _, schemaField := range schemaFields { + for _, value := range schemaEnum(t, schemaField.structType, schemaField.fieldName) { + assert.Containsf(t, doc, "`"+value+"`", + "azldev-overlays skill must document %s.%s value %q", + schemaField.structType.Name(), schemaField.fieldName, value) + } + } } diff --git a/internal/app/azldev/agentskill/content/add-component.md.tmpl b/internal/app/azldev/agentskill/content/add-component.md.tmpl new file mode 100644 index 00000000..f201b172 --- /dev/null +++ b/internal/app/azldev/agentskill/content/add-component.md.tmpl @@ -0,0 +1,93 @@ +--- +name: {{ .Name }} +description: {{ printf "%q" .Description }} +--- + +# Add a component + +## Before you start + +Confirm the component does not already exist: + +```sh +azldev comp list -p -q -O json +``` + +### Inspect the upstream spec first + +The reliable way to see what you are importing (direct web fetches of upstream +dist-git often fail bot detection): + +1. Add a bare entry so azldev can resolve the component: run `azldev comp add ` + (it appends `[components.]` to the root config file), or hand-add + `[components.]` to an included config file if you want it to live elsewhere. + A bare root entry is *perfect* for initial testing, but real distros will usually + segment the configuration into included files. Once the initial pass is done, + ensure the component is in the right place and remove the root entry. + +2. Create the initial lock so source resolution is pinned before inspection: + + ```sh + azldev comp update -p + ``` + +3. Pull the sources without overlays into a scratch dir under the work dir: + + ```sh + azldev comp prep-sources -p --skip-overlays --force -o {{ .WorkDir }}/scratch/ -q + ``` + +4. Read the spec and plan any overlays. + +## Inline vs dedicated file + +- **Inline** — a bare upstream import with no changes stays in a shared config file: + + ```toml + [components.jq] + ``` + +- **Dedicated** — anything that needs overlays, build config, or a local spec gets its + own `/.comp.toml`. Rule of thumb: more than `[components.]` earns a + dedicated file. An `includes = ["**/*.comp.toml"]` glob picks it up automatically. + +`azldev comp add [...]` adds bare `[components.]` entries that inherit +the distro defaults; it writes to the **root** config file and does not scaffold spec or +source files. If your distro keeps components in included or dedicated files, move the +entry there afterward. + +## Customize + +For spec source types and overlays, read the `azldev-comp-toml` and `azldev-overlays` skills. Key +points when adding a component: + +- Prefer **overlays** over forking the spec — overlays get upstream updates for free. + Forking a spec is a last resort and a long-term maintenance commitment. Get explicit + user sign-off first, document every change, and keep the delta minimal. +- Every overlay needs a `description` explaining why it is needed. +- Keep `%check` enabled. Disable it only as a last resort via `build.check.skip = true` + with a required `build.check.skip_reason` (see the `azldev-build-component` skill). + +## Validate + +The initial lock pins the upstream revision you inspected. Refresh it after the +component inputs settle, then validate: + +```sh +azldev comp update -p # resolve the upstream commit and write the lock +azldev comp render -p # apply overlays and write the rendered spec +azldev comp diff-sources -p # see exactly what the overlays change +azldev comp build -p # build the RPMs +``` + +Inspect the rendered spec under `{{ .RenderedSpecsDir }}/`. A new component always needs a +smoke test — see the `azldev-build-component` and `azldev-mock` skills. + +After all component inputs are final, run `azldev comp update -p ` again and +re-render. Stage the component definition and sources, `{{ .LockDir }}/.lock`, and +the rendered output before committing. Then re-render, stage +`{{ .RenderedSpecsDir }}///`, and amend the commit so `%changelog` and +`Release:` reflect it. See the `azldev-update-component` skill for the complete +finalization workflow. + +Generated by `azldev docs agent`; do not hand-edit. Generated for azldev version `{{ .Version }}`. diff --git a/internal/app/azldev/agentskill/content/azldev.md.tmpl b/internal/app/azldev/agentskill/content/azldev.md.tmpl index 34174532..37640acb 100644 --- a/internal/app/azldev/agentskill/content/azldev.md.tmpl +++ b/internal/app/azldev/agentskill/content/azldev.md.tmpl @@ -58,7 +58,14 @@ mock helpers; it is intentionally omitted from normal help. ## Where to go next -Start with `azldev --help` for the current command syntax and flags. +- Add a new component — read the `azldev-add-component` skill. +- Edit or review a component's TOML — read the `azldev-comp-toml` skill. +- Add or change overlays — read the `azldev-overlays` skill. +- Build, iterate, and debug a component — read the `azldev-build-component` skill. +- Build, boot, and register images — read the `azldev-image` skill. +- Refresh a lock and finalize for a PR — read the `azldev-update-component` skill. +- Remove a component — read the `azldev-remove-component` skill. +- Test built RPMs in a chroot — read the `azldev-mock` skill. ## Golden rules diff --git a/internal/app/azldev/agentskill/content/build-component.md.tmpl b/internal/app/azldev/agentskill/content/build-component.md.tmpl new file mode 100644 index 00000000..ba2f2d13 --- /dev/null +++ b/internal/app/azldev/agentskill/content/build-component.md.tmpl @@ -0,0 +1,60 @@ +--- +name: {{ .Name }} +description: {{ printf "%q" .Description }} +--- + +# Build and debug a component + +**Never install built RPMs on your host** — they target the distro, not your dev +machine. Test them in a chroot with the `azldev-mock` skill. Building and testing are separate +steps: `azldev comp build` produces RPMs; it does not test them. + +## Build + +```sh +azldev comp build -p # one component +azldev comp build -p -p --local-repo-with-publish # chain deps via a local repo +azldev comp build -p --local-repo # rebuild against a populated repo +``` + +Build foundational packages before their dependents. RPMs land in the project's +configured output directory (`out` by default). `-q` quiets output but hides build +progress — use it only for inner-loop builds you expect to succeed. + +## The inner loop + +investigate → modify → render → build → test → inspect + +| Step | Command | +| --- | --- | +| Investigate | read the rendered spec under `{{ .RenderedSpecsDir }}/`, or `azldev comp diff-sources -p ` | +| Modify | edit the component's `.comp.toml` (see the `azldev-overlays` and `azldev-comp-toml` skills) | +| Verify | `azldev comp render -p ` (fast — skips source tarballs) | +| Build | `azldev comp build -p ` | +| Test | `azldev adv mock shell --add-package ` (see the `azldev-mock` skill) | +| Inspect | `azldev comp build -p --preserve-buildenv always`, then a mock shell | + +Prefer `comp render` for quick overlay verification; use `comp diff-sources` to see the +exact overlay effect (it fetches sources once, applies overlays to a copy, and diffs the +two trees). Builds can be slow — set generous timeouts. + +Finalize with `azldev comp update -p ` before opening a PR (see the +`azldev-update-component` skill). + +## Debugging build failures + +1. **Render error mentioning a non-standard `Release` tag** — a release-calculation + issue; see the `azldev-comp-toml` skill. +2. **Overlay did not apply as expected** — `azldev comp diff-sources -p ` shows + what the overlays actually change. +3. **Inspect the build environment** — `azldev comp build -p --preserve-buildenv + on-failure` (values `on-failure`, `always`, `never`), then enter a mock shell. +4. **Failing `%check`** — fix the tests first (root cause, upstream patches, targeted + fixes). Only as a last resort, disable with `build.check.skip = true` and a required + `build.check.skip_reason` explaining what fails, why it cannot be fixed, and whether it + is temporary. A transient `--no-check` build flag exists for one-off local builds. + +Per-component build tweaks (`build.defines`, `build.without`) live in the `.comp.toml` — +see the `azldev-comp-toml` skill. + +Generated by `azldev docs agent`; do not hand-edit. Generated for azldev version `{{ .Version }}`. diff --git a/internal/app/azldev/agentskill/content/comp-toml.md.tmpl b/internal/app/azldev/agentskill/content/comp-toml.md.tmpl new file mode 100644 index 00000000..af15f42a --- /dev/null +++ b/internal/app/azldev/agentskill/content/comp-toml.md.tmpl @@ -0,0 +1,141 @@ +--- +name: {{ .Name }} +description: {{ printf "%q" .Description }} +--- + +# Component definition files (`*.comp.toml`) + +A component definition tells azldev where a package's spec comes from and how to +customize it for your distro. Every component lives under `[components.]`. + +Get the authoritative, always-current field list from the schema: + +```sh +azldev config generate-schema +``` + +## Structure + +A bare entry inherits everything from your distro's defaults — most upstream packages +need nothing more: + +```toml +[components.curl] +``` + +Add sub-tables only for what you change. The fields you will reach for most: + +| Field | Purpose | +| --- | --- | +| `spec` | where the spec comes from (see below) | +| `overlays` / `overlay-files` | targeted spec/source edits (see the `azldev-overlays` skill) | +| `build.defines` / `build.with` / `build.without` | RPM macro and bcond build tweaks | +| `release.calculation` | how the `Release` tag is managed | +| `render.skip-file-filter` | rendering edge-case escape hatch | + +## Spec source + +The `spec` field selects where the spec is fetched from. When omitted, the component +inherits the distro default (normally an upstream import). + +```toml +# Upstream import (the usual case) — inherits the distro's upstream version +[components.curl] + +# Upstream, but pinned to a specific upstream distro/version +[components.curl] +spec = { type = "upstream", upstream-distro = { name = "fedora", version = "rawhide" } } + +# Upstream package whose name differs from the component name +[components.mydistro-rpm-config] +spec = { type = "upstream", upstream-name = "redhat-rpm-config" } + +# Local spec that lives in your repo (not imported from an upstream distro) +[components.mydistro-release] +spec = { type = "local", path = "mydistro-release.spec" } +``` + +## Build configuration + +```toml +[components.mypackage.build] +defines = { rhel = "11" } # override RPM macros +with = ["feature_x"] # enable %bcond_with conditionals +without = ["plugin_rhsm"] # disable %bcond_with conditionals +``` + +## Release calculation + +`release.calculation` controls the `Release:` tag. There are four modes: + +- `auto` (default) — auto-detect whether the spec uses `%autorelease` or a static + release and handle it accordingly. Correct for most packages. +- `autorelease` — force `%autorelease` handling (use when auto-detection misreads a + spec that wraps `%autorelease` in a conditional). +- `static` — force static-integer handling and bump the integer on render (the + inverse of `autorelease`). +- `manual` — you own the `Release:` value. Use this only when render fails with a + "non-standard Release tag" error. **A `manual` component is not bumped by the + render/commit/amend cycle, so increment its release yourself in the same change** + (see the `azldev-update-component` skill). + +```toml +[components.mypackage.release] +calculation = "manual" +``` + +## Render configuration + +`render.skip-file-filter = true` keeps all source and patch files during render. +azldev normally prunes files not referenced by the rendered spec; set this only for +the rare spec whose `Source`/`Patch` filenames use macros the filter cannot expand. + +## File organization + +- **Inline** — put simple, customization-free components directly in a shared config + file (e.g. `[components.jq]`). +- **Dedicated** — give a component its own `/.comp.toml` once it needs + overlays, build config, or a local spec. Rule of thumb: anything more than + `[components.]` earns a dedicated file. +- A parent config picks up dedicated files through an `includes` glob, for example + `includes = ["**/*.comp.toml"]`. + +## Review checklist + +Start with `azldev comp list -p -q -O json`, then use +`azldev comp query -p -q -O json` when the review needs parsed spec details. +For a change review, focus on the diff while checking enough surrounding context to +ensure it fits the component and repository conventions. + +- **Organization:** The component follows the repository's inline-versus-dedicated-file + convention; its name matches upstream or sets `spec.upstream-name`; no stale or + orphaned component files remain. +- **Spec source:** The default upstream source is preferred. Pins explain why they are + needed, and local specs are used only when overlays cannot express the change. +- **Overlays:** Every overlay explains why it exists. Prefer structured overlay types + over regex; scope unavoidable `spec-search-replace` expressions by section and, when + applicable, package, and use TOML literal strings. `spec-search-replace` cannot span + lines. Remove overlays that upstream has made unnecessary. See the `azldev-overlays` skill. +- **Build config:** Defines and bcond overrides are necessary and correspond to the + spec. If `build.check.skip = true`, require a specific `build.check.skip_reason` and + verify that fixing the tests is impractical; skipped `%check` is a last resort. +- **Release mode:** `auto` is the default. Force `autorelease` or `static` only when + auto-detection is wrong. Use `manual` only for a non-standard release tag, and verify + the component increments its release itself. +- **Generated state:** The lock matches the final component inputs, and rendered output + contains the intended changes without unrelated drift. +- **Testing:** Changes that can affect RPM output were built and smoke-tested in a mock + chroot. Organization, comment, or documentation-only metadata edits do not require a + rebuild when the resolved component inputs are unchanged. + +Report findings by severity: errors for correctness or required-policy violations, +warnings for maintainability risks, and info for optional improvements. Prefer small, +actionable fixes over unrelated cleanup. + +## Documenting changes + +Add a TOML comment explaining *why* a non-obvious field is set (a version pin, a +workaround), and link the upstream commit or bug when the change is based on one. For +overlays, use the overlay `metadata` table instead (see the `azldev-overlays` skill). + +Generated by `azldev docs agent`; do not hand-edit. Generated for azldev version `{{ .Version }}`. diff --git a/internal/app/azldev/agentskill/content/image.md.tmpl b/internal/app/azldev/agentskill/content/image.md.tmpl new file mode 100644 index 00000000..ca4108eb --- /dev/null +++ b/internal/app/azldev/agentskill/content/image.md.tmpl @@ -0,0 +1,60 @@ +--- +name: {{ .Name }} +description: {{ printf "%q" .Description }} +--- + +# Build and boot images + +An image assembles built RPMs into a deliverable — a container or a bootable VM. +azldev drives the build from a kiwi-ng definition; the `.kiwi` file itself is +**upstream KIWI NG** ([docs](https://osinside.github.io/kiwi/)) and azldev does not +own its schema. + +## Commands + +Images are selected by their **name as a positional argument** (not `-p`). + +| Task | Command | +| --- | --- | +| List images | `azldev image list` | +| Build an image (kiwi-ng) | `azldev image build ` | +| Boot an image in a QEMU VM | `azldev image boot ` | +| Run an image's tests | `azldev image test ` | +| Customize a pre-built image | `azldev image customize` | + +`azldev image build` takes `--local-repo` / `--remote-repo` to add package sources and +`--arch` to target an architecture. `azldev image boot` takes a built image name, or an +explicit `--image-path` / `--iso`. Confirm current flags with +`azldev image --help` (there are more subcommands, e.g. `inject-files`). + +## Registering an image + +Images are declared under `[images.]` (conventionally in an `images.toml`): + +```toml +[images.container-base] +description = "Container base image" +definition = { type = "kiwi", path = "container-base/container-base.kiwi", profile = "core" } + + [images.container-base.capabilities] + container = true + systemd = false + + [images.container-base.tests] + test-suites = [{ name = "smoke" }] +``` + +- `definition.type` is `kiwi` (the only type today); `path` points at the `.kiwi` file; + `profile` selects a kiwi profile (optional). +- `capabilities` are tri-state flags describing the image — `machine-bootable`, + `container`, `systemd`, `runtime-package-management`. Set only the ones that apply. +- `tests.test-suites` lists the test suites `azldev image test` runs. +- `publish.channels` lists the channels the image publishes to. + +## The kiwi definition + +The `.kiwi` file is upstream KIWI NG XML — azldev does not own its schema. It selects the +image type (container vs. VM), package lists, repositories, and boot configuration. See +the [KIWI NG documentation](https://osinside.github.io/kiwi/) for the format and elements. + +Generated by `azldev docs agent`; do not hand-edit. Generated for azldev version `{{ .Version }}`. diff --git a/internal/app/azldev/agentskill/content/mock.md.tmpl b/internal/app/azldev/agentskill/content/mock.md.tmpl new file mode 100644 index 00000000..777f6d21 --- /dev/null +++ b/internal/app/azldev/agentskill/content/mock.md.tmpl @@ -0,0 +1,92 @@ +--- +name: {{ .Name }} +description: {{ printf "%q" .Description }} +--- + +# Test and inspect packages in a mock chroot + +> **Never install built RPMs on the host.** They target the distro you are +> building, not your dev machine. Use a mock chroot instead. + +azldev tests and inspects packages inside `mock` build roots via `mock shell`, +which lives under the hidden `advanced` group (`azldev adv` is an alias for +`azldev advanced`; it does not appear in `azldev --help`). + +## Non-interactive chroot (preferred for agents) + +Pipe commands in via a heredoc — fully autonomous, no user interaction needed: + +```sh +azldev adv mock shell --add-package /path/to/package.rpm <<'CMDS' + --version +echo "exit code: $?" +exit +CMDS +``` + +Use this for automated checks: running binaries, querying installed packages, +inspecting paths. + +## Inspect an RPM without installing it + +When `--add-package` fails to install, or you just want to look at an RPM's +contents, get the file inside the chroot and inspect it there rather than +installing on the host. If the RPM is not already reachable inside the build +root, copy it in first with `mock --copyin /tmp/` (using the project's +distro mock config), then: + +```sh +azldev adv mock shell <<'CMDS' +rpm -qip /tmp/*.rpm # package info +rpm -qlp /tmp/*.rpm # file list +rpm -qRp /tmp/*.rpm # dependencies +dnf install /tmp/*.rpm # retry the install to see the error details +exit +CMDS +``` + +This gives you the full distro environment for debugging dependency issues, file +conflicts, and failed installs, without touching the host. + +## Interactive chroot + +Use when you need to explore interactively or react to output between commands: + +```sh +azldev adv mock shell --add-package /path/to/package.rpm --enable-network +``` + +When driving an interactive shell as an agent: + +1. Run a simple command (e.g. `echo ready`) in a foreground terminal to establish a shared shell. +2. Ask the user to run the `azldev adv mock shell` command in that same terminal. +3. Wait for confirmation that they are inside the mock shell. +4. Run diagnostic commands in the same foreground terminal — they execute inside the chroot. +5. Run `exit` to leave the chroot. + +Prefer the non-interactive heredoc approach; use interactive only when you must +react to output between commands. + +## Key flags + +| Flag | Purpose | +| --- | --- | +| `-p`, `--add-package ` | Install a package into the chroot before entering. Accepts an RPM file path or a package name. May be repeated. | +| `--enable-network` | Allow network access (dependency resolution, downloads). | + +> **`-p` means `--add-package`, not a component name.** Unlike `azldev comp build -p `, +> `mock shell` has no component selector. `-p ` installs a package *by name* from the +> configured repos — it does not set up a chroot with that component's build dependencies. + +## Gotchas + +- **Don't mix `-p ` with `--add-package /path/to/.rpm` for the same package** — that + installs both the repo build and your local build, which conflict. Use one or the other. +- **The mock chroot is persistent** across `azldev adv mock shell` sessions — installed packages and + created files survive. Handy (install once, re-enter later), but stale state can confuse. azldev + has no built-in reset; clear it with `mock` directly (`--scrub=chroot`, or `--scrub=all` to also + drop the bootstrap), pointing at the project's distro mock config. +- **One mock session per terminal.** Running `azldev adv mock shell` while a terminal is already + inside a mock shell fails — run `exit` first. + +Generated by `azldev docs agent`; do not hand-edit. Generated for azldev version `{{ .Version }}`. diff --git a/internal/app/azldev/agentskill/content/overlays.md.tmpl b/internal/app/azldev/agentskill/content/overlays.md.tmpl new file mode 100644 index 00000000..695b595c --- /dev/null +++ b/internal/app/azldev/agentskill/content/overlays.md.tmpl @@ -0,0 +1,158 @@ +--- +name: {{ .Name }} +description: {{ printf "%q" .Description }} +--- + +# Working with overlays + +Overlays are **semantic patches** applied to a component's RPM spec and loose +source files at render time. They let you make targeted changes to an upstream +spec without forking it. Prefer an overlay over hand-editing a rendered spec: +overlays are re-applied on every render, so a manual edit to a rendered spec is +overwritten. + +## The inner loop + +Overlays live in the component's TOML config — inline `[[components..overlays]]` +entries, or per-file overlay documents referenced by the component's `overlay-files` +glob. They apply **in order** and are **non-atomic**: if one fails part-way, the +overlays before it stay applied. + +1. Add or edit the overlay in the component config. +2. Re-render and inspect the result: + + ```sh + azldev comp render -p + ``` + + Read the rendered spec (under `{{ .RenderedSpecsDir }}/`) to confirm the change + landed where you intended, and iterate until it is correct. +3. Finalize the lock and changelog with the normal end-of-work refresh (see the + `azldev-update-component` skill): update the lock, commit, then re-render and amend. + +Config errors reference the offending overlay by its `description`, so give every +overlay a short, specific `description`. + +## Diagnose common failures + +Start with `azldev comp diff-sources -p ` to see the exact overlay effect. +Use separate pre/post `prep-sources` directories only when you need persistent trees +for deeper inspection. + +| Symptom | Likely cause and fix | +| --- | --- | +| `spec-add-tag`: tag already exists | Upstream already has the tag. Use `spec-set-tag`, or `spec-update-tag` when its prior existence is an invariant. | +| `spec-search-replace`: no match | Inspect the current upstream line, check TOML regex quoting, and narrow the expression to the actual section/package. | +| Section or file not found | Inspect the upstream spec/source names; upstream may have renamed or removed the target. | +| Overlay applies but output/build is wrong | Inspect `diff-sources` for an over-broad match, malformed replacement, or a dependency/file change the overlay omitted. | + +## Choosing an overlay type + +Match the change to the narrowest overlay type. Required fields are enforced when +the config loads, so a missing field fails fast rather than at apply time. + +### Spec overlays (structured `.spec` edits) + +| Type | Use for | Required | +| --- | --- | --- | +| `spec-add-tag` | add a tag; fails if it already exists | `tag`, `value` | +| `spec-insert-tag` | add a tag next to its family (e.g. after the last `Source*`) | `tag`, `value` | +| `spec-set-tag` | set a tag, replacing it if present or adding it if not | `tag`, `value` | +| `spec-update-tag` | change an existing tag; fails if it is missing | `tag`, `value` | +| `spec-remove-tag` | delete tag instances; without `value`, deletes every instance | `tag` | +| `spec-prepend-lines` | insert lines at the top of a section (or the whole file) | `lines` | +| `spec-append-lines` | insert lines at the end of a section (or the whole file) | `lines` | +| `spec-search-replace` | regex replace within a section (or the whole spec) | `regex` | +| `spec-remove-section` | delete a whole section | `section` | +| `spec-remove-subpackage` | delete every section of a sub-package | `package` | +| `patch-add` | add a `.patch` file and register it in the spec | `source` | +| `patch-remove` | remove a patch and its spec references | `file` | + +### File overlays (loose non-spec files; never `.spec`) + +| Type | Use for | Required | +| --- | --- | --- | +| `file-prepend-lines` | prepend lines to a file | `file`, `lines` | +| `file-search-replace` | regex replace in a file | `file`, `regex` | +| `file-add` | copy in a new file; fails if it already exists | `file`, `source` | +| `file-remove` | delete a file | `file` | +| `file-rename` | rename a file in place | `file`, `replacement` | + +## Rules that trip people up + +- **`spec-remove-tag` without `value` removes every instance** of the named tag. + To remove one dependency, set both `tag` and the exact `value` to match: + + ```toml + [[components.mypackage.overlays]] + description = "Remove an unavailable build dependency" + type = "spec-remove-tag" + tag = "BuildRequires" + value = "unwanted-package" + ``` +- **`section` is optional only** for `spec-prepend-lines`, `spec-append-lines`, and + `spec-search-replace` (omit it to target the whole spec). It is **required** for + `spec-remove-section`. +- **`package` needs `section`** on the whole-file-capable overlays — a sub-package is + a sub-qualifier of a section. `spec-remove-subpackage` is the exception: it takes + `package` and rejects `section`. +- **`replacement` is literal** — `$1`-style capture-group references are not expanded; + omit it to delete matched text. +- **Quote `regex` as a TOML literal string** — write `regex = '\.so$'`, not + `regex = "\.so$"`. A basic (double-quoted) TOML string interprets backslash escapes, so + `\s`, `\.`, `\d` and friends are mangled before the regex engine ever sees them; single + quotes keep the pattern verbatim. +- **Anchor regex overlays to whole lines, and prefer macro toggles.** When + `spec-search-replace` is unavoidable, anchor the full line (for example, + `regex = '^%setup -q$'`) instead of matching a fragment, and combine several + near-identical patterns into one rather than stacking brittle overlays. If the + upstream spec already exposes a conditional such as `%if 0%{?rhel}` / + `%if 0%{?fedora}` or a definable macro, set that macro instead of rewriting the + line with regex; the explicit toggle survives upstream changes more reliably. +- **`spec-search-replace` matches one line at a time** — the pattern is applied to each + spec line independently, so it can never span a newline and `(?s)`/DOTALL does nothing. + For a multi-line change use a structured spec overlay (`spec-remove-section`, + `spec-prepend-lines`/`spec-append-lines`, etc.). `file-search-replace` is different: it + matches against the whole file, so multi-line patterns (and `(?s)`) work there. +- **`file` is a glob** (`**` supported) for the multi-file file overlays; for `file-add` + and `file-rename` it is a single name, and `file-rename`'s `replacement` is a + filename only (not a path). +- **`source` paths are relative** to the config that declares the overlay — the overlay + file when loaded via `overlay-files`, otherwise the component config. +- **`file-add` lands beside the spec**, in the dist-git sources root — not inside the + extracted upstream tree. Adding a file there does not make the build use it; wire it in + with a `SourceN` tag plus `%prep`/`%install` steps, or use `patch-add` to change tracked + sources. +- **Don't rename the `Name:` tag** with `spec-update-tag`/`spec-set-tag`. `%{name}` feeds + `Source*` URLs, `%setup -n`, and `%files` paths, so renaming it silently breaks those + references. Keep the spec `Name` aligned with the component instead. +- To add a real `.patch` file (rather than an inline edit), use `patch-add`; it copies + the `source` into the component sources and registers a `PatchN` tag or `%patchlist` + entry. + +## Document intent with `metadata` + +Give non-trivial overlays a `metadata` table. It is documentation only — excluded from +the component fingerprint, so editing it never invalidates the build cache — but it +records *why* the overlay exists and *when* it can be dropped. Every metadata block +requires `category`; pick the narrowest of: + +`upstream-backport`, `azl-pruning`, `azl-compatibility`, `azl-temp-workaround`, +`azl-branding-policy`, `azl-disable-flaky-tests`, `azl-disable-unsupported-tests`, +`azl-security-compliance`, `azl-release-management`, `azl-platform-adaptation`. + +It also requires `upstream-status`: `upstreamed`, `upstreamable`, +`needs-upstream-hook`, `inapplicable`, or `unknown`. Add `commits` and `bugs` as +`{ url = "https://..." }` entries where they apply. `commits` is required for +`upstream-backport`, whose status must be `upstreamed` or `upstreamable`. When several +overlays share one provenance, put them in a per-file overlay document (`overlay-files`) +with a single file-level `[metadata]`. + +## Full reference + +The tables above are the working subset. For the exhaustive field rules, metadata +constraints, and the per-file overlay format, generate the machine-readable schema +with `azldev config generate-schema` (see the `ComponentOverlay` definition), or read +azldev's overlays configuration reference. + +Generated by `azldev docs agent`; do not hand-edit. Generated for azldev version `{{ .Version }}`. diff --git a/internal/app/azldev/agentskill/content/remove-component.md.tmpl b/internal/app/azldev/agentskill/content/remove-component.md.tmpl new file mode 100644 index 00000000..fe989a18 --- /dev/null +++ b/internal/app/azldev/agentskill/content/remove-component.md.tmpl @@ -0,0 +1,81 @@ +--- +name: {{ .Name }} +description: {{ printf "%q" .Description }} +--- + +# Remove a component + +There is **no `azldev` command to remove a component** — it is a manual edit. A +deletion-only change needs no package build, but changes to dependents, package/image +configuration, or other built inputs must follow the repository's normal validation +requirements. The steps below delete the component's definition, lock, and rendered +spec, then clean up references. + +## Before you start + +Confirm the component exists and inspect its resolved configuration: + +```sh +azldev comp list -p -q -O json +``` + +Check for **reverse dependencies** first: if other components `BuildRequires` or +`Requires` this one, removing it breaks their builds. Search the tree for the +component name. Identify its binary subpackage names from the rendered output under +`{{ .RenderedSpecsDir }}///`, then search for those names too. +`azldev comp query` currently supports local spec sources only, not the usual upstream +spec source. + +## Steps + +1. **Remove the definition.** Delete the component's dedicated + `/.comp.toml` directory, or remove its inline `[components.]` + entry from whichever included TOML defines it. +2. **Remove publish / package config references.** If your project configures + publish channels or package groups, drop any references to the component or to + its binary subpackages. Publishing is component-scoped; per-binary exceptions + are binary-RPM-scoped — search for both. +3. **Remove the lock.** There is no azldev command for this: + + ```sh + rm {{ .LockDir }}/.lock + ``` + +4. **Remove the rendered spec.** Let azldev prune orphaned spec directories: + + ```sh + azldev comp render -a --clean-stale + ``` + + `--clean-stale` (only valid with `-a`) removes rendered-spec directories that no + longer correspond to a configured component. It re-renders everything, so it is + slow; for a targeted removal you can instead delete the component's rendered + spec under `{{ .RenderedSpecsDir }}/` by hand. +5. **Check other references.** Grep for the component (and its binary names) in + image definitions and any `*.kiwi` / package-list files, and remove or replace + them — an image that installs a now-removed package will not build. + +## Verify + +```sh +azldev comp list -p -q -O json # should report the component is not found +``` + +Also confirm the lock and rendered spec directory are gone and that no image or +package configuration still references the component. + +If the removal required changes to dependents or package/image configuration, build +and test the affected outputs according to the repository's normal validation policy. + +## Notes + +- **Dependents with manual release.** If removing this component forces a change + in a *dependent* (e.g. dropping a `BuildRequires`), and that dependent sets + `release.calculation = "manual"`, bump its release counter yourself in the same + change. Components with automatic release calculation (`auto`, `autorelease`, + `static`) are handled by the normal commit/render/amend cycle. +- **Remove exclusive dependencies together.** If a package is only needed by the + component you are dropping, remove it in the same change to keep the tree + consistent. + +Generated by `azldev docs agent`; do not hand-edit. Generated for azldev version `{{ .Version }}`. diff --git a/internal/app/azldev/agentskill/content/update-component.md.tmpl b/internal/app/azldev/agentskill/content/update-component.md.tmpl new file mode 100644 index 00000000..fc3fc587 --- /dev/null +++ b/internal/app/azldev/agentskill/content/update-component.md.tmpl @@ -0,0 +1,83 @@ +--- +name: {{ .Name }} +description: {{ printf "%q" .Description }} +--- + +# Update component lock files + +`azldev comp update` (`comp` is an alias for `component`) refreshes one or more +component lock files under `{{ .LockDir }}/`. A lock pins the resolved upstream commit plus +an input fingerprint computed from the component's render inputs — its TOML config, +overlays, the pinned upstream commit, and the distro release version. If any of +those change, the lock is stale. + +## When to run `update` + +| Situation | Run `update`? | +| --- | --- | +| Adding a new upstream component (no lock yet) | **Yes** — first, to create the lock before `render`/`build` can resolve it | +| Finalizing a component change for a PR | **Yes** — once at the end | +| Changing source resolution (commit pin, upstream distro/version, or snapshot) | **Yes** — also mid-workflow (see below) | +| Iterating on overlays / build config / metadata | No — once the lock exists, `render` alone is enough while iterating | +| Just reading or building existing components | No | + +Refresh a single component with `-p `. Use `-a` (all components) only for +coordinated mass refreshes (e.g. a new distro snapshot) or when investigating +lock drift across many components — it is slow. For day-to-day work use `-p`. Add +`-O json` for machine-readable lock output when debugging. + +## End-of-work refresh (the common case) + +For most edits — overlays, build flags, metadata — run `update` once at the end, +then re-render *after committing* so the generated changelog and release reflect +your new commit: + +```sh +azldev comp update -p +azldev comp render -p +git add \ + {{ .LockDir }}/.lock \ + {{ .RenderedSpecsDir }}/// +git commit -m "fix(): ..." + +# Re-render and amend so the changelog / Release: track the new commit. +azldev comp render -p +git add {{ .RenderedSpecsDir }}/// +git commit --amend --no-edit +``` + +### Why the second render-and-amend? + +Changelog generation and release calculation are separate. When a spec uses +`%autochangelog`, rpmautospec derives it from the component's **git history** for +every release mode. For non-manual release modes, azldev also derives or bumps +`Release:` from that history. The first render happens before your commit exists, +so a fresh render *after* committing incorporates the new changelog entry and any +automatic release bump. Amending folds that output into a single clean commit and +keeps rendered-spec / lock CI gates (which run against committed state) green. + +For a component with `release.calculation = "manual"`, increment the release +counter yourself in the same change. Manual mode is not an exemption from the +post-commit render-stage-amend cycle: when the spec uses `%autochangelog`, that +render incorporates the new commit. + +## Changing source resolution + +A source-resolution change follows the same rule, using one commit followed by a +post-render amend: + +1. Change the commit pin, upstream distro/version, or snapshot, then `azldev comp update -p `; sanity-check `{{ .LockDir }}/.lock`. +2. `azldev comp render -p ` — the spec body now tracks the newly resolved source. `%changelog` / `Release:` still reflect the previous source; that is expected until you commit. +3. Iterate on overlays / patches / build config as the new source requires, re-rendering after each change. Re-run `update` only if you change a source-resolution input again. +4. `azldev comp update -p `, then stage and commit all component inputs changed above with the refreshed lock and rendered output: + + ```sh + git add \ + {{ .LockDir }}/.lock \ + {{ .RenderedSpecsDir }}/// + git commit -m "update(): ..." + ``` +5. `azldev comp render -p ` — `%changelog` / `Release:` now reflect the new lock. +6. `git add {{ .RenderedSpecsDir }}///`, then `git commit --amend --no-edit` so the source change and rendered output land together. + +Generated by `azldev docs agent`; do not hand-edit. Generated for azldev version `{{ .Version }}`.