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
25 changes: 25 additions & 0 deletions pkg/cli/workflows/test-top-level-github-app-activation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
on:
issues:
types: [opened]
reaction: eyes
permissions:
contents: read

github-app:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
safe-outputs:
create-issue:
title-prefix: "[automated] "
engine: copilot
---

# Top-Level GitHub App Fallback for Activation

This workflow demonstrates using a top-level github-app as a fallback for activation operations.

The top-level `github-app` is automatically applied to the activation job (reactions, status
comments, skip-if checks) when no `on.github-app` is defined.

When an issue is opened, react with 👀 and create a follow-up issue using the GitHub App token.
27 changes: 27 additions & 0 deletions pkg/cli/workflows/test-top-level-github-app-checkout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
on:
issues:
types: [opened]
permissions:
contents: read

github-app:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
checkout:
repository: myorg/private-repo
path: private
safe-outputs:
create-issue:
title-prefix: "[automated] "
engine: copilot
---

# Top-Level GitHub App Fallback for Checkout

This workflow demonstrates using a top-level github-app as a fallback for checkout operations.

The top-level `github-app` is automatically applied to checkout operations that do not have
their own `github-app` or `github-token` configured.

This is useful for checking out private repositories using the GitHub App installation token.
26 changes: 26 additions & 0 deletions pkg/cli/workflows/test-top-level-github-app-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
on:
issues:
types: [opened]
permissions:
contents: read

github-app:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
dependencies:
packages:
- myorg/private-skill
safe-outputs:
create-issue:
title-prefix: "[automated] "
engine: copilot
---

# Top-Level GitHub App Fallback for APM Dependencies

This workflow demonstrates using a top-level github-app as a fallback for APM dependencies.

The top-level `github-app` is automatically applied to APM package installations when no
`dependencies.github-app` is configured. This allows installing APM packages from private
repositories across organizations using the GitHub App installation token.
28 changes: 28 additions & 0 deletions pkg/cli/workflows/test-top-level-github-app-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
on:
issues:
types: [opened]
permissions:
contents: read

github-app:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
tools:
github:
mode: remote
toolsets: [default]
safe-outputs:
create-issue:
title-prefix: "[automated] "
engine: copilot
---

# Top-Level GitHub App Fallback for GitHub MCP Tool

This workflow demonstrates using a top-level github-app as a fallback for tools.github
token minting operations.

The top-level `github-app` is automatically applied to the GitHub MCP tool configuration
when no `tools.github.github-app` is defined. This mints a GitHub App installation access
token for the GitHub MCP server to use when making API calls.
33 changes: 33 additions & 0 deletions pkg/cli/workflows/test-top-level-github-app-override.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
on:
issues:
types: [opened]
reaction: eyes
github-app:
app-id: ${{ vars.ACTIVATION_APP_ID }}
private-key: ${{ secrets.ACTIVATION_APP_KEY }}
permissions:
contents: read

github-app:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
safe-outputs:
github-app:
app-id: ${{ vars.SAFE_OUTPUTS_APP_ID }}
private-key: ${{ secrets.SAFE_OUTPUTS_APP_KEY }}
create-issue:
title-prefix: "[automated] "
engine: copilot
---

# Section-Specific GitHub App Takes Precedence Over Top-Level

This workflow demonstrates that section-specific github-app configurations take precedence
over the top-level github-app fallback.

- `on.github-app` is explicitly set → activation uses ACTIVATION_APP_ID, not APP_ID
- `safe-outputs.github-app` is explicitly set → safe-outputs uses SAFE_OUTPUTS_APP_ID, not APP_ID

The top-level github-app (APP_ID) is only used as a fallback when sections do not define
their own github-app.
25 changes: 25 additions & 0 deletions pkg/cli/workflows/test-top-level-github-app-safe-outputs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
on:
issues:
types: [opened]
permissions:
contents: read

github-app:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
safe-outputs:
create-issue:
title-prefix: "[automated] "
labels: [automation]
engine: copilot
---

# Top-Level GitHub App Fallback for Safe Outputs

This workflow demonstrates using a top-level github-app as a fallback for safe-outputs.

The top-level `github-app` is automatically applied to the safe-outputs job when no
section-specific `github-app` is defined under `safe-outputs:`.

When an issue is opened, analyze it and create a follow-up issue using the GitHub App token.
40 changes: 35 additions & 5 deletions pkg/parser/import_field_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type importAccumulator struct {
// First on.github-token / on.github-app found across all imported files (first-wins strategy)
activationGitHubToken string
activationGitHubApp string // JSON-encoded GitHubAppConfig
// First top-level github-app found across all imported files (first-wins strategy)
topLevelGitHubApp string // JSON-encoded GitHubAppConfig
}

// newImportAccumulator creates and initializes a new importAccumulator.
Expand Down Expand Up @@ -228,6 +230,14 @@ func (acc *importAccumulator) extractAllImportFields(content []byte, item import
}
}

// Extract top-level github-app from imported file (first-wins: only set if not yet populated)
if acc.topLevelGitHubApp == "" {
if appJSON := extractTopLevelGitHubApp(string(content)); appJSON != "" {
acc.topLevelGitHubApp = appJSON
log.Printf("Extracted top-level github-app from import: %s", item.fullPath)
}
}

// Extract and merge plugins from imported file (merge into set to avoid duplicates).
// Handles both simple string format and object format with MCP configs.
pluginsContent, err := extractFrontmatterField(string(content), "plugins", "[]")
Expand Down Expand Up @@ -331,6 +341,7 @@ func (acc *importAccumulator) toImportsResult(topologicalOrder []string) *Import
ImportInputs: acc.importInputs,
MergedActivationGitHubToken: acc.activationGitHubToken,
MergedActivationGitHubApp: acc.activationGitHubApp,
MergedTopLevelGitHubApp: acc.topLevelGitHubApp,
}
}

Expand Down Expand Up @@ -370,11 +381,10 @@ func extractOnGitHubToken(content string) string {
return token
}

// extractOnGitHubApp returns the JSON-encoded on.github-app object from workflow content.
// Returns "" if the field is absent, not a valid object, or missing required fields.
func extractOnGitHubApp(content string) string {
appJSON, err := extractOnSectionAnyField(content, "github-app")
if err != nil || appJSON == "" || appJSON == "null" {
// validateGitHubAppJSON validates that a JSON-encoded GitHub App configuration has the required
// fields (app-id and private-key). Returns the input JSON if valid, or "" otherwise.
func validateGitHubAppJSON(appJSON string) string {
if appJSON == "" || appJSON == "null" {
return ""
}
var appMap map[string]any
Expand All @@ -389,3 +399,23 @@ func extractOnGitHubApp(content string) string {
}
return appJSON
}

// extractOnGitHubApp returns the JSON-encoded on.github-app object from workflow content.
// Returns "" if the field is absent, not a valid object, or missing required fields.
func extractOnGitHubApp(content string) string {
appJSON, err := extractOnSectionAnyField(content, "github-app")
if err != nil {
return ""
}
return validateGitHubAppJSON(appJSON)
}

// extractTopLevelGitHubApp returns the JSON-encoded top-level github-app object from workflow content.
// Returns "" if the field is absent, not a valid object, or missing required fields.
func extractTopLevelGitHubApp(content string) string {
appJSON, err := extractFrontmatterField(content, "github-app", "")
if err != nil {
return ""
}
return validateGitHubAppJSON(appJSON)
}
1 change: 1 addition & 0 deletions pkg/parser/import_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type ImportsResult struct {
MergedSkipBots []string // Merged skip-bots list from all imports (union of usernames)
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
MergedPostSteps string // Merged post-steps configuration from all imports (appended in order)
MergedLabels []string // Merged labels from all imports (union of label names)
MergedCaches []string // Merged cache configurations from all imports (appended in order)
Expand Down
4 changes: 4 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8162,6 +8162,10 @@
"additionalProperties": false
}
]
},
"github-app": {
"$ref": "#/$defs/github_app",
"description": "Top-level GitHub App configuration used as a fallback for all nested github-app token minting operations (on, safe-outputs, checkout, tools.github, dependencies). When a nested section does not define its own github-app, this top-level configuration is used automatically."
}
},
"additionalProperties": false,
Expand Down
Loading
Loading