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
47 changes: 47 additions & 0 deletions docs/adr/53698-source-to-destination-mappings-in-includes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# ADR-53698: Source-to-Destination Mappings in `aw.yml` `includes`

**Date**: 2026-08-18
**Status**: Draft
**Deciders**: Unknown

---

### Context

The `aw.yml` package manifest's `includes` field previously accepted only path strings. String entries beginning with `.github/` are resolved relative to the consuming repository root (a rule preserved for backward compatibility). This made it impossible for a distribution repository to keep workflow assets outside its own `.github/workflows/` directory — because any path declared as `.github/workflows/foo.md` pointed to the consumer's repository root — while also preventing assets stored outside `.github/` in the distribution repository from being installed into the consumer's `.github/workflows/`. Package maintainers had no mechanism to declare separate source and destination paths, so assets that should not run in the distribution repository were forced into `.github/workflows/`, where the GitHub Actions runtime would execute them. Issue #52770 filed this limitation explicitly.

### Decision

We will extend the `includes` array to accept object entries (source-to-destination mappings) with required `source` (package-relative path) and `destination` (consuming-repository-root-relative path) fields, plus an optional `kind` discriminator (`agentic-workflow` | `action-workflow`). Mapping sources are always resolved relative to the package root regardless of their prefix; the `.github/` special case that applies to string entries does not apply. Destinations are restricted to direct children of `.github/workflows/`. All three install commands (`gh aw add`, `gh aw add-wizard`, `gh aw update`) share the same mapping semantics through a unified `resolvedPackageInstallable` struct. Validation failures for mapping entries are hard errors that abort before any file is written.

### Alternatives Considered

#### Alternative 1: Change resolution rules for `.github/**` string entries in nested packages

Modify the existing special case so that `.github/**` string entries in a nested package are resolved package-relative rather than repository-root-relative. This would allow `factory/aw.yml` to declare `.github/workflows/foo.md` and have it resolve to `factory/.github/workflows/foo.md`. This was rejected because it breaks backward compatibility: existing manifests depend on the repository-root-relative behavior, and that behavior is explicitly documented (and recorded in ADR-41790).

#### Alternative 2: Add a separate top-level `mappings` field to `aw.yml`

Introduce a new `mappings` key alongside `includes` and `files`. This was rejected because it fragments the authoring surface further — `files` is already deprecated in favor of `includes` — and it prevents authors from mixing string entries and mappings in a single ordered list. Extending `includes` with a union type (string | object) keeps the manifest surface minimal and preserves entry ordering.

### Consequences

#### Positive
- Distribution repositories can now keep workflow assets outside `.github/workflows/`, preventing accidental execution in the distribution context, while still installing those assets into the consumer's `.github/workflows/`.
- Source file names are decoupled from install names: a file at `payload/workflows/reviewer.md` can be installed as `.github/workflows/code-reviewer.md`.
- All existing string-entry semantics are unchanged; existing manifests continue to work without modification.
- Semantic validation (absolute paths, path traversal, symlinks, extension mismatches, unsupported extensions, duplicate destinations) is applied eagerly and fails before any file is written.

#### Negative
- The internal model evolves from plain `[]string` to `[]resolvedPackageInstallable` structs, requiring updates across all callers including tests — a broad but mechanical change.
- `extractManifestIncludes` now returns an error in addition to warnings (compared to the previous warnings-only return), changing the call signature at all call sites.
- Validation failures for mapping entries are hard errors (not soft skips), which is a stricter behavior than the existing warning-and-continue approach for unsupported string entries.

#### Neutral
- The `aw_manifest_schema.json` JSON Schema gains a `oneOf` branch to accommodate object entries alongside string entries.
- `gh aw update` keys installed workflows by destination name and maps them back to source paths, so manifest-scoped source tracking is preserved without additional changes.
- `files` is now marked deprecated in the reference documentation and specification (§4.8); string-form `files` entries continue to work via conversion to `repositoryPackageInclude{Source: p}`.

---

*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*
38 changes: 35 additions & 3 deletions docs/src/content/docs/reference/aw-yml-package-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ The package root is the folder that contains `aw.yml`.
| `name` | string | Yes | Human-readable package name. Must be non-empty after trimming whitespace. |
| `emoji` | string | No | Optional package emoji for display in package metadata. |
| `description` | string | No | Optional package description. `gh aw add` warns when it exceeds 255 characters. |
| `files` | array of strings | No | Package-root-relative paths. Agentic markdown workflows under `workflows/` or `.github/workflows/`; raw GitHub Actions YAML (`.yml`) is also accepted as direct children of `.github/workflows/`. |
| `files` | array of strings | No | Deprecated; use `includes`. Package-root-relative paths. Agentic markdown workflows under `workflows/` or `.github/workflows/`; raw GitHub Actions YAML (`.yml`) is also accepted as direct children of `.github/workflows/`. |
| `includes` | array | No | Installable entries. Each entry is either a path string (same rules as `files`, plus skill and agent paths) or a source-to-destination mapping. |

## Installable workflows

Expand All @@ -39,6 +40,35 @@ If `files` is present, valid entries become the install bundle. Two entry kinds
- **Agentic workflow markdown** — paths ending in `.md` under `workflows/` or `.github/workflows/`. `gh aw add` compiles these to lock files and fetches their dependencies.
- **Raw GitHub Actions YAML** — paths ending in `.yml` (but not `.lock.yml`) that are direct children of `.github/workflows/`. `gh aw add` copies these verbatim to `.github/workflows/<name>.yml` with no frontmatter processing, no dependency fetch, and no compilation. Nested subdirectories under `.github/workflows/` and `.yml` files under `workflows/` are not accepted.

### Path resolution rules

- A **string entry** that starts with `.github/` is resolved relative to the **consuming repository root**, even inside a nested package. For example, `.github/workflows/nightly.md` in `factory/aw.yml` refers to the repository-root file, not to `factory/.github/workflows/nightly.md`.
- Every other string entry (such as `workflows/review.md`) is resolved relative to the package root.
- A **mapping entry** always resolves `source` relative to the package root and `destination` relative to the consuming repository root.

### Source-to-destination mappings

Use mapping entries to keep workflow assets inert in the distribution repository while still installing them into the consuming repository's `.github/workflows/`:

```yaml
name: Factory
includes:
- source: payload/workflows/reviewer.md
destination: .github/workflows/reviewer.md
kind: agentic-workflow
- source: payload/workflows/controller.yml
destination: .github/workflows/controller.yml
kind: action-workflow
```

With a nested package reference such as `owner/repo/factory`, the files above are fetched from `factory/payload/workflows/` and installed to `.github/workflows/`. Because the sources live outside `.github/workflows/` in the distribution repository, they never run there.

The optional `kind` field is either `agentic-workflow` (`.md`) or `action-workflow` (`.yml`) and must match the source extension.

Mappings are rejected when `source` or `destination` is absolute, contains `..`, points at a symbolic link, uses an unsupported extension (or `.lock.yml`), changes the file extension between source and destination, or targets anything other than a direct child of `.github/workflows/`. Two entries installing to the same destination are rejected before any file is written.

`gh aw add`, `gh aw add-wizard`, and `gh aw update` all use these same mapping rules.

If `files` is omitted, or no valid entries remain after filtering,
`gh aw add` discovers installable markdown files under:

Expand All @@ -60,8 +90,10 @@ Missing `README.md` causes package validation to fail.
name: Repo Assist
emoji: 🤖
description: Friendly repository automation for review and issue triage
files:
includes:
- workflows/review.md # agentic workflow — compiled on install
- .github/workflows/nightly-review.md
- .github/workflows/nightly-review.md # repository-root-relative string entry
- .github/workflows/ci.yml # raw Actions YAML — copied verbatim
- source: payload/workflows/reviewer.md # package-relative source
destination: .github/workflows/reviewer.md
```
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ The manifest document MUST be a YAML mapping. Unknown top-level fields MUST be r
| `emoji` | string | No | Optional package emoji for display in package metadata. |
| `description` | string | No | Human-readable package description. |
| `license` | string | No | SPDX license identifier or license name for the package. |
| `files` | array of strings | No | Explicit installable workflow file list. |
| `files` | array of strings | No | Deprecated. Explicit installable workflow file list. Use `includes` instead. |
| `includes` | array of strings or mappings | No | Explicit installable package entries. String entries use path conventions; mapping entries declare an explicit source-to-destination install path. |

### 4.2 `manifest-version`

Expand Down Expand Up @@ -94,6 +95,37 @@ Duplicate entries SHOULD be ignored after normalization.

**Path-traversal safety**: Each entry in `files` MUST NOT contain a path-traversal sequence. Specifically, any entry that contains `../` (or `..\` on Windows-style paths), begins with `../`, or resolves to a path outside the package root after normalization MUST be rejected with a validation error. Implementations MUST NOT follow symlinks that would escape the package root during file resolution. This rule applies regardless of the number of traversal components in the path (e.g., `../../etc/passwd` and `workflows/../../hidden` are both prohibited).

### 4.9 `includes`

If present, `includes` MUST be an array whose entries are either strings or mappings.

**String entries** follow the same rules as `files` (§4.8), with one special case that MUST be preserved for backward compatibility: a string entry beginning with `.github/` is resolved relative to the **consuming repository root**, not relative to the package root, even for nested packages. All other string entries (for example `workflows/review.md`) are resolved relative to the package root.

**Mapping entries** declare an explicit source-to-destination install mapping and MUST contain:

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `source` | string | Yes | Path of the file to install, always resolved relative to the package root, including for nested packages. The `.github/` special case of string entries MUST NOT apply. |
| `destination` | string | Yes | Install path, resolved relative to the consuming repository root. |
| `kind` | string | No | Either `agentic-workflow` or `action-workflow`. When present, it MUST match the file extension of `source`. |

Mapping entries let a distribution repository keep executable workflow assets inert outside its own `.github/workflows/` directory and still install them into the consuming repository's `.github/workflows/`.

Implementations MUST reject a mapping entry when any of the following holds:

- `source` or `destination` is empty, absolute, or contains a path-traversal sequence that escapes its root;
- `source` resolves to a symbolic link or to a path outside the package root;
- `source` or `destination` does not end in `.md` or `.yml`, or ends in `.lock.yml`;
Comment on lines +116 to +118
- `destination` is not a direct child of `.github/workflows/`;
- the file extension of `destination` differs from the file extension of `source`;
- `kind` is present and does not match the kind implied by the `source` extension.

Implementations MUST detect two entries that resolve to the same `destination` and MUST fail before writing any file.

Mapping entries follow the same install semantics as string entries: `.md` sources are compiled under their destination file name, and `.yml` sources are copied verbatim. Package-provided post-install shell code MUST NOT be executed.

`gh aw add`, `gh aw add-wizard`, and `gh aw update` MUST use identical mapping semantics, and `gh aw update` MUST continue to track the manifest source of installed files.

## 5. Installable file resolution

Supported installable paths are:
Expand All @@ -102,6 +134,8 @@ Supported installable paths are:
- `.github/workflows/<name>.md`
- `.github/workflows/<name>.yml` (raw GitHub Actions YAML; direct children only, `.lock.yml` excluded)

Mapping entries in `includes` (§4.9) may declare any package-relative `source`; their `destination` MUST be a direct child of `.github/workflows/`.

Nested descendants under the markdown directories are also valid when referenced explicitly in `files`. Raw `.yml` action workflows MUST be direct children of `.github/workflows/`; nested `.yml` files are rejected.

Raw `.yml` action workflows are installed verbatim: `gh aw add` copies the file to `.github/workflows/<name>.yml` and performs no frontmatter parsing, no dependency resolution, and no compilation. No `.lock.yml` is produced.
Expand Down
2 changes: 2 additions & 0 deletions pkg/cli/add_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ func addWorkflowWithTracking(ctx context.Context, resolved *ResolvedWorkflow, tr
return err
}

// For package manifest entries WorkflowName is derived from the entry's install
// destination, so mapped entries install under their declared destination name.
workflowName := workflowSpec.WorkflowName
if opts.Name != "" {
workflowName = opts.Name
Expand Down
Loading
Loading