Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/user/reference/config/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ A component definition tells azldev where to find the spec file, how to customiz
| Spec source | `spec` | [SpecSource](#spec-source) | No | Where to find the spec file for this component. Inherited from distro defaults if not specified. |
| Release config | `release` | [ReleaseConfig](#release-configuration) | No | Controls how the Release tag is managed during rendering |
| Overlays | `overlays` | array of [Overlay](overlays.md) | No | Modifications to apply to the spec and/or source files |
| Overlay files | `overlay-files` | array of string | No | Glob patterns that load per-file overlay documents. See [Per-file overlay format](overlays.md#per-file-overlay-format). |
| Build config | `build` | [BuildConfig](#build-configuration) | No | Build-time options (macros, conditionals, check config) |
| Render config | `render` | [RenderConfig](#render-configuration) | No | Options controlling spec rendering behavior |
| Source files | `source-files` | array of [SourceFileReference](#source-file-references) | No | Additional source files to download for this component |
Expand Down
145 changes: 144 additions & 1 deletion docs/user/reference/config/overlays.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,153 @@ successfully makes a replacement to at least one matching file.
| Replacement | `replacement` | Literal replacement text; capture group references like `$1` are **not** expanded. Omit or leave empty to delete matched text. | `spec-search-replace`, `file-search-replace`, `file-rename` |
| Lines | `lines` | Array of text lines to insert | `spec-prepend-lines`, `spec-append-lines`, `file-prepend-lines` |
| File | `file` | The name of the non-spec file to modify or add | `file-prepend-lines`, `file-search-replace`, `file-add`, `file-remove`, `file-rename`, `patch-add` (optional), `patch-remove` |
| Source | `source` | Path to source file for `file-add` and `patch-add`; relative paths are relative to the config file | `file-add`, `patch-add` |
| Source | `source` | Path to source file for `file-add` and `patch-add`; relative paths are relative to the config file that defines the overlay (the overlay file if loaded via [`overlay-files`](#per-file-overlay-format), otherwise the component config) | `file-add`, `patch-add` |
| Metadata | `metadata` | Documentation table describing intent and provenance — see [Overlay Metadata](#overlay-metadata). Not allowed inside an overlay file loaded via `overlay-files` (the file-level `[metadata]` block applies to every overlay in the file). | All (optional) |

> **Note:** For `file-rename`, the `replacement` field is a **filename only** (not a path). The file is renamed within its current directory.

### Component-level fields for overlays

In addition to per-overlay fields, the following fields are set directly on the component:

| Field | TOML Key | Description |
|-------|----------|-------------|
| Overlay files | `overlay-files` | List of glob patterns (relative to the component's config file) matched against the filesystem at load time to locate per-file overlay documents. Patterns support `**` (globstar). Matches are concatenated in declaration order; within a single pattern, matches are applied in filename (lexicographic) order, with full path as a tie-breaker for duplicate filenames. Each pattern must match at least one file. Duplicate matches across patterns are de-duplicated. See [Per-file overlay format](#per-file-overlay-format). |

## Overlay Metadata

Overlays can carry an optional `metadata` table that documents *why* the overlay exists and *when* it can be removed. Metadata is reviewed by humans and surfaced in tooling; it does **not** affect how the overlay is applied and is excluded from component fingerprints (so editing metadata never invalidates build caches).

### `metadata` fields

| Field | TOML Key | Description |
|-------|----------|-------------|
| Category | `category` | **Required.** Classification of the overlay's intent. See the table below. |
| Commits | `commits` | List of upstream commit URLs (Fedora dist-git or upstream project) that this overlay backports or implements. Each entry must be an absolute http(s) URL. |
| Bugs | `bugs` | List of bug-tracker references. Each entry is a table with a required `url`. See [Bug references](#bug-references). |
| Upstreamable | `upstreamable` | Boolean indicating whether this change can be upstreamed: `true` or `false`. Omit the field when upstreamability has not yet been assessed. |

### Categories

| Category | When to use |
|----------|-------------|
| `backport-dist-git` | Fix backported from (or being upstreamed to) a dist-git or upstream project. Self-resolves when AZL bumps past it. Requires at least one entry in `commits`. |
| `azl-pruning` | Removing content from a component for AZL: dependencies that are not shipped, unneeded features, subpackages, or files. |
| `azl-compatibility` | Making a component work in the AZL build/runtime environment: toolchain and mock adjustments, and similar compatibility fixes that are not themselves backports. |
| `azl-dep-missing-workaround` | Working around a runtime or build dependency that has not yet been imported into AZL (or is unavailable on a given target). Drop the overlay once the dependency lands. |
| `azl-branding-policy` | Fedora→Azure Linux name/path changes; RHEL/enterprise convention alignment. |
| `azl-disable-flaky-tests` | Skipping tests that fail intermittently or due to environmental flakiness rather than a real problem with the component. |
| `azl-disable-unsupported-tests` | Skipping tests that cannot meaningfully run in AZL's build/runtime environment (e.g. tests that require network access, root, or hardware that is unavailable in mock). |
| `azl-security-compliance` | FIPS or crypto-policy changes. |
| `azl-release-management` | Release-tag and changelog mechanics. |
| `azl-platform-adaptation` | Architecture-specific adjustments. |

### Bug references

The `bugs` field is a list of references to issue-tracker entries. Each entry is a table with a single required field:

| Field | TOML Key | Description |
|-------|----------|-------------|
| URL | `url` | **Required.** HTTP(S) link to the bug entry. |

Example:

```toml
[[components.mypackage.overlays.metadata.bugs]]
url = "https://bugzilla.redhat.com/show_bug.cgi?id=2234567"

[[components.mypackage.overlays.metadata.bugs]]
url = "https://github.com/example/repo/issues/42"
```

The inline-table form is more compact for short lists:

```toml
bugs = [
{ url = "https://bugzilla.redhat.com/show_bug.cgi?id=2234567" },
{ url = "https://github.com/example/repo/issues/42" },
]
```

### Inline metadata example

TOML inline tables (`metadata = { ... }`) must fit on a single line. When the metadata has more than one or two fields, use a sub-table (`[components.<name>.overlays.metadata]`) so each field gets its own line:

```toml
[[components.xclock.overlays]]
type = "spec-search-replace"
description = "Pass --force to autoreconf"
regex = "autoreconf -i"
replacement = "autoreconf -fi"

[components.xclock.overlays.metadata]
category = "backport-dist-git"
commits = ["https://src.fedoraproject.org/rpms/xclock/c/1e407488"]
```

For short metadata, the single-line inline form is also valid:

```toml
[[components.xclock.overlays]]
type = "spec-set-tag"
tag = "Vendor"
value = "Microsoft"
metadata = { category = "azl-branding-policy" }
```

## Per-file overlay format

When a single logical change (a CVE backport, a feature disablement, a Fedora cherry-pick…) needs **several overlays** that all share the same provenance, declaring them inline in the component config gets noisy and makes the boundary between unrelated changes hard to see. Use the per-file format instead.

### Layout

Set `overlay-files` on the component to one or more globs (relative to the component config) and drop one overlay file per logical change into a directory of your choosing. The conventional layout uses a sibling `overlays/` directory and a `*.overlay.toml` filename suffix, but neither is required — `overlay-files` is just a glob, so any layout you can describe with `**`/`*` patterns works.

```
base/comps/mypackage/
├── mypackage.comp.toml
└── overlays/
├── 0001-cve-2024-1234.overlay.toml
├── 0002-disable-broken-tests.overlay.toml
└── 0003-azl-branding.overlay.toml
```

```toml
# mypackage.comp.toml
[components.mypackage]
overlay-files = ["overlays/*.overlay.toml"]
```

Files are loaded in **filename (lexicographic) order** within each glob, using the full path as a tie-breaker when multiple matches have the same filename. Globs are concatenated in declaration order, so prefix each file with a numeric ordinal (`0001-`, `0002-`, …) to make the apply order obvious and stable. Files that don't match any of your globs are ignored, so you can keep `README.md` or other notes alongside without naming them out explicitly. Each declared glob must match at least one file; an empty match is treated as a misconfiguration and surfaced as an error.

Overlays loaded via `overlay-files` are **appended after** any inline overlays declared directly on the component.

### File structure

Each overlay file represents one logical change. It has:

* exactly one top-level `[metadata]` table (uses the same fields documented in [Overlay Metadata](#overlay-metadata)); and
* one or more `[[overlays]]` entries, applied in declaration order.

Per-overlay `metadata` is **not allowed** inside an overlay file — the file-level `[metadata]` is the single source of truth and is stamped onto every overlay in the file. Relative `source` paths are resolved against the directory of the overlay file (not the component config).

```toml
# overlays/0001-cve-2024-1234.overlay.toml
[metadata]
category = "backport-dist-git"
commits = ["https://src.fedoraproject.org/rpms/mypackage/c/abc123def456"]
bugs = [{ url = "https://bugzilla.redhat.com/show_bug.cgi?id=12345" }]

[[overlays]]
type = "patch-add"
source = "patches/CVE-2024-1234.patch"

[[overlays]]
type = "spec-append-lines"
section = "%changelog"
lines = ["- Fix CVE-2024-1234"]
```

## Examples

### Adding a Build Dependency
Expand Down
17 changes: 17 additions & 0 deletions internal/projectconfig/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,19 @@ type ComponentConfig struct {
// Overlays to apply to sources after they've been acquired. May mutate the spec as well as sources.
Overlays []ComponentOverlay `toml:"overlays,omitempty" json:"overlays,omitempty" table:"-" jsonschema:"title=Overlays,description=Overlays to apply to this component's spec and/or sources"`

// OverlayFiles, if set, lists glob patterns (relative to this component config file)
// matched against the filesystem at load time to locate per-file overlay documents.
// Each matched file is parsed as an [OverlayFile]: one logical change consisting of a
// file-level `[metadata]` block plus an ordered list of `[[overlays]]`. The per-file
// metadata is stamped onto every overlay in the file. Matches are concatenated in the
// order patterns are declared; within a single pattern, matches are applied in
// filename (lexicographic) order, using the full path as a tie-breaker when
// filenames match. Duplicate matches are de-duplicated, preserving first
// occurrence. The resulting overlays are appended to [ComponentConfig.Overlays]
// after any inline overlays. Excluded from the fingerprint because the value affects
// only where overlays are sourced from, not their content.
OverlayFiles []string `toml:"overlay-files,omitempty" json:"overlayFiles,omitempty" table:"-" validate:"dive,required" jsonschema:"title=Overlay files,description=Glob patterns (relative to the component config file) matched against the filesystem to locate per-file overlay documents at load time" fingerprint:"-"`

// Configuration for building the component.
Build ComponentBuildConfig `toml:"build,omitempty" json:"build,omitempty" table:"-" jsonschema:"title=Build configuration,description=Configuration for building the component"`

Expand Down Expand Up @@ -383,6 +396,10 @@ func (c *ComponentConfig) WithAbsolutePaths(referenceDir string) *ComponentConfi
SourceFiles: deep.MustCopy(c.SourceFiles),
Packages: deep.MustCopy(c.Packages),
Publish: deep.MustCopy(c.Publish),
// OverlayFiles is consumed at load time (see applyOverlayFiles) before paths are
// absolutized; nothing reads it afterward, so preserve the original value verbatim
// rather than redundantly re-rooting each glob.
OverlayFiles: slices.Clone(c.OverlayFiles),
}

// Fix up paths.
Expand Down
43 changes: 43 additions & 0 deletions internal/projectconfig/component_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,49 @@ func TestComponentPublishConfig_Validate(t *testing.T) {
}
}

func TestComponentConfig_OverlayFilesValidate(t *testing.T) {
t.Parallel()

validate := validator.New()

validCases := []struct {
name string
overlayFiles []string
}{
{name: "unset"},
{name: "empty slice", overlayFiles: []string{}},
{name: "non-empty pattern", overlayFiles: []string{"overlays/*.overlay.toml"}},
}

for _, testCase := range validCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()

cfg := projectconfig.ComponentConfig{OverlayFiles: testCase.overlayFiles}
require.NoError(t, validate.Struct(&cfg))
})
}

invalidCases := []struct {
name string
overlayFiles []string
}{
{name: "empty pattern", overlayFiles: []string{""}},
{name: "empty pattern among valid patterns", overlayFiles: []string{"overlays/*.overlay.toml", ""}},
}

for _, testCase := range invalidCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()

cfg := projectconfig.ComponentConfig{OverlayFiles: testCase.overlayFiles}
err := validate.Struct(&cfg)
require.Error(t, err)
assert.Contains(t, err.Error(), "OverlayFiles")
})
}
}

func TestIsDebugInfoPackage(t *testing.T) {
t.Parallel()

Expand Down
6 changes: 6 additions & 0 deletions internal/projectconfig/fingerprint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ func TestAllFingerprintedFieldsHaveDecision(t *testing.T) {
// ComponentOverlay.Source — absolute path that varies by checkout location.
// Overlay content is hashed separately by ComputeIdentity.
"ComponentOverlay.Source": true,
// ComponentOverlay.Metadata — documentation describing overlay intent and provenance.
"ComponentOverlay.Metadata": true,

// ComponentConfig.OverlayFiles — affects only where overlays are sourced from, not
// their content; the resulting overlays are fingerprinted normally.
"ComponentConfig.OverlayFiles": true,

// SourceFileReference.Component — back-reference to parent, not a build input.
"SourceFileReference.Component": true,
Expand Down
7 changes: 7 additions & 0 deletions internal/projectconfig/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,13 @@ func loadProjectConfigFile(
cfg.sourcePath = absFilePath
cfg.dir = filepath.Dir(absFilePath)

// Resolve per-component overlay file globs, stamping each
// file's [metadata] onto its overlays and appending them to the component before
// validation runs.
if err := applyOverlayFiles(fs, cfg, permissiveConfigParsing); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems out-of-place as a special case in the toplevel loader function. Is there a way we could have done this closer to where the component / overlays are being resolved/loaded?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered moving this closer to component resolution, but it would require passing permissiveConfigParsing into the merge/resolve path, which currently only works on already-decoded config structs. Daniel will clean up the loading stuff, and we can move this closer to component resolution then.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth making that change -- I see this as adding debt; I'm okay if you defer to a later PR, but my request is that you file a bug on it and assign to yourself ( or Daniel if he's willing to take it) to make sure this doesn't get forgotten.

return nil, err
}

// Make sure that the read data is internally consistent.
err = cfg.Validate()
if err != nil {
Expand Down
13 changes: 13 additions & 0 deletions internal/projectconfig/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ type ComponentOverlay struct {
// Excluded from fingerprint because it contains an absolute path that varies by checkout
// location. Overlay content is hashed separately by [fingerprint.ComputeIdentity].
Source string `toml:"source,omitempty" json:"source,omitempty" jsonschema:"title=Source,description=For overlays that require a source file as input, indicates a path to that file; relative paths are relative to the config file that defines the overlay" fingerprint:"-"`

// Metadata describes the intent and provenance of the overlay (category, related
// commits, bug links, upstreamability, etc.). Optional. Populated either inline
// in the component config file or by the [ComponentConfig.OverlayFiles] loader,
// which stamps the per-file metadata onto every overlay declared in that file.
// Excluded from the fingerprint because it is documentation only.
Metadata *OverlayMetadata `toml:"metadata,omitempty" json:"metadata,omitempty" jsonschema:"title=Metadata,description=Optional documentation metadata describing the overlay's intent and provenance" fingerprint:"-"`
}

// EffectiveSourceName returns the checkout-independent identity of the overlay's
Expand Down Expand Up @@ -333,6 +340,12 @@ func (c *ComponentOverlay) Validate() error {
return fmt.Errorf("unknown overlay type %#q: %#q", c.Type, desc)
}

if c.Metadata != nil {
if err := c.Metadata.Validate(); err != nil {
return fmt.Errorf("invalid metadata for overlay %q:\n%w", desc, err)
}
}

return nil
}

Expand Down
Loading
Loading