Skip to content
Closed
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
9 changes: 4 additions & 5 deletions .github/workflows/shared/squad.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@
# the activation job.
engine:
id: copilot
on:
ambient-folders:
- .squad
- .github/agents
ambient-folders:
- .squad
- .github/agents

jobs:
activation:
Expand Down Expand Up @@ -62,7 +61,7 @@ install/init lifecycle out of the agent job:
npm release, optionally mints a GitHub App installation token (or uses a supplied
PAT) so `squad init` can see other organizations or private repositories, and runs
`squad init --preset default` (idempotent).
2. **`on.ambient-folders`** — bundles the resulting `.squad/` team state and
2. **`ambient-folders`** — bundles the resulting `.squad/` team state and
`.github/agents/` files into the standard activation artifact alongside the rest
of the prompt/skills/sub-agent packaging, then restores them into the agent
checkout. The Squad CLI itself is never installed in the agent job; only the
Expand Down
50 changes: 4 additions & 46 deletions .github/workflows/squad-game-planner.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Shared workflows that run activation steps (e.g., Squad CLI initialization) prod

### Decision

We will introduce a new `on.ambient-folders` frontmatter field in gh-aw workflow markdown files. Shared workflows declare the workspace-relative folder paths they produce; the compiler merges those declarations across all imports (deduplicating), adds the declared folders to the activation sparse-checkout, stages them into `/tmp/gh-aw/ambient-folders/` immediately before the activation artifact is uploaded, includes that staging directory in the artifact path list, and emits a restore step in the agent job after the last custom checkout (so multi-checkout workflows do not lose ambient content). Workflows whose `on:` block contains only import-safe keys (including `ambient-folders`) are classified as shared components rather than standalone workflows.
We will introduce a top-level `ambient-folders` frontmatter field in gh-aw workflow markdown files. Shared workflows declare the workspace-relative folder paths they produce; the compiler merges those declarations across all imports (deduplicating), adds the declared folders to the activation sparse-checkout, and adds them directly to the activation artifact path list. The artifact is extracted at its root in downstream jobs, preserving both the generated `/tmp/gh-aw` files and the declared workspace folders. Workflows with no trigger event remain shared components.

### Alternatives Considered

Expand All @@ -31,8 +31,7 @@ Not chosen because: it couples the platform to specific tooling choices, does no
### Consequences

#### Positive
- Shared workflows can declare their folder dependencies once in frontmatter; the compiler handles staging and restore automatically, removing the need for per-consumer download steps.
- The restore step is injected after the last custom checkout, which prevents multi-checkout workflows from clobbering ambient content.
- Shared workflows can declare their folder dependencies once in frontmatter; the compiler packages them with the standard activation artifact, removing the need for per-consumer download steps.
- The merge strategy (union/deduplicated) means multiple shared workflows each declaring overlapping folders still produce a single coherent restore.
- The field is validated by JSON Schema with path-traversal protections (no `..`, no absolute paths), keeping the attack surface small.

Expand All @@ -41,8 +40,7 @@ Not chosen because: it couples the platform to specific tooling choices, does no
- The shared-workflow classification logic now depends on an `on:` field value inspection (`IsImportSafeSharedWorkflowOn`), which increases coupling between the parser and compiler orchestration.

#### Neutral
- Workflows with `on: ambient-folders: [...]` and no trigger event are now classified as shared components, consistent with the existing behaviour for other import-safe `on:` keys (`skip-if-match`, `github-token`, etc.).
- The staging script uses `cp -a` (archive copy) and silently skips missing source folders, so workflows that conditionally produce folders do not fail the activation job.
- Workflows with `ambient-folders: [...]` and no trigger event are classified as shared components.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/frontmatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ The `on:` section uses standard GitHub Actions syntax to define workflow trigger
- `restore-memory:` - Opt in to restoring memory stores before `on.steps` in pre-activation (default: `false`)
- `permissions:` - Grant additional GitHub token scopes to the pre-activation job (for use with `on.steps:` API calls)
- `needs:` - Add custom job dependencies that both `pre_activation` and `activation` must wait for
- `ambient-folders:` - Workspace-relative folders to bundle in the activation artifact and restore before the agent runs
- `ambient-folders:` - Workspace-relative folders to include in the activation artifact
- `github-token:` - Custom token for activation job reactions, status comments, and skip-if search queries
- `github-app:` - GitHub App for minting a short-lived token used by the activation job and all skip-if search steps

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Paths are resolved within the `.github` folder. You can specify paths with or wi

## Shared Workflow Components

Files without a trigger event are shared workflow components: they are validated, can be imported by other workflows, and are not compiled into standalone GitHub Actions. Shared components may also define import-safe `on` keys (`ambient-folders`, `skip-if-match`, `skip-if-no-match`, `skip-roles`, `skip-bots`, `github-token`, `github-app`) for reuse through imports.
Files without a trigger event are shared workflow components: they are validated, can be imported by other workflows, and are not compiled into standalone GitHub Actions. Shared components may also define `ambient-folders` and import-safe `on` keys (`skip-if-match`, `skip-if-no-match`, `skip-roles`, `skip-bots`, `github-token`, `github-app`) for reuse through imports.

A shared workflow's frontmatter can contain comments only. Comment-only frontmatter is treated as present, so the file still parses as a shared component and produces an empty frontmatter map rather than failing with `no frontmatter found`. Only truly missing or whitespace-only frontmatter is rejected.

Expand Down
7 changes: 0 additions & 7 deletions pkg/constants/job_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,6 @@ const ArtifactPrefixOutputName = "artifact_prefix"
// (aw_info.json and prompt.txt).
const ActivationArtifactName = "activation"

// ActivationStageAmbientFoldersStepName is the step name used to stage ambient
// folders before the activation artifact is packaged. It is a stable anchor
// used to determine the insertion point for jobs.activation.steps injected
// via built-in job step merging; keep it in sync with any renames of that
// step.
const ActivationStageAmbientFoldersStepName = "Stage ambient folders for activation artifact"

// ActivationUploadArtifactStepName is the step name used to upload the
// activation artifact. It is a stable anchor used to determine the insertion
// point for jobs.activation.steps injected via built-in job step merging;
Expand Down
30 changes: 30 additions & 0 deletions pkg/parser/content_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,36 @@ func extractOnSectionFieldFromMap(frontmatter map[string]any, fieldName string)
return string(jsonData), nil
}

// extractTopLevelListFieldFromMap extracts a top-level string or array field as a JSON array.
func extractTopLevelListFieldFromMap(frontmatter map[string]any, fieldName string) (string, error) {
fieldValue, exists := frontmatter[fieldName]
if !exists {
return "[]", nil
}

var normalizedValue []any
switch v := fieldValue.(type) {
case string:
if v != "" {
normalizedValue = []any{v}
}
case []any:
normalizedValue = v
case []string:
for _, s := range v {
normalizedValue = append(normalizedValue, s)
}
default:
return "[]", nil
}

jsonData, err := json.Marshal(normalizedValue)
if err != nil {
return "[]", nil
}
return string(jsonData), nil
}

// extractOnSectionAnyFieldFromMap extracts a specific field from the on: section in an already-parsed
// frontmatter map as a JSON string, handling any value type.
// This avoids re-parsing YAML when the frontmatter has already been parsed.
Expand Down
4 changes: 2 additions & 2 deletions pkg/parser/import_field_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ func (acc *importAccumulator) appendYAMLBuilderField(fm map[string]any, field st

// extractActivationFields extracts activation and authentication-related fields from
// the frontmatter map: bots, skip-roles, skip-bots, skip-if-match, skip-if-no-match,
// on.ambient-folders, on.github-token, on.github-app, top-level github-app, and checkout.
// ambient-folders, on.github-token, on.github-app, top-level github-app, and checkout.
//
// Side effects: acc.bots, acc.botsSet, acc.skipRoles, acc.skipRolesSet, acc.skipBots,
// acc.skipBotsSet, acc.skipIfMatch, acc.skipIfNoMatch, acc.activationGitHubToken,
Expand Down Expand Up @@ -517,7 +517,7 @@ func (acc *importAccumulator) mergeSkipBots(fm map[string]any) {
}

func (acc *importAccumulator) mergeAmbientFolders(fm map[string]any) {
mergeJSONStringListField(fm, "ambient-folders", "[]", acc.ambientFoldersSet, &acc.ambientFolders, extractOnSectionFieldFromMap)
mergeJSONStringListField(fm, "ambient-folders", "[]", acc.ambientFoldersSet, &acc.ambientFolders, extractTopLevelListFieldFromMap)
}

func mergeJSONStringListField(
Expand Down
14 changes: 6 additions & 8 deletions pkg/parser/import_field_extractor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,19 +251,17 @@ func TestAmbientFoldersExtractedFromMdImport(t *testing.T) {
sharedDir := filepath.Join(tmpDir, "shared")
require.NoError(t, os.MkdirAll(sharedDir, 0755), "Failed to create shared dir")
require.NoError(t, os.WriteFile(filepath.Join(sharedDir, "first.md"), []byte(`---
on:
ambient-folders:
- .squad
- .github/agents
ambient-folders:
- .squad
- .github/agents
---

# First shared workflow
`), 0644), "Failed to write first shared file")
require.NoError(t, os.WriteFile(filepath.Join(sharedDir, "second.md"), []byte(`---
on:
ambient-folders:
- .squad
- .config/agents
ambient-folders:
- .squad
- .config/agents
---

# Second shared workflow
Expand Down
2 changes: 1 addition & 1 deletion pkg/parser/import_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ type ImportsResult struct {
MergedSkipBots []string // Merged skip-bots list from all imports (union of usernames)
MergedSkipIfMatch string // on.skip-if-match from first imported workflow that defines it (JSON-encoded)
MergedSkipIfNoMatch string // on.skip-if-no-match from first imported workflow that defines it (JSON-encoded)
MergedAmbientFolders []string // Merged on.ambient-folders list from all imports (union, deduplicated)
MergedAmbientFolders []string // Merged ambient-folders list from all imports (union, deduplicated)
MergedActivationGitHubToken string // GitHub token from on.github-token in first imported workflow that defines it
MergedActivationGitHubApp string // JSON-encoded on.github-app from first imported workflow that defines it
MergedTopLevelGitHubApp string // JSON-encoded top-level github-app from first imported workflow that defines it
Expand Down
2 changes: 0 additions & 2 deletions pkg/parser/schema_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ var schemaValidationLog = logger.New("parser:schema_validation")
var sharedWorkflowForbiddenFields = buildForbiddenFieldsMap()

var sharedWorkflowAllowedOnFieldList = []string{
"ambient-folders",
"skip-if-match",
"skip-if-no-match",
"skip-roles",
Expand All @@ -27,7 +26,6 @@ var sharedWorkflowAllowedOnFieldList = []string{
}

var sharedWorkflowAllowedOnFields = map[string]struct{}{
"ambient-folders": {},
"skip-if-match": {},
"skip-if-no-match": {},
"skip-roles": {},
Expand Down
Loading