From 8e5088e4167b2f2debf39a0f314bd73b4387cc48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:12:40 +0000 Subject: [PATCH 1/8] Initial plan From d2a12a3c60eb34921439831ac4e29999088f3204 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:44:18 +0000 Subject: [PATCH 2/8] Add scope, github-token, and github-app support to skip-if-no-match and skip-if-match Implements Option A from the feature request: users can now add scope:none, github-token, and github-app fields to skip-if-no-match and skip-if-match conditions to enable cross-repo and org-wide queries. - Add Scope, GitHubToken, GitHubApp fields to SkipIfMatchConfig and SkipIfNoMatchConfig - Extract new fields from frontmatter in stop_after.go using shared helper extractSkipIfAuthConfig - Emit GH_AW_SKIP_SCOPE env var and pass custom token/app token in compiler_pre_activation_job.go - Add buildSkipIfAppTokenMintStep and resolveSkipIfToken helpers for app token generation - Honor GH_AW_SKIP_SCOPE=none in check_skip_if_no_match.cjs and check_skip_if_match.cjs - Add scope, github-token, github-app to JSON schema for skip-if conditions - Comment out new fields in lock file frontmatter extraction (frontmatter_extraction_yaml.go) - Add comprehensive Go and JavaScript tests for all new functionality Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/check_skip_if_match.cjs | 17 +- actions/setup/js/check_skip_if_match.test.cjs | 65 +++++++ actions/setup/js/check_skip_if_no_match.cjs | 18 +- .../setup/js/check_skip_if_no_match.test.cjs | 37 ++++ pkg/parser/schemas/main_workflow_schema.json | 82 ++++++++- pkg/workflow/compiler_pre_activation_job.go | 78 ++++++++ pkg/workflow/compiler_types.go | 14 +- pkg/workflow/frontmatter_extraction_yaml.go | 4 +- pkg/workflow/skip_if_match_test.go | 165 +++++++++++++++++ pkg/workflow/skip_if_no_match_test.go | 170 ++++++++++++++++++ pkg/workflow/stop_after.go | 70 +++++++- 11 files changed, 695 insertions(+), 25 deletions(-) diff --git a/actions/setup/js/check_skip_if_match.cjs b/actions/setup/js/check_skip_if_match.cjs index d646d03c935..522504add2d 100644 --- a/actions/setup/js/check_skip_if_match.cjs +++ b/actions/setup/js/check_skip_if_match.cjs @@ -8,6 +8,7 @@ async function main() { const skipQuery = process.env.GH_AW_SKIP_QUERY; const workflowName = process.env.GH_AW_WORKFLOW_NAME; const maxMatchesStr = process.env.GH_AW_SKIP_MAX_MATCHES ?? "1"; + const skipScope = process.env.GH_AW_SKIP_SCOPE; if (!skipQuery) { core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_SKIP_QUERY not specified.`); @@ -28,14 +29,20 @@ async function main() { core.info(`Checking skip-if-match query: ${skipQuery}`); core.info(`Maximum matches threshold: ${maxMatches}`); - const { owner, repo } = context.repo; - const scopedQuery = `${skipQuery} repo:${owner}/${repo}`; - - core.info(`Scoped query: ${scopedQuery}`); + let searchQuery; + if (skipScope === "none") { + // Use query as-is without appending repo:owner/repo scoping + searchQuery = skipQuery; + core.info(`Using raw query (scope: none): ${searchQuery}`); + } else { + const { owner, repo } = context.repo; + searchQuery = `${skipQuery} repo:${owner}/${repo}`; + core.info(`Scoped query: ${searchQuery}`); + } try { const response = await github.rest.search.issuesAndPullRequests({ - q: scopedQuery, + q: searchQuery, per_page: 1, }); diff --git a/actions/setup/js/check_skip_if_match.test.cjs b/actions/setup/js/check_skip_if_match.test.cjs index 92713d14465..88faea8bf74 100644 --- a/actions/setup/js/check_skip_if_match.test.cjs +++ b/actions/setup/js/check_skip_if_match.test.cjs @@ -174,3 +174,68 @@ const mockCore = { })); })); })); + +describe("check_skip_if_match.cjs - scope support", () => { + const { main } = require("./check_skip_if_match.cjs"); + const { ERR_CONFIG } = require("./error_codes.cjs"); + + let mockCoreScope; + let mockGithubScope; + let mockContextScope; + + beforeEach(() => { + vi.clearAllMocks(); + mockCoreScope = { + info: vi.fn(), + warning: vi.fn(), + setFailed: vi.fn(), + setOutput: vi.fn(), + }; + mockGithubScope = { rest: { search: { issuesAndPullRequests: vi.fn() } } }; + mockContextScope = { repo: { owner: "testowner", repo: "testrepo" } }; + global.core = mockCoreScope; + global.github = mockGithubScope; + global.context = mockContextScope; + }); + + afterEach(() => { + delete process.env.GH_AW_SKIP_QUERY; + delete process.env.GH_AW_WORKFLOW_NAME; + delete process.env.GH_AW_SKIP_MAX_MATCHES; + delete process.env.GH_AW_SKIP_SCOPE; + }); + + it("should use raw query when GH_AW_SKIP_SCOPE is 'none'", async () => { + process.env.GH_AW_SKIP_QUERY = "org:myorg label:blocked is:issue is:open"; + process.env.GH_AW_WORKFLOW_NAME = "test-workflow"; + process.env.GH_AW_SKIP_SCOPE = "none"; + + let capturedQuery; + mockGithubScope.rest.search.issuesAndPullRequests.mockImplementation(async ({ q }) => { + capturedQuery = q; + return { data: { total_count: 0 } }; + }); + + await main(); + + expect(capturedQuery).toBe("org:myorg label:blocked is:issue is:open"); + expect(mockCoreScope.info).toHaveBeenCalledWith("Using raw query (scope: none): org:myorg label:blocked is:issue is:open"); + expect(mockCoreScope.setOutput).toHaveBeenCalledWith("skip_check_ok", "true"); + }); + + it("should scope query to repo when GH_AW_SKIP_SCOPE is not set", async () => { + process.env.GH_AW_SKIP_QUERY = "is:issue is:open label:bug"; + process.env.GH_AW_WORKFLOW_NAME = "test-workflow"; + + let capturedQuery; + mockGithubScope.rest.search.issuesAndPullRequests.mockImplementation(async ({ q }) => { + capturedQuery = q; + return { data: { total_count: 0 } }; + }); + + await main(); + + expect(capturedQuery).toBe("is:issue is:open label:bug repo:testowner/testrepo"); + expect(mockCoreScope.info).toHaveBeenCalledWith("Scoped query: is:issue is:open label:bug repo:testowner/testrepo"); + }); +}); diff --git a/actions/setup/js/check_skip_if_no_match.cjs b/actions/setup/js/check_skip_if_no_match.cjs index 083ea68eb16..f1935d7ccdb 100644 --- a/actions/setup/js/check_skip_if_no_match.cjs +++ b/actions/setup/js/check_skip_if_no_match.cjs @@ -5,7 +5,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_API, ERR_CONFIG } = require("./error_codes.cjs"); async function main() { - const { GH_AW_SKIP_QUERY: skipQuery, GH_AW_WORKFLOW_NAME: workflowName, GH_AW_SKIP_MIN_MATCHES: minMatchesStr = "1" } = process.env; + const { GH_AW_SKIP_QUERY: skipQuery, GH_AW_WORKFLOW_NAME: workflowName, GH_AW_SKIP_MIN_MATCHES: minMatchesStr = "1", GH_AW_SKIP_SCOPE: skipScope } = process.env; if (!skipQuery) { core.setFailed(`${ERR_CONFIG}: Configuration error: GH_AW_SKIP_QUERY not specified.`); @@ -26,16 +26,22 @@ async function main() { core.info(`Checking skip-if-no-match query: ${skipQuery}`); core.info(`Minimum matches threshold: ${minMatches}`); - const { owner, repo } = context.repo; - const scopedQuery = `${skipQuery} repo:${owner}/${repo}`; - - core.info(`Scoped query: ${scopedQuery}`); + let searchQuery; + if (skipScope === "none") { + // Use query as-is without appending repo:owner/repo scoping + searchQuery = skipQuery; + core.info(`Using raw query (scope: none): ${searchQuery}`); + } else { + const { owner, repo } = context.repo; + searchQuery = `${skipQuery} repo:${owner}/${repo}`; + core.info(`Scoped query: ${searchQuery}`); + } try { const { data: { total_count: totalCount }, } = await github.rest.search.issuesAndPullRequests({ - q: scopedQuery, + q: searchQuery, per_page: 1, }); diff --git a/actions/setup/js/check_skip_if_no_match.test.cjs b/actions/setup/js/check_skip_if_no_match.test.cjs index 449010078af..8d89d7bfbe6 100644 --- a/actions/setup/js/check_skip_if_no_match.test.cjs +++ b/actions/setup/js/check_skip_if_no_match.test.cjs @@ -215,4 +215,41 @@ describe("check_skip_if_no_match", () => { expect(mockCore.infos).toContain("Scoped query: is:open is:issue label:enhancement repo:test-owner/test-repo"); expect(mockCore.infos).toContain("Search found 8 matching items"); }); + + it("should use raw query when GH_AW_SKIP_SCOPE is 'none'", async () => { + process.env.GH_AW_SKIP_QUERY = "org:myorg label:agent-fix is:issue is:open"; + process.env.GH_AW_WORKFLOW_NAME = "test-workflow"; + process.env.GH_AW_SKIP_SCOPE = "none"; + delete process.env.GH_AW_SKIP_MIN_MATCHES; + + let capturedQuery; + mockGithub.rest.search.issuesAndPullRequests = async ({ q }) => { + capturedQuery = q; + return { data: { total_count: 3 } }; + }; + + await main(); + + expect(capturedQuery).toBe("org:myorg label:agent-fix is:issue is:open"); + expect(mockCore.infos).toContain("Using raw query (scope: none): org:myorg label:agent-fix is:issue is:open"); + expect(mockCore.outputs["skip_no_match_check_ok"]).toBe("true"); + }); + + it("should scope query to repo when GH_AW_SKIP_SCOPE is not set", async () => { + process.env.GH_AW_SKIP_QUERY = "is:issue is:open label:bug"; + process.env.GH_AW_WORKFLOW_NAME = "test-workflow"; + delete process.env.GH_AW_SKIP_SCOPE; + delete process.env.GH_AW_SKIP_MIN_MATCHES; + + let capturedQuery; + mockGithub.rest.search.issuesAndPullRequests = async ({ q }) => { + capturedQuery = q; + return { data: { total_count: 1 } }; + }; + + await main(); + + expect(capturedQuery).toBe("is:issue is:open label:bug repo:test-owner/test-repo"); + expect(mockCore.infos).toContain("Scoped query: is:issue is:open label:bug repo:test-owner/test-repo"); + }); }); diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index be32a4cd42e..952c9555825 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -1339,13 +1339,50 @@ "description": "GitHub Actions expression that resolves to an integer at runtime" } ] + }, + "scope": { + "type": "string", + "enum": ["none"], + "description": "Scope for the search query. Set to 'none' to disable the automatic 'repo:owner/repo' scoping, enabling org-wide or cross-repo queries." + }, + "github-token": { + "type": "string", + "description": "Custom GitHub token to use for the search API call. Useful for cross-repo or org-wide searches that require additional permissions.", + "examples": ["${{ secrets.CROSS_ORG_TOKEN }}"] + }, + "github-app": { + "type": "object", + "description": "GitHub App configuration for minting a token used for the search API call.", + "properties": { + "app-id": { + "type": "string", + "description": "GitHub App ID (e.g., '${{ secrets.APP_ID }}'). Required." + }, + "private-key": { + "type": "string", + "description": "GitHub App private key (e.g., '${{ secrets.APP_PRIVATE_KEY }}'). Required." + }, + "owner": { + "type": "string", + "description": "Optional owner of the GitHub App installation (defaults to current repository owner)." + }, + "repositories": { + "type": "array", + "description": "Optional list of repositories to grant access to. Use ['*'] for org-wide access.", + "items": { + "type": "string" + } + } + }, + "required": ["app-id", "private-key"], + "additionalProperties": false } }, "additionalProperties": false, - "description": "Skip-if-match configuration object with query and maximum match count" + "description": "Skip-if-match configuration object with query, maximum match count, and optional scope/auth settings" } ], - "description": "Conditionally skip workflow execution when a GitHub search query has matches. Can be a string (query only, implies max=1) or an object with 'query' and optional 'max' fields." + "description": "Conditionally skip workflow execution when a GitHub search query has matches. Can be a string (query only, implies max=1) or an object with 'query', optional 'max', 'scope', 'github-token', and 'github-app' fields." }, "skip-if-no-match": { "oneOf": [ @@ -1365,13 +1402,50 @@ "type": "integer", "minimum": 1, "description": "Minimum number of items that must be matched for the workflow to proceed. Defaults to 1 if not specified." + }, + "scope": { + "type": "string", + "enum": ["none"], + "description": "Scope for the search query. Set to 'none' to disable the automatic 'repo:owner/repo' scoping, enabling org-wide or cross-repo queries." + }, + "github-token": { + "type": "string", + "description": "Custom GitHub token to use for the search API call. Useful for cross-repo or org-wide searches that require additional permissions.", + "examples": ["${{ secrets.CROSS_ORG_TOKEN }}"] + }, + "github-app": { + "type": "object", + "description": "GitHub App configuration for minting a token used for the search API call.", + "properties": { + "app-id": { + "type": "string", + "description": "GitHub App ID (e.g., '${{ secrets.APP_ID }}'). Required." + }, + "private-key": { + "type": "string", + "description": "GitHub App private key (e.g., '${{ secrets.APP_PRIVATE_KEY }}'). Required." + }, + "owner": { + "type": "string", + "description": "Optional owner of the GitHub App installation (defaults to current repository owner)." + }, + "repositories": { + "type": "array", + "description": "Optional list of repositories to grant access to. Use ['*'] for org-wide access.", + "items": { + "type": "string" + } + } + }, + "required": ["app-id", "private-key"], + "additionalProperties": false } }, "additionalProperties": false, - "description": "Skip-if-no-match configuration object with query and minimum match count" + "description": "Skip-if-no-match configuration object with query, minimum match count, and optional scope/auth settings" } ], - "description": "Conditionally skip workflow execution when a GitHub search query has no matches (or fewer than minimum). Can be a string (query only, implies min=1) or an object with 'query' and optional 'min' fields." + "description": "Conditionally skip workflow execution when a GitHub search query has no matches (or fewer than minimum). Can be a string (query only, implies min=1) or an object with 'query', optional 'min', 'scope', 'github-token', and 'github-app' fields." }, "skip-roles": { "oneOf": [ diff --git a/pkg/workflow/compiler_pre_activation_job.go b/pkg/workflow/compiler_pre_activation_job.go index 0d42c7a5885..19661cabdea 100644 --- a/pkg/workflow/compiler_pre_activation_job.go +++ b/pkg/workflow/compiler_pre_activation_job.go @@ -93,6 +93,11 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec // Extract workflow name for the skip-if-match check workflowName := data.Name + // Mint a GitHub App token for the skip-if-match check if a GitHub App is configured + if data.SkipIfMatch.GitHubApp != nil { + steps = append(steps, c.buildSkipIfAppTokenMintStep(data.SkipIfMatch.GitHubApp, constants.CheckSkipIfMatchStepID)...) + } + steps = append(steps, " - name: Check skip-if-match query\n") steps = append(steps, fmt.Sprintf(" id: %s\n", constants.CheckSkipIfMatchStepID)) steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/github-script"))) @@ -100,7 +105,15 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec steps = append(steps, fmt.Sprintf(" GH_AW_SKIP_QUERY: %q\n", data.SkipIfMatch.Query)) steps = append(steps, fmt.Sprintf(" GH_AW_WORKFLOW_NAME: %q\n", workflowName)) steps = append(steps, fmt.Sprintf(" GH_AW_SKIP_MAX_MATCHES: \"%d\"\n", data.SkipIfMatch.Max)) + if data.SkipIfMatch.Scope != "" { + steps = append(steps, fmt.Sprintf(" GH_AW_SKIP_SCOPE: %q\n", data.SkipIfMatch.Scope)) + } steps = append(steps, " with:\n") + // Use custom token or minted app token if configured + skipIfMatchToken := c.resolveSkipIfToken(data.SkipIfMatch.GitHubToken, data.SkipIfMatch.GitHubApp, constants.CheckSkipIfMatchStepID) + if skipIfMatchToken != "" { + steps = append(steps, fmt.Sprintf(" github-token: %s\n", skipIfMatchToken)) + } steps = append(steps, " script: |\n") steps = append(steps, generateGitHubScriptWithRequire("check_skip_if_match.cjs")) } @@ -110,6 +123,11 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec // Extract workflow name for the skip-if-no-match check workflowName := data.Name + // Mint a GitHub App token for the skip-if-no-match check if a GitHub App is configured + if data.SkipIfNoMatch.GitHubApp != nil { + steps = append(steps, c.buildSkipIfAppTokenMintStep(data.SkipIfNoMatch.GitHubApp, constants.CheckSkipIfNoMatchStepID)...) + } + steps = append(steps, " - name: Check skip-if-no-match query\n") steps = append(steps, fmt.Sprintf(" id: %s\n", constants.CheckSkipIfNoMatchStepID)) steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/github-script"))) @@ -117,7 +135,15 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec steps = append(steps, fmt.Sprintf(" GH_AW_SKIP_QUERY: %q\n", data.SkipIfNoMatch.Query)) steps = append(steps, fmt.Sprintf(" GH_AW_WORKFLOW_NAME: %q\n", workflowName)) steps = append(steps, fmt.Sprintf(" GH_AW_SKIP_MIN_MATCHES: \"%d\"\n", data.SkipIfNoMatch.Min)) + if data.SkipIfNoMatch.Scope != "" { + steps = append(steps, fmt.Sprintf(" GH_AW_SKIP_SCOPE: %q\n", data.SkipIfNoMatch.Scope)) + } steps = append(steps, " with:\n") + // Use custom token or minted app token if configured + skipIfNoMatchToken := c.resolveSkipIfToken(data.SkipIfNoMatch.GitHubToken, data.SkipIfNoMatch.GitHubApp, constants.CheckSkipIfNoMatchStepID) + if skipIfNoMatchToken != "" { + steps = append(steps, fmt.Sprintf(" github-token: %s\n", skipIfNoMatchToken)) + } steps = append(steps, " script: |\n") steps = append(steps, generateGitHubScriptWithRequire("check_skip_if_no_match.cjs")) } @@ -407,3 +433,55 @@ func (c *Compiler) extractPreActivationCustomFields(jobs map[string]any) ([]stri return customSteps, customOutputs, nil } + +// buildSkipIfAppTokenMintStep generates a GitHub App token mint step for use in skip-if-match or skip-if-no-match checks. +// The stepIDPrefix is used to derive a unique step id for the minted token. +func (c *Compiler) buildSkipIfAppTokenMintStep(app *GitHubAppConfig, checkStepID constants.StepID) []string { + var steps []string + tokenStepID := string(checkStepID) + "-app-token" + + steps = append(steps, " - name: Generate GitHub App token for skip-if check\n") + steps = append(steps, fmt.Sprintf(" id: %s\n", tokenStepID)) + steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/create-github-app-token"))) + steps = append(steps, " with:\n") + steps = append(steps, fmt.Sprintf(" app-id: %s\n", app.AppID)) + steps = append(steps, fmt.Sprintf(" private-key: %s\n", app.PrivateKey)) + + owner := app.Owner + if owner == "" { + owner = "${{ github.repository_owner }}" + } + steps = append(steps, fmt.Sprintf(" owner: %s\n", owner)) + + // Repositories field: use configured list, or default to current repo + if len(app.Repositories) == 1 && app.Repositories[0] == "*" { + // Org-wide access: omit repositories field entirely + } else if len(app.Repositories) == 1 { + steps = append(steps, fmt.Sprintf(" repositories: %s\n", app.Repositories[0])) + } else if len(app.Repositories) > 1 { + steps = append(steps, " repositories: |-\n") + for _, repo := range app.Repositories { + steps = append(steps, fmt.Sprintf(" %s\n", repo)) + } + } else { + steps = append(steps, " repositories: ${{ github.event.repository.name }}\n") + } + + steps = append(steps, " github-api-url: ${{ github.api_url }}\n") + + return steps +} + +// resolveSkipIfToken returns the GitHub token expression to use for a skip-if check step. +// Priority: GitHub App minted token > custom github-token > empty (use default GITHUB_TOKEN). +// When a non-empty value is returned, callers should emit `with.github-token: ` in the step. +func (c *Compiler) resolveSkipIfToken(githubToken string, githubApp *GitHubAppConfig, checkStepID constants.StepID) string { + if githubApp != nil { + tokenStepID := string(checkStepID) + "-app-token" + return fmt.Sprintf("${{ steps.%s.outputs.token }}", tokenStepID) + } + if githubToken != "" { + return githubToken + } + return "" +} diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 802042d46fe..d56d7b6b4a7 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -323,14 +323,20 @@ func (c *Compiler) GetSharedActionCache() *ActionCache { // SkipIfMatchConfig holds the configuration for skip-if-match conditions type SkipIfMatchConfig struct { - Query string // GitHub search query to check before running workflow - Max int // Maximum number of matches before skipping (defaults to 1) + Query string // GitHub search query to check before running workflow + Max int // Maximum number of matches before skipping (defaults to 1) + Scope string // Scope for the query: "none" disables auto repo:owner/repo scoping + GitHubToken string // Custom GitHub token to use for the search API call + GitHubApp *GitHubAppConfig // GitHub App config for minting a token for the search API call } // SkipIfNoMatchConfig holds the configuration for skip-if-no-match conditions type SkipIfNoMatchConfig struct { - Query string // GitHub search query to check before running workflow - Min int // Minimum number of matches required to proceed (defaults to 1) + Query string // GitHub search query to check before running workflow + Min int // Minimum number of matches required to proceed (defaults to 1) + Scope string // Scope for the query: "none" disables auto repo:owner/repo scoping + GitHubToken string // Custom GitHub token to use for the search API call + GitHubApp *GitHubAppConfig // GitHub App config for minting a token for the search API call } // WorkflowData holds all the data needed to generate a GitHub Actions workflow diff --git a/pkg/workflow/frontmatter_extraction_yaml.go b/pkg/workflow/frontmatter_extraction_yaml.go index 7d47dbd755e..512532d39ca 100644 --- a/pkg/workflow/frontmatter_extraction_yaml.go +++ b/pkg/workflow/frontmatter_extraction_yaml.go @@ -368,14 +368,14 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat } else if strings.HasPrefix(trimmedLine, "skip-if-match:") { shouldComment = true commentReason = " # Skip-if-match processed as search check in pre-activation job" - } else if inSkipIfMatch && (strings.HasPrefix(trimmedLine, "query:") || strings.HasPrefix(trimmedLine, "max:")) { + } else if inSkipIfMatch && (strings.HasPrefix(trimmedLine, "query:") || strings.HasPrefix(trimmedLine, "max:") || strings.HasPrefix(trimmedLine, "scope:") || strings.HasPrefix(trimmedLine, "github-token:") || strings.HasPrefix(trimmedLine, "github-app:")) { // Comment out nested fields in skip-if-match object shouldComment = true commentReason = "" } else if strings.HasPrefix(trimmedLine, "skip-if-no-match:") { shouldComment = true commentReason = " # Skip-if-no-match processed as search check in pre-activation job" - } else if inSkipIfNoMatch && (strings.HasPrefix(trimmedLine, "query:") || strings.HasPrefix(trimmedLine, "min:")) { + } else if inSkipIfNoMatch && (strings.HasPrefix(trimmedLine, "query:") || strings.HasPrefix(trimmedLine, "min:") || strings.HasPrefix(trimmedLine, "scope:") || strings.HasPrefix(trimmedLine, "github-token:") || strings.HasPrefix(trimmedLine, "github-app:")) { // Comment out nested fields in skip-if-no-match object shouldComment = true commentReason = "" diff --git a/pkg/workflow/skip_if_match_test.go b/pkg/workflow/skip_if_match_test.go index 51a679cfa51..a4c6d313ccf 100644 --- a/pkg/workflow/skip_if_match_test.go +++ b/pkg/workflow/skip_if_match_test.go @@ -285,4 +285,169 @@ This workflow uses object format but omits max (defaults to 1). t.Error("Expected GH_AW_SKIP_MAX_MATCHES environment variable with default value 1") } }) + + t.Run("skip_if_match_with_scope_none", func(t *testing.T) { + workflowContent := `--- +on: + schedule: + - cron: "*/15 * * * *" + skip-if-match: + query: "org:myorg label:blocked is:issue is:open" + scope: none +engine: claude +--- + +# Skip If Match With Scope None + +This workflow uses scope:none for org-wide search. +` + workflowFile := filepath.Join(tmpDir, "skip-match-scope-none-workflow.md") + if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil { + t.Fatal(err) + } + + err := compiler.CompileWorkflow(workflowFile) + if err != nil { + t.Fatalf("Compilation failed: %v", err) + } + + lockFile := stringutil.MarkdownToLockFile(workflowFile) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + lockContentStr := string(lockContent) + + // Verify skip-if-match check is present + if !strings.Contains(lockContentStr, "Check skip-if-match query") { + t.Error("Expected skip-if-match check to be present") + } + + // Verify GH_AW_SKIP_SCOPE is set to "none" + if !strings.Contains(lockContentStr, `GH_AW_SKIP_SCOPE: "none"`) { + t.Error("Expected GH_AW_SKIP_SCOPE environment variable set to none") + } + + // Verify scope is commented out in frontmatter + if !strings.Contains(lockContentStr, "# scope:") { + t.Error("Expected scope to be commented out in lock file") + } + }) + + t.Run("skip_if_match_with_github_token", func(t *testing.T) { + workflowContent := `--- +on: + schedule: + - cron: "*/15 * * * *" + skip-if-match: + query: "org:myorg label:blocked is:issue is:open" + scope: none + github-token: ${{ secrets.CROSS_ORG_TOKEN }} +engine: claude +--- + +# Skip If Match With Custom Token + +This workflow uses a custom token for org-wide search. +` + workflowFile := filepath.Join(tmpDir, "skip-match-github-token-workflow.md") + if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil { + t.Fatal(err) + } + + err := compiler.CompileWorkflow(workflowFile) + if err != nil { + t.Fatalf("Compilation failed: %v", err) + } + + lockFile := stringutil.MarkdownToLockFile(workflowFile) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + lockContentStr := string(lockContent) + + // Verify skip-if-match check is present + if !strings.Contains(lockContentStr, "Check skip-if-match query") { + t.Error("Expected skip-if-match check to be present") + } + + // Verify the custom github-token is passed via with.github-token + if !strings.Contains(lockContentStr, "github-token: ${{ secrets.CROSS_ORG_TOKEN }}") { + t.Error("Expected github-token to be set in with section for skip-if-match step") + } + + // Verify GH_AW_SKIP_SCOPE is set to "none" + if !strings.Contains(lockContentStr, `GH_AW_SKIP_SCOPE: "none"`) { + t.Error("Expected GH_AW_SKIP_SCOPE environment variable set to none") + } + }) + + t.Run("skip_if_match_with_github_app", func(t *testing.T) { + workflowContent := `--- +on: + schedule: + - cron: "*/15 * * * *" + skip-if-match: + query: "org:myorg label:blocked is:issue is:open" + scope: none + github-app: + app-id: ${{ secrets.WORKFLOW_APP_ID }} + private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} + owner: myorg +engine: claude +--- + +# Skip If Match With GitHub App + +This workflow uses a GitHub App token for org-wide search. +` + workflowFile := filepath.Join(tmpDir, "skip-match-github-app-workflow.md") + if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil { + t.Fatal(err) + } + + err := compiler.CompileWorkflow(workflowFile) + if err != nil { + t.Fatalf("Compilation failed: %v", err) + } + + lockFile := stringutil.MarkdownToLockFile(workflowFile) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + lockContentStr := string(lockContent) + + // Verify the GitHub App token mint step is generated + if !strings.Contains(lockContentStr, "Generate GitHub App token for skip-if check") { + t.Error("Expected GitHub App token mint step to be present") + } + + // Verify app-id and private-key are in the mint step + if !strings.Contains(lockContentStr, "app-id: ${{ secrets.WORKFLOW_APP_ID }}") { + t.Error("Expected app-id in the GitHub App token mint step") + } + if !strings.Contains(lockContentStr, "private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }}") { + t.Error("Expected private-key in the GitHub App token mint step") + } + + // Verify owner is passed + if !strings.Contains(lockContentStr, "owner: myorg") { + t.Error("Expected owner to be set in GitHub App token mint step") + } + + // Verify the minted token is used in the skip-if step + if !strings.Contains(lockContentStr, "github-token: ${{ steps.check_skip_if_match-app-token.outputs.token }}") { + t.Error("Expected minted app token to be used in skip-if-match step") + } + + // Verify GH_AW_SKIP_SCOPE is set to "none" + if !strings.Contains(lockContentStr, `GH_AW_SKIP_SCOPE: "none"`) { + t.Error("Expected GH_AW_SKIP_SCOPE environment variable set to none") + } + }) } diff --git a/pkg/workflow/skip_if_no_match_test.go b/pkg/workflow/skip_if_no_match_test.go index a51657d1fc0..a0f09de52e1 100644 --- a/pkg/workflow/skip_if_no_match_test.go +++ b/pkg/workflow/skip_if_no_match_test.go @@ -339,4 +339,174 @@ This workflow uses both skip-if-match and skip-if-no-match. t.Error("Expected activated output to include skip_no_match_check_ok condition") } }) + + t.Run("skip_if_no_match_with_scope_none", func(t *testing.T) { + workflowContent := `--- +on: + schedule: + - cron: "*/15 * * * *" + skip-if-no-match: + query: "org:myorg label:agent-fix is:issue is:open" + scope: none +engine: claude +--- + +# Skip If No Match With Scope None + +This workflow uses scope:none for org-wide search. +` + workflowFile := filepath.Join(tmpDir, "skip-no-match-scope-none-workflow.md") + if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil { + t.Fatal(err) + } + + err := compiler.CompileWorkflow(workflowFile) + if err != nil { + t.Fatalf("Compilation failed: %v", err) + } + + lockFile := stringutil.MarkdownToLockFile(workflowFile) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + lockContentStr := string(lockContent) + + // Verify skip-if-no-match check is present + if !strings.Contains(lockContentStr, "Check skip-if-no-match query") { + t.Error("Expected skip-if-no-match check to be present") + } + + // Verify the skip query environment variable is set correctly + if !strings.Contains(lockContentStr, `GH_AW_SKIP_QUERY: "org:myorg label:agent-fix is:issue is:open"`) { + t.Error("Expected GH_AW_SKIP_QUERY environment variable with correct value") + } + + // Verify GH_AW_SKIP_SCOPE is set to "none" + if !strings.Contains(lockContentStr, `GH_AW_SKIP_SCOPE: "none"`) { + t.Error("Expected GH_AW_SKIP_SCOPE environment variable set to none") + } + + // Verify scope is commented out in frontmatter + if !strings.Contains(lockContentStr, "# scope:") { + t.Error("Expected scope to be commented out in lock file") + } + }) + + t.Run("skip_if_no_match_with_github_token", func(t *testing.T) { + workflowContent := `--- +on: + schedule: + - cron: "*/15 * * * *" + skip-if-no-match: + query: "org:myorg label:agent-fix is:issue is:open" + scope: none + github-token: ${{ secrets.CROSS_ORG_TOKEN }} +engine: claude +--- + +# Skip If No Match With Custom Token + +This workflow uses a custom token for org-wide search. +` + workflowFile := filepath.Join(tmpDir, "skip-no-match-github-token-workflow.md") + if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil { + t.Fatal(err) + } + + err := compiler.CompileWorkflow(workflowFile) + if err != nil { + t.Fatalf("Compilation failed: %v", err) + } + + lockFile := stringutil.MarkdownToLockFile(workflowFile) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + lockContentStr := string(lockContent) + + // Verify skip-if-no-match check is present + if !strings.Contains(lockContentStr, "Check skip-if-no-match query") { + t.Error("Expected skip-if-no-match check to be present") + } + + // Verify the custom github-token is passed via with.github-token + if !strings.Contains(lockContentStr, "github-token: ${{ secrets.CROSS_ORG_TOKEN }}") { + t.Error("Expected github-token to be set in with section for skip-if-no-match step") + } + + // Verify GH_AW_SKIP_SCOPE is set to "none" + if !strings.Contains(lockContentStr, `GH_AW_SKIP_SCOPE: "none"`) { + t.Error("Expected GH_AW_SKIP_SCOPE environment variable set to none") + } + }) + + t.Run("skip_if_no_match_with_github_app", func(t *testing.T) { + workflowContent := `--- +on: + schedule: + - cron: "*/15 * * * *" + skip-if-no-match: + query: "org:myorg label:agent-fix is:issue is:open" + scope: none + github-app: + app-id: ${{ secrets.WORKFLOW_APP_ID }} + private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} + owner: myorg +engine: claude +--- + +# Skip If No Match With GitHub App + +This workflow uses a GitHub App token for org-wide search. +` + workflowFile := filepath.Join(tmpDir, "skip-no-match-github-app-workflow.md") + if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil { + t.Fatal(err) + } + + err := compiler.CompileWorkflow(workflowFile) + if err != nil { + t.Fatalf("Compilation failed: %v", err) + } + + lockFile := stringutil.MarkdownToLockFile(workflowFile) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + lockContentStr := string(lockContent) + + // Verify the GitHub App token mint step is generated before the skip check + if !strings.Contains(lockContentStr, "Generate GitHub App token for skip-if check") { + t.Error("Expected GitHub App token mint step to be present") + } + + // Verify app-id and private-key are in the mint step + if !strings.Contains(lockContentStr, "app-id: ${{ secrets.WORKFLOW_APP_ID }}") { + t.Error("Expected app-id in the GitHub App token mint step") + } + if !strings.Contains(lockContentStr, "private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }}") { + t.Error("Expected private-key in the GitHub App token mint step") + } + + // Verify owner is passed + if !strings.Contains(lockContentStr, "owner: myorg") { + t.Error("Expected owner to be set in GitHub App token mint step") + } + + // Verify the minted token is used in the skip-if step + if !strings.Contains(lockContentStr, "github-token: ${{ steps.check_skip_if_no_match-app-token.outputs.token }}") { + t.Error("Expected minted app token to be used in skip-if-no-match step") + } + + // Verify GH_AW_SKIP_SCOPE is set to "none" + if !strings.Contains(lockContentStr, `GH_AW_SKIP_SCOPE: "none"`) { + t.Error("Expected GH_AW_SKIP_SCOPE environment variable set to none") + } + }) } diff --git a/pkg/workflow/stop_after.go b/pkg/workflow/stop_after.go index 0f781beb50d..62328996b05 100644 --- a/pkg/workflow/stop_after.go +++ b/pkg/workflow/stop_after.go @@ -254,9 +254,18 @@ func (c *Compiler) extractSkipIfMatchFromOn(frontmatter map[string]any, workflow } } + // Extract scope, github-token, and github-app (optional auth/scope overrides) + scope, githubToken, githubApp, err := extractSkipIfAuthConfig(skip, "skip-if-match") + if err != nil { + return nil, err + } + return &SkipIfMatchConfig{ - Query: queryStr, - Max: maxVal, + Query: queryStr, + Max: maxVal, + Scope: scope, + GitHubToken: githubToken, + GitHubApp: githubApp, }, nil default: return nil, fmt.Errorf("skip-if-match value must be a string or object, got %T. Examples:\n skip-if-match: \"is:issue is:open\"\n skip-if-match:\n query: \"is:pr is:open\"\n max: 3", skipIfMatch) @@ -333,9 +342,18 @@ func (c *Compiler) extractSkipIfNoMatchFromOn(frontmatter map[string]any, workfl } } + // Extract scope, github-token, and github-app (optional auth/scope overrides) + scope, githubToken, githubApp, err := extractSkipIfAuthConfig(skip, "skip-if-no-match") + if err != nil { + return nil, err + } + return &SkipIfNoMatchConfig{ - Query: queryStr, - Min: minVal, + Query: queryStr, + Min: minVal, + Scope: scope, + GitHubToken: githubToken, + GitHubApp: githubApp, }, nil default: return nil, fmt.Errorf("skip-if-no-match value must be a string or object, got %T. Examples:\n skip-if-no-match: \"is:pr is:open\"\n skip-if-no-match:\n query: \"is:pr is:open\"\n min: 3", skipIfNoMatch) @@ -386,3 +404,47 @@ func (c *Compiler) processSkipIfNoMatchConfiguration(frontmatter map[string]any, return nil } + +// extractSkipIfAuthConfig extracts the optional scope, github-token, and github-app fields +// shared by both skip-if-match and skip-if-no-match object configurations. +// conditionName is used only for error messages (e.g. "skip-if-match"). +func extractSkipIfAuthConfig(skip map[string]any, conditionName string) (scope string, githubToken string, githubApp *GitHubAppConfig, err error) { + // Extract scope value (optional) + if scopeRaw, hasScope := skip["scope"]; hasScope { + scopeStr, ok := scopeRaw.(string) + if !ok { + return "", "", nil, fmt.Errorf("%s 'scope' field must be a string, got %T. Example: scope: none", conditionName, scopeRaw) + } + if scopeStr != "none" { + return "", "", nil, fmt.Errorf("%s 'scope' field must be \"none\" or omitted, got %q", conditionName, scopeStr) + } + scope = scopeStr + } + + // Extract github-token value (optional) + if tokenRaw, hasToken := skip["github-token"]; hasToken { + tokenStr, ok := tokenRaw.(string) + if !ok { + return "", "", nil, fmt.Errorf("%s 'github-token' field must be a string, got %T", conditionName, tokenRaw) + } + githubToken = tokenStr + } + + // Extract github-app value (optional) + if appRaw, hasApp := skip["github-app"]; hasApp { + appMap, ok := appRaw.(map[string]any) + if !ok { + return "", "", nil, fmt.Errorf("%s 'github-app' field must be an object, got %T", conditionName, appRaw) + } + githubApp = parseAppConfig(appMap) + if githubApp.AppID == "" || githubApp.PrivateKey == "" { + return "", "", nil, fmt.Errorf("%s 'github-app' requires both 'app-id' and 'private-key' fields", conditionName) + } + } + + if githubToken != "" && githubApp != nil { + return "", "", nil, fmt.Errorf("%s 'github-token' and 'github-app' cannot both be set. Use one authentication method", conditionName) + } + + return scope, githubToken, githubApp, nil +} From b496627d458ba5b094c9665c7e892bea5897155c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:53:23 +0000 Subject: [PATCH 3/8] docs: update frontmatter-full, triggers, and frontmatter docs for skip-if scope/auth fields Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../docs/reference/frontmatter-full.md | 448 ++++++++++++++++-- .../src/content/docs/reference/frontmatter.md | 2 + docs/src/content/docs/reference/triggers.md | 40 ++ 3 files changed, 439 insertions(+), 51 deletions(-) diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 7228eb74452..f1edc143c0a 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -64,6 +64,16 @@ metadata: # (optional) imports: [] +# Optional list of additional workflow or action files that should be fetched +# alongside this workflow when running 'gh aw add'. Entries are relative paths +# (from the same directory as this workflow in the source repository) to agentic +# workflow .md files or GitHub Actions .yml/.yaml files. GitHub Actions expression +# syntax (${{) is not allowed in resource paths. +# (optional) +resources: [] + # Array of Relative path to a workflow .md file or action .yml/.yaml file. Must be + # a static path; GitHub Actions expression syntax (${{) is not allowed. + # If true, inline all imports (including those without inputs) at compilation time # in the generated lock.yml instead of using runtime-import macros. When enabled, # the frontmatter hash covers the entire markdown body so any change to the @@ -547,8 +557,8 @@ on: stop-after: "example-value" # Conditionally skip workflow execution when a GitHub search query has matches. - # Can be a string (query only, implies max=1) or an object with 'query' and - # optional 'max' fields. + # Can be a string (query only, implies max=1) or an object with 'query', optional + # 'max', 'scope', 'github-token', and 'github-app' fields. # (optional) # This field supports multiple formats (oneOf): @@ -558,7 +568,8 @@ on: # label:bug' skip-if-match: "example-value" - # Option 2: Skip-if-match configuration object with query and maximum match count + # Option 2: Skip-if-match configuration object with query, maximum match count, + # and optional scope/auth settings skip-if-match: # GitHub search query string to check before running workflow. Query is # automatically scoped to the current repository. @@ -576,9 +587,39 @@ on: # Option 2: GitHub Actions expression that resolves to an integer at runtime max: "example-value" + # Scope for the search query. Set to 'none' to disable the automatic + # 'repo:owner/repo' scoping, enabling org-wide or cross-repo queries. + # (optional) + scope: "none" + + # Custom GitHub token to use for the search API call. Useful for cross-repo or + # org-wide searches that require additional permissions. + # (optional) + github-token: "${{ secrets.GITHUB_TOKEN }}" + + # GitHub App configuration for minting a token used for the search API call. + # (optional) + github-app: + # GitHub App ID (e.g., '${{ secrets.APP_ID }}'). Required. + app-id: "example-value" + + # GitHub App private key (e.g., '${{ secrets.APP_PRIVATE_KEY }}'). Required. + private-key: "example-value" + + # Optional owner of the GitHub App installation (defaults to current repository + # owner). + # (optional) + owner: "example-value" + + # Optional list of repositories to grant access to. Use ['*'] for org-wide access. + # (optional) + repositories: [] + # Array of strings + # Conditionally skip workflow execution when a GitHub search query has no matches # (or fewer than minimum). Can be a string (query only, implies min=1) or an - # object with 'query' and optional 'min' fields. + # object with 'query', optional 'min', 'scope', 'github-token', and 'github-app' + # fields. # (optional) # This field supports multiple formats (oneOf): @@ -588,8 +629,8 @@ on: # label:ready-to-deploy' skip-if-no-match: "example-value" - # Option 2: Skip-if-no-match configuration object with query and minimum match - # count + # Option 2: Skip-if-no-match configuration object with query, minimum match count, + # and optional scope/auth settings skip-if-no-match: # GitHub search query string to check before running workflow. Query is # automatically scoped to the current repository. @@ -600,6 +641,35 @@ on: # (optional) min: 1 + # Scope for the search query. Set to 'none' to disable the automatic + # 'repo:owner/repo' scoping, enabling org-wide or cross-repo queries. + # (optional) + scope: "none" + + # Custom GitHub token to use for the search API call. Useful for cross-repo or + # org-wide searches that require additional permissions. + # (optional) + github-token: "${{ secrets.GITHUB_TOKEN }}" + + # GitHub App configuration for minting a token used for the search API call. + # (optional) + github-app: + # GitHub App ID (e.g., '${{ secrets.APP_ID }}'). Required. + app-id: "example-value" + + # GitHub App private key (e.g., '${{ secrets.APP_PRIVATE_KEY }}'). Required. + private-key: "example-value" + + # Optional owner of the GitHub App installation (defaults to current repository + # owner). + # (optional) + owner: "example-value" + + # Optional list of repositories to grant access to. Use ['*'] for org-wide access. + # (optional) + repositories: [] + # Array of strings + # Skip workflow execution for users with specific repository roles. Useful for # workflows that should only run for external contributors or specific permission # levels. @@ -895,6 +965,7 @@ concurrency: # Concurrency group name. Workflows in the same group cannot run simultaneously. # Supports GitHub Actions expressions for dynamic group names based on branch, # workflow, or other context. + # (optional) group: "example-value" # Whether to cancel in-progress workflows in the same concurrency group when a new @@ -905,13 +976,14 @@ concurrency: cancel-in-progress: true # Additional discriminator expression appended to compiler-generated job-level - # concurrency groups (agent, output jobs). Use this in fan-out patterns where - # multiple workflow instances are dispatched concurrently with different inputs, - # to prevent job-level concurrency groups from colliding and causing cancellations. - # Supports GitHub Actions expressions. Stripped from the compiled lock file - # (gh-aw extension, not a GitHub Actions field). + # concurrency groups (agent, output jobs). Use this when multiple workflow + # instances are dispatched concurrently with different inputs (fan-out pattern) to + # prevent job-level concurrency groups from colliding. For example, '${{ + # inputs.finding_id }}' ensures each dispatched run gets a unique job-level group. + # Supports GitHub Actions expressions. This field is stripped from the compiled + # lock file (it is a gh-aw extension, not a GitHub Actions field). # (optional) - job-discriminator: "${{ inputs.finding_id }}" + job-discriminator: "example-value" # Environment variables for the workflow # (optional) @@ -1306,16 +1378,16 @@ post-steps: [] # (optional) # This field supports multiple formats (oneOf): -# Option 1: Simple engine name: 'claude' (default, Claude Code), 'copilot' (GitHub -# Copilot CLI), 'codex' (OpenAI Codex CLI), or 'gemini' (Google Gemini CLI) -engine: "claude" +# Option 1: Engine name: built-in ('claude', 'codex', 'copilot', 'gemini') or a +# named catalog entry +engine: "example-value" # Option 2: Extended engine configuration object with advanced options for model # selection, turn limiting, environment variables, and custom steps engine: - # AI engine identifier: 'claude' (Claude Code), 'codex' (OpenAI Codex CLI), - # 'copilot' (GitHub Copilot CLI), or 'gemini' (Google Gemini CLI) - id: "claude" + # AI engine identifier: built-in ('claude', 'codex', 'copilot', 'gemini') or a + # named catalog entry + id: "example-value" # Optional version of the AI engine action (e.g., 'beta', 'stable', 20). Has # sensible defaults and can typically be omitted. Numeric values are automatically @@ -1422,9 +1494,9 @@ engine: agent: "example-value" # Custom API endpoint hostname for the agentic engine. Used for GitHub Enterprise - # Cloud (GHEC), GitHub Enterprise Server (GHES), or custom AI endpoints. - # Accepts a hostname only (no protocol or path). - # Examples: "api.acme.ghe.com" (GHEC), "api.enterprise.githubcopilot.com" (GHES) + # Cloud (GHEC), GitHub Enterprise Server (GHES), or custom AI endpoints. Example: + # 'api.acme.ghe.com' for GHEC, 'api.enterprise.githubcopilot.com' for GHES, or + # custom endpoint hostnames. # (optional) api-target: "example-value" @@ -1434,6 +1506,186 @@ engine: args: [] # Array of strings +# Option 3: Inline engine definition: specifies a runtime adapter and optional +# provider settings directly in the workflow frontmatter, without requiring a +# named catalog entry +engine: + # Runtime adapter reference for the inline engine definition + runtime: + # Runtime adapter identifier (e.g. 'codex', 'claude', 'copilot', 'gemini') + id: "example-value" + + # Optional version of the runtime adapter (e.g. '0.105.0', 'beta') + # (optional) + version: null + + # Optional provider configuration for the inline engine definition + # (optional) + provider: + # Provider identifier (e.g. 'openai', 'anthropic', 'github', 'google') + # (optional) + id: "example-value" + + # Optional specific LLM model to use (e.g. 'gpt-5', 'claude-3-5-sonnet-20241022') + # (optional) + model: "example-value" + + # Authentication configuration for the provider + # (optional) + auth: + # Name of the GitHub Actions secret that contains the API key for this provider + # (optional) + secret: "example-value" + + # Authentication strategy for the provider (default: api-key when secret is set) + # (optional) + strategy: "api-key" + + # OAuth 2.0 token endpoint URL. Required when strategy is + # 'oauth-client-credentials'. + # (optional) + token-url: "example-value" + + # GitHub Actions secret name that holds the OAuth client ID. Required when + # strategy is 'oauth-client-credentials'. + # (optional) + client-id: "example-value" + + # GitHub Actions secret name that holds the OAuth client secret. Required when + # strategy is 'oauth-client-credentials'. + # (optional) + client-secret: "example-value" + + # JSON field name in the token response that contains the access token. Defaults + # to 'access_token'. + # (optional) + token-field: "example-value" + + # HTTP header name to inject the API key or token into (e.g. 'api-key', + # 'x-api-key'). Required when strategy is not 'bearer'. + # (optional) + header-name: "example-value" + + # Request shaping configuration for non-standard provider URL and body + # transformations + # (optional) + request: + # URL path template with {model} and other variable placeholders (e.g. + # '/openai/deployments/{model}/chat/completions') + # (optional) + path-template: "example-value" + + # Static or template query-parameter values appended to every request + # (optional) + query: + {} + + # Key/value pairs injected into the JSON request body before sending + # (optional) + body-inject: + {} + +# Option 4: Engine definition: full declarative metadata for a named engine entry +# (used in builtin engine shared workflow files such as @builtin:engines/*.md) +engine: + # Unique engine identifier (e.g. 'copilot', 'claude', 'codex', 'gemini') + id: "example-value" + + # Human-readable display name for the engine + display-name: "example-value" + + # Human-readable description of the engine + # (optional) + description: "Description of the workflow" + + # Runtime adapter identifier. Maps to the CodingAgentEngine registered in the + # engine registry. Defaults to id when omitted. + # (optional) + runtime-id: "example-value" + + # Provider metadata for the engine + # (optional) + provider: + # Provider name (e.g. 'anthropic', 'github', 'google', 'openai') + # (optional) + name: "My Workflow" + + # Default authentication configuration for the provider + # (optional) + auth: + # Name of the GitHub Actions secret that contains the API key + # (optional) + secret: "example-value" + + # Authentication strategy + # (optional) + strategy: "api-key" + + # OAuth 2.0 token endpoint URL + # (optional) + token-url: "example-value" + + # GitHub Actions secret name for the OAuth client ID + # (optional) + client-id: "example-value" + + # GitHub Actions secret name for the OAuth client secret + # (optional) + client-secret: "example-value" + + # JSON field name in the token response containing the access token + # (optional) + token-field: "example-value" + + # HTTP header name to inject the API key or token into + # (optional) + header-name: "example-value" + + # Request shaping configuration + # (optional) + request: + # URL path template with variable placeholders + # (optional) + path-template: "example-value" + + # Static query parameters + # (optional) + query: + {} + + # Key/value pairs injected into the JSON request body + # (optional) + body-inject: + {} + + # Model selection configuration for the engine + # (optional) + models: + # Default model identifier + # (optional) + default: "example-value" + + # List of supported model identifiers + # (optional) + supported: [] + # Array of strings + + # Authentication bindings — maps logical roles (e.g. 'api-key') to GitHub Actions + # secret names + # (optional) + auth: [] + # Array items: + # Logical authentication role (e.g. 'api-key', 'token') + role: "example-value" + + # Name of the GitHub Actions secret that provides credentials for this role + secret: "example-value" + + # Additional engine-specific options + # (optional) + options: + {} + # MCP server definitions # (optional) mcp-servers: @@ -1512,22 +1764,26 @@ tools: # Array of Mount specification in format 'host:container:mode' # Guard policy: repository access configuration. Restricts which repositories the - # agent can access. Use 'all' to allow all repos or an array of 'owner/repo' - # strings. + # agent can access. Use 'all' to allow all repos, 'public' for public repositories + # only, or an array of repository patterns (e.g., 'owner/repo', 'owner/*', + # 'owner/prefix*'). # (optional) # This field supports multiple formats (oneOf): - # Option 1: Allow access to all repositories + # Option 1: Allow access to all repositories ('all') or only public repositories + # ('public') repos: "all" - # Option 2: Allow access to specific repositories + # Option 2: Allow access to specific repositories using patterns (e.g., + # 'owner/repo', 'owner/*', 'owner/prefix*') repos: [] - # Array items: Repository slug in the format 'owner/repo' + # Array items: Repository pattern in the format 'owner/repo', 'owner/*' (all repos + # under owner), or 'owner/prefix*' (repos with name prefix) # Guard policy: minimum required integrity level for repository access. Restricts # the agent to users with at least the specified permission level. # (optional) - min-integrity: "unapproved" + min-integrity: "none" # GitHub App configuration for token minting. When configured, a GitHub App # installation access token is minted at workflow start and used instead of the @@ -1873,6 +2129,11 @@ tools: # (optional) max-file-count: 1 + # Maximum total patch size in bytes (default: 10240 = 10KB, max: 102400 = 100KB). + # The total size of the git diff must not exceed this value. + # (optional) + max-patch-size: 1 + # Optional description for the memory that will be shown in the agent prompt # (optional) description: "Description of the workflow" @@ -1881,11 +2142,11 @@ tools: # (optional) create-orphan: true - # Use the GitHub Wiki git repository instead of the regular repository. When enabled, - # files are stored in and read from the wiki, and the agent will be instructed to - # follow GitHub Wiki markdown syntax (default: false) + # Use the GitHub Wiki git repository instead of the regular repository. When + # enabled, files are stored in and read from the wiki, and the agent will be + # instructed to follow GitHub Wiki markdown syntax (default: false) # (optional) - wiki: false + wiki: true # List of allowed file extensions (e.g., [".json", ".txt"]). Default: [".json", # ".jsonl", ".txt", ".md", ".csv"] @@ -2964,19 +3225,23 @@ safe-outputs: # (optional) github-token-for-extra-empty-commit: "example-value" - # Controls protected-file protection policy for this safe output. blocked - # (default): hard-block any patch that modifies package manifests (e.g. - # package.json, go.mod), engine instruction files (e.g. AGENTS.md, CLAUDE.md) or - # .github/ files. allowed: allow all changes. fallback-to-issue: push the branch - # but create a review issue instead of a PR so a human can review before merging. + # Controls protected-file protection. blocked (default): hard-block any patch that + # modifies package manifests (e.g. package.json, go.mod), engine instruction files + # (e.g. AGENTS.md, CLAUDE.md) or .github/ files. allowed: allow all changes. + # fallback-to-issue: push the branch but create a review issue instead of a PR, so + # a human can review the manifest changes before merging. # (optional) protected-files: "blocked" - # List of glob patterns for files the workflow is allowed to modify. Acts as a - # strict allowlist: every file in the patch must match at least one pattern. Runs - # independently of protected-files; both checks must pass. To modify a protected - # file it must both match allowed-files and have protected-files set to 'allowed'. - # Supports * (any characters except /) and ** (any characters including /). + # Exclusive allowlist of glob patterns. When set, every file in the patch must + # match at least one pattern — files outside the list are always refused, + # including normal source files. This is a restriction, not an exception: setting + # allowed-files: [".github/workflows/*"] blocks all other files. To allow multiple + # sets of files, list all patterns explicitly. Acts independently of the + # protected-files policy; both checks must pass. To modify a protected file, it + # must both match allowed-files and be permitted by protected-files (e.g. + # protected-files: allowed). Supports * (any characters except /) and ** (any + # characters including /). # (optional) allowed-files: [] # Array of strings @@ -3077,6 +3342,19 @@ safe-outputs: # (optional) target: "example-value" + # Target repository in format 'owner/repo' for cross-repository PR review + # submission. Takes precedence over trial target repo settings. + # (optional) + target-repo: "example-value" + + # List of additional repositories in format 'owner/repo' that PR reviews can be + # submitted in. When specified, the agent can use a 'repo' field in the output to + # specify which repository to submit the review in. The target repository (current + # or target-repo) is always implicitly allowed. + # (optional) + allowed-repos: [] + # Array of strings + # GitHub token to use for this specific output type. Overrides global github-token # if specified. # (optional) @@ -3898,19 +4176,23 @@ safe-outputs: allowed-repos: [] # Array of strings - # Controls protected-file protection policy for this safe output. blocked - # (default): hard-block any patch that modifies package manifests (e.g. - # package.json, go.mod), engine instruction files (e.g. AGENTS.md, CLAUDE.md) or - # .github/ files. allowed: allow all changes. fallback-to-issue: create a review - # issue instead of pushing so a human can review before applying the changes. + # Controls protected-file protection. blocked (default): hard-block any patch that + # modifies package manifests (e.g. package.json, go.mod), engine instruction files + # (e.g. AGENTS.md, CLAUDE.md) or .github/ files. allowed: allow all changes. + # fallback-to-issue: create a review issue instead of pushing to the PR branch, so + # a human can review the changes before applying. # (optional) protected-files: "blocked" - # List of glob patterns for files the workflow is allowed to modify. Acts as a - # strict allowlist: every file in the patch must match at least one pattern. Runs - # independently of protected-files; both checks must pass. To modify a protected - # file it must both match allowed-files and have protected-files set to 'allowed'. - # Supports * (any characters except /) and ** (any characters including /). + # Exclusive allowlist of glob patterns. When set, every file in the patch must + # match at least one pattern — files outside the list are always refused, + # including normal source files. This is a restriction, not an exception: setting + # allowed-files: [".github/workflows/*"] blocks all other files. To allow multiple + # sets of files, list all patterns explicitly. Acts independently of the + # protected-files policy; both checks must pass. To modify a protected file, it + # must both match allowed-files and be permitted by protected-files (e.g. + # protected-files: allowed). Supports * (any characters except /) and ** (any + # characters including /). # (optional) allowed-files: [] # Array of strings @@ -4035,6 +4317,18 @@ safe-outputs: # (optional) github-token: "${{ secrets.GITHUB_TOKEN }}" + # Target repository in format 'owner/repo' for cross-repository workflow dispatch. + # When specified, the workflow will be dispatched to the target repository instead + # of the current one. + # (optional) + target-repo: "example-value" + + # Git ref (branch, tag, or SHA) to use when dispatching the workflow. For + # workflow_call relay scenarios this is auto-injected by the compiler from + # needs.activation.outputs.target_ref. Overrides the caller's GITHUB_REF. + # (optional) + target-ref: "example-value" + # Option 2: Shorthand array format: list of workflow names (without .md extension) # to allow dispatching dispatch-workflow: [] @@ -4489,6 +4783,18 @@ safe-outputs: # (optional) group-reports: true + # When false, disables creating failure tracking issues when workflows fail. + # Useful for workflows where failures are expected or handled elsewhere. Defaults + # to true. + # (optional) + report-failure-as-issue: true + + # Repository to create failure tracking issues in, in the format 'owner/repo'. + # Useful when the current repository has issues disabled. Defaults to the current + # repository. + # (optional) + failure-issue-repo: "example-value" + # Maximum number of bot trigger references (e.g. 'fixes #123', 'closes #456') # allowed in output before all of them are neutralized. Default: 10. Supports # integer or GitHub Actions expression (e.g. '${{ inputs.max-bot-mentions }}'). @@ -4518,6 +4824,25 @@ safe-outputs: # (optional) concurrency-group: "example-value" + # Override the GitHub deployment environment for the safe-outputs job. When set, + # this environment is used instead of the top-level environment: field. When not + # set, the top-level environment: field is propagated automatically so that + # environment-scoped secrets are accessible in the safe-outputs job. + # (optional) + # This field supports multiple formats (oneOf): + + # Option 1: Environment name as a string + environment: "example-value" + + # Option 2: Environment object with name and optional URL + environment: + # The name of the environment configured in the repo + name: "My Workflow" + + # A deployment URL + # (optional) + url: "example-value" + # Runner specification for all safe-outputs jobs (activation, create-issue, # add-comment, etc.). Single runner label (e.g., 'ubuntu-slim', 'ubuntu-latest', # 'windows-latest', 'self-hosted'). Defaults to 'ubuntu-slim'. See @@ -4630,6 +4955,27 @@ runtimes: # Option 2: Multiple checkout configurations checkout: [] # Array items: undefined + +# APM package references to install. Supports array format (list of package slugs) +# or object format with packages and isolated fields. +# (optional) +# This field supports multiple formats (oneOf): + +# Option 1: Simple array of APM package references. +dependencies: [] + # Array items: APM package reference in the format 'org/repo' or + # 'org/repo/path/to/skill' + +# Option 2: Object format with packages and optional isolated flag. +dependencies: + # List of APM package references to install. + packages: [] + # Array of APM package reference in the format 'org/repo' or + # 'org/repo/path/to/skill' + + # If true, agent restore step clears primitive dirs before unpacking. + # (optional) + isolated: true --- ``` diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index 5f2987e2d0e..eea336cbd3c 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -35,6 +35,8 @@ The `on:` section uses standard GitHub Actions syntax to define workflow trigger - `forks:` - Configure fork filtering for pull_request triggers - `skip-roles:` - Skip workflow execution for specific repository roles - `skip-bots:` - Skip workflow execution for specific GitHub actors +- `skip-if-match:` - Skip execution when a search query has matches (supports `scope`, `github-token`, `github-app`) +- `skip-if-no-match:` - Skip execution when a search query has no matches (supports `scope`, `github-token`, `github-app`) - `github-token:` - Custom token for activation job reactions and status comments - `github-app:` - GitHub App for minting a short-lived token used by the activation job diff --git a/docs/src/content/docs/reference/triggers.md b/docs/src/content/docs/reference/triggers.md index b6450f9b201..3b0fbb03551 100644 --- a/docs/src/content/docs/reference/triggers.md +++ b/docs/src/content/docs/reference/triggers.md @@ -390,6 +390,31 @@ on: weekly on monday A pre-activation check runs the search query against the current repository. If matches reach or exceed the threshold (default `max: 1`), the workflow is skipped. The query is automatically scoped to the current repository and supports all standard GitHub search qualifiers (`is:`, `label:`, `in:title`, `author:`, etc.). +#### Cross-Repo and Org-Wide Queries + +By default the query is scoped to the current repository. Use `scope: none` to disable this and search across an entire org. Combine with `github-token` or `github-app` to provide a token with the required permissions: + +```yaml wrap +on: + schedule: + - cron: "*/15 * * * *" + skip-if-match: + query: "org:myorg label:ops:in-progress is:issue is:open" + scope: none + github-app: + app-id: ${{ secrets.WORKFLOW_APP_ID }} + private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} + owner: myorg +``` + +| Field | Description | +|-------|-------------| +| `scope: none` | Disables the automatic `repo:owner/repo` qualifier | +| `github-token` | Custom PAT or token for the search step (e.g. `${{ secrets.CROSS_ORG_TOKEN }}`) | +| `github-app` | Mints a short-lived installation token; requires `app-id` and `private-key` | + +`github-token` and `github-app` are mutually exclusive. String shorthand always uses the default `GITHUB_TOKEN` scoped to the current repository. + ### Skip-If-No-Match Condition (`skip-if-no-match:`) Conditionally skip workflow execution when a GitHub search query has **no matches** (or fewer than the minimum required). This is the opposite of `skip-if-match`. @@ -409,6 +434,21 @@ on: A pre-activation check runs the search query against the current repository. If matches are below the threshold (default `min: 1`), the workflow is skipped. Can be combined with `skip-if-match` for complex conditions. +The same `scope`, `github-token`, and `github-app` fields available on `skip-if-match` work identically here: + +```yaml wrap +on: + schedule: + - cron: "*/15 * * * *" + skip-if-no-match: + query: "org:myorg label:agent-fix -label:ops:agentic is:issue is:open" + scope: none + github-app: + app-id: ${{ secrets.WORKFLOW_APP_ID }} + private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} + owner: myorg +``` + ## Trigger Shorthands Instead of writing full YAML trigger configurations, you can use natural-language shorthand strings with `on:`. The compiler expands these into standard GitHub Actions trigger syntax and automatically includes `workflow_dispatch` so the workflow can also be run manually. From abcdf1c4bd66b0d784a05f33c84d2df9e5263633 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:00:06 +0000 Subject: [PATCH 4/8] refactor: extract buildSearchQuery helper from duplicate skip-if scope logic Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/check_skip_if_helpers.cjs | 21 +++++++++++++++++++++ actions/setup/js/check_skip_if_match.cjs | 12 ++---------- actions/setup/js/check_skip_if_no_match.cjs | 12 ++---------- 3 files changed, 25 insertions(+), 20 deletions(-) create mode 100644 actions/setup/js/check_skip_if_helpers.cjs diff --git a/actions/setup/js/check_skip_if_helpers.cjs b/actions/setup/js/check_skip_if_helpers.cjs new file mode 100644 index 00000000000..1ede805da65 --- /dev/null +++ b/actions/setup/js/check_skip_if_helpers.cjs @@ -0,0 +1,21 @@ +// @ts-check +/// + +/** + * Builds the GitHub search query, optionally scoping it to the current repository. + * @param {string} skipQuery - The base query string + * @param {string|undefined} skipScope - The scope setting ('none' to disable repo scoping) + * @returns {string} The final search query + */ +function buildSearchQuery(skipQuery, skipScope) { + if (skipScope === "none") { + core.info(`Using raw query (scope: none): ${skipQuery}`); + return skipQuery; + } + const { owner, repo } = context.repo; + const searchQuery = `${skipQuery} repo:${owner}/${repo}`; + core.info(`Scoped query: ${searchQuery}`); + return searchQuery; +} + +module.exports = { buildSearchQuery }; diff --git a/actions/setup/js/check_skip_if_match.cjs b/actions/setup/js/check_skip_if_match.cjs index 522504add2d..6385262ac56 100644 --- a/actions/setup/js/check_skip_if_match.cjs +++ b/actions/setup/js/check_skip_if_match.cjs @@ -3,6 +3,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_API, ERR_CONFIG } = require("./error_codes.cjs"); +const { buildSearchQuery } = require("./check_skip_if_helpers.cjs"); async function main() { const skipQuery = process.env.GH_AW_SKIP_QUERY; @@ -29,16 +30,7 @@ async function main() { core.info(`Checking skip-if-match query: ${skipQuery}`); core.info(`Maximum matches threshold: ${maxMatches}`); - let searchQuery; - if (skipScope === "none") { - // Use query as-is without appending repo:owner/repo scoping - searchQuery = skipQuery; - core.info(`Using raw query (scope: none): ${searchQuery}`); - } else { - const { owner, repo } = context.repo; - searchQuery = `${skipQuery} repo:${owner}/${repo}`; - core.info(`Scoped query: ${searchQuery}`); - } + const searchQuery = buildSearchQuery(skipQuery, skipScope); try { const response = await github.rest.search.issuesAndPullRequests({ diff --git a/actions/setup/js/check_skip_if_no_match.cjs b/actions/setup/js/check_skip_if_no_match.cjs index f1935d7ccdb..8d089068322 100644 --- a/actions/setup/js/check_skip_if_no_match.cjs +++ b/actions/setup/js/check_skip_if_no_match.cjs @@ -3,6 +3,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_API, ERR_CONFIG } = require("./error_codes.cjs"); +const { buildSearchQuery } = require("./check_skip_if_helpers.cjs"); async function main() { const { GH_AW_SKIP_QUERY: skipQuery, GH_AW_WORKFLOW_NAME: workflowName, GH_AW_SKIP_MIN_MATCHES: minMatchesStr = "1", GH_AW_SKIP_SCOPE: skipScope } = process.env; @@ -26,16 +27,7 @@ async function main() { core.info(`Checking skip-if-no-match query: ${skipQuery}`); core.info(`Minimum matches threshold: ${minMatches}`); - let searchQuery; - if (skipScope === "none") { - // Use query as-is without appending repo:owner/repo scoping - searchQuery = skipQuery; - core.info(`Using raw query (scope: none): ${searchQuery}`); - } else { - const { owner, repo } = context.repo; - searchQuery = `${skipQuery} repo:${owner}/${repo}`; - core.info(`Scoped query: ${searchQuery}`); - } + const searchQuery = buildSearchQuery(skipQuery, skipScope); try { const { From fc6f15e46dc46e0c37305f0b1d7a7592eda8f5d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:18:46 +0000 Subject: [PATCH 5/8] plan: move github-app/github-token to top-level on: for skip-if checks Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/daily-doc-healer.lock.yml | 6 +++--- .github/workflows/daily-doc-updater.lock.yml | 6 +++--- .github/workflows/developer-docs-consolidator.lock.yml | 6 +++--- .github/workflows/dictation-prompt.lock.yml | 6 +++--- .github/workflows/glossary-maintainer.lock.yml | 6 +++--- .github/workflows/technical-doc-writer.lock.yml | 6 +++--- .github/workflows/unbloat-docs.lock.yml | 6 +++--- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/daily-doc-healer.lock.yml b/.github/workflows/daily-doc-healer.lock.yml index dae2bd54385..a394c9fd58e 100644 --- a/.github/workflows/daily-doc-healer.lock.yml +++ b/.github/workflows/daily-doc-healer.lock.yml @@ -28,7 +28,7 @@ # - shared/mcp/qmd-docs.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"e4f96b1dbfc72616f65e3b3e6640e1d4b9ca66d10d1bdf130ed7c6f832ae1a60","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"3281ba1de9c3a7453fd4e74eb14dcbf06ea83c84ac6709d0dfe024f818dd4a3d","strict":true} name: "Daily Documentation Healer" "on": @@ -309,10 +309,10 @@ jobs: uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} - path: /tmp/gh-aw/cache/qmd-docs + path: ~/.cache/qmd restore-keys: qmd-docs- - name: Start QMD MCP server - run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/ /tmp/gh-aw/cache/qmd-docs ~/.cache\nln -sfn /tmp/gh-aw/cache/qmd-docs ~/.cache/qmd\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" + run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" # Cache memory file share configuration from frontmatter processed below - name: Create cache-memory directory diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index 552b9dc4054..bf219fdb3dc 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -27,7 +27,7 @@ # Imports: # - shared/mcp/qmd-docs.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"fe8a240ea8336e8b642577dc172dd573a6c425784419632463308d357be0b753","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"029b4f22798fe4cdef702904b87b929499b2e7faab3fbd4c75fc2d2aa6905d03","strict":true} name: "Daily Documentation Updater" "on": @@ -305,10 +305,10 @@ jobs: uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} - path: /tmp/gh-aw/cache/qmd-docs + path: ~/.cache/qmd restore-keys: qmd-docs- - name: Start QMD MCP server - run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/ /tmp/gh-aw/cache/qmd-docs ~/.cache\nln -sfn /tmp/gh-aw/cache/qmd-docs ~/.cache/qmd\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" + run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" # Cache memory file share configuration from frontmatter processed below - name: Create cache-memory directory diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index 2b964fdcc91..e17d1f2d752 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -29,7 +29,7 @@ # - shared/mcp/serena-go.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"20c36f8e327a140ac929db97c45391e69994645cb56a02951bbaf6c4575b8f81","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"04ab9b346523011c327645a8bfdd1d836c37666cd30bb4509fc9a1973c797275","strict":true} name: "Developer Documentation Consolidator" "on": @@ -328,10 +328,10 @@ jobs: uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} - path: /tmp/gh-aw/cache/qmd-docs + path: ~/.cache/qmd restore-keys: qmd-docs- - name: Start QMD MCP server - run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/ /tmp/gh-aw/cache/qmd-docs ~/.cache\nln -sfn /tmp/gh-aw/cache/qmd-docs ~/.cache/qmd\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" + run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" # Cache memory file share configuration from frontmatter processed below - name: Create cache-memory directory diff --git a/.github/workflows/dictation-prompt.lock.yml b/.github/workflows/dictation-prompt.lock.yml index a9606c2458b..3b4f5f33b4e 100644 --- a/.github/workflows/dictation-prompt.lock.yml +++ b/.github/workflows/dictation-prompt.lock.yml @@ -28,7 +28,7 @@ # - shared/mcp/qmd-docs.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"95efc73940ca0c7c170dca71f137ad47e80873445b33c95090f52d713b79f591","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"06d87876523bb7d9b4f99d72c34ff0965474e7b4ca8f7e657a276596b119b594","strict":true} name: "Dictation Prompt Generator" "on": @@ -296,10 +296,10 @@ jobs: uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} - path: /tmp/gh-aw/cache/qmd-docs + path: ~/.cache/qmd restore-keys: qmd-docs- - name: Start QMD MCP server - run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/ /tmp/gh-aw/cache/qmd-docs ~/.cache\nln -sfn /tmp/gh-aw/cache/qmd-docs ~/.cache/qmd\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" + run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" - name: Configure Git credentials env: diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml index 6765f0ca321..1f599ea6e23 100644 --- a/.github/workflows/glossary-maintainer.lock.yml +++ b/.github/workflows/glossary-maintainer.lock.yml @@ -30,7 +30,7 @@ # - shared/mcp/qmd-docs.md # - shared/mcp/serena-go.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"c93b02ab9448ae744c33ffd66ce6435fcdd3a8efce53ec6da833283aabed58a9","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"474cc67d7db6b1b65413c80bc89f19fabc09cb1b40bf4a639e4826ad1e628578","strict":true} name: "Glossary Maintainer" "on": @@ -342,10 +342,10 @@ jobs: uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} - path: /tmp/gh-aw/cache/qmd-docs + path: ~/.cache/qmd restore-keys: qmd-docs- - name: Start QMD MCP server - run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/ /tmp/gh-aw/cache/qmd-docs ~/.cache\nln -sfn /tmp/gh-aw/cache/qmd-docs ~/.cache/qmd\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" + run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" # Cache memory file share configuration from frontmatter processed below - name: Create cache-memory directory diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 38847c5e1e1..2b5987846da 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -29,7 +29,7 @@ # - ../skills/documentation/SKILL.md # - shared/mcp/qmd-docs.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"6bfbae28e605cee1bda90beea0fc1a949f9bf24de09611ad3e2d8c4446f15f02","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"ff83f6edccccc4e75b6305b5374e23aba06ee6be27e5dd9fd6cb4cc33839f052","strict":true} name: "Rebuild the documentation after making changes" "on": @@ -346,10 +346,10 @@ jobs: uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} - path: /tmp/gh-aw/cache/qmd-docs + path: ~/.cache/qmd restore-keys: qmd-docs- - name: Start QMD MCP server - run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/ /tmp/gh-aw/cache/qmd-docs ~/.cache\nln -sfn /tmp/gh-aw/cache/qmd-docs ~/.cache/qmd\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" + run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" - name: Install dependencies run: npm ci working-directory: ./docs diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 669ce6fba63..7f91ea04f35 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -29,7 +29,7 @@ # - shared/mcp/qmd-docs.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"ca1d2986551f0d2769f4034506bee7a81b462d860dc36fa864b9b849a813cfdb","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"f80a0d009fbcd00205e16fe0c698bd48fd56c0dfbf5a0130608b7e7e99b7adc3","strict":true} name: "Documentation Unbloat" "on": @@ -348,10 +348,10 @@ jobs: uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} - path: /tmp/gh-aw/cache/qmd-docs + path: ~/.cache/qmd restore-keys: qmd-docs- - name: Start QMD MCP server - run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/ /tmp/gh-aw/cache/qmd-docs ~/.cache\nln -sfn /tmp/gh-aw/cache/qmd-docs ~/.cache/qmd\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" + run: "set -e\nmkdir -p /tmp/gh-aw/mcp-logs/qmd/\n\n# Start QMD MCP server in HTTP daemon mode (default port 8181)\nqmd mcp --http --daemon > /tmp/gh-aw/mcp-logs/qmd/server.log 2>&1\n\n# Poll until the server is healthy (up to 15 seconds)\nfor i in $(seq 1 30); do\n if curl -sf http://localhost:8181/health > /dev/null 2>&1; then\n echo \"QMD MCP server started successfully\"\n echo \"Status: $(curl -s http://localhost:8181/health)\"\n exit 0\n fi\n sleep 0.5\ndone\n\necho \"QMD MCP server health check timed out after 15 seconds\"\necho \"Server logs:\"\ncat /tmp/gh-aw/mcp-logs/qmd/server.log || true\nexit 1" - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: From 263d538e7699fe877988cf0877d675c2973daf6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:47:54 +0000 Subject: [PATCH 6/8] feat: move github-app/github-token to top-level on: for skip-if checks with unified mint step and import support Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/constants/constants.go | 4 + pkg/parser/content_extractor.go | 34 ++++++ pkg/parser/import_field_extractor.go | 111 +++++++++++++----- pkg/parser/import_processor.go | 56 ++++----- pkg/parser/schemas/main_workflow_schema.json | 76 +----------- .../compiler_orchestrator_workflow.go | 4 +- pkg/workflow/compiler_pre_activation_job.go | 63 +++++----- pkg/workflow/compiler_types.go | 18 ++- pkg/workflow/frontmatter_extraction_yaml.go | 4 +- pkg/workflow/role_checks.go | 40 +++++++ pkg/workflow/skip_if_match_test.go | 24 ++-- pkg/workflow/skip_if_no_match_test.go | 83 +++++++++++-- pkg/workflow/stop_after.go | 66 +++-------- 13 files changed, 337 insertions(+), 246 deletions(-) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 887256f76e4..01944ff92b6 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -676,6 +676,10 @@ const CheckRateLimitStepID StepID = "check_rate_limit" const CheckSkipRolesStepID StepID = "check_skip_roles" const CheckSkipBotsStepID StepID = "check_skip_bots" +// PreActivationAppTokenStepID is the step ID for the unified GitHub App token mint step +// emitted in the pre-activation job when on.github-app is configured alongside skip-if checks. +const PreActivationAppTokenStepID StepID = "pre-activation-app-token" + // Output names for pre-activation job steps const IsTeamMemberOutput = "is_team_member" const StopTimeOkOutput = "stop_time_ok" diff --git a/pkg/parser/content_extractor.go b/pkg/parser/content_extractor.go index f71addd6d7b..4cd08ca98ae 100644 --- a/pkg/parser/content_extractor.go +++ b/pkg/parser/content_extractor.go @@ -213,3 +213,37 @@ func extractOnSectionField(content, fieldName string) (string, error) { contentExtractorLog.Printf("Successfully extracted field %s from on: section: %d bytes", fieldName, len(jsonData)) return string(jsonData), nil } + +// extractOnSectionAnyField extracts a specific field from the on: section in frontmatter as +// a JSON string, handling any value type (string, object, array, etc.). +// Returns "" when the field is absent or an error occurs. +func extractOnSectionAnyField(content, fieldName string) (string, error) { + contentExtractorLog.Printf("Extracting on: section field (any): %s", fieldName) + result, err := ExtractFrontmatterFromContent(content) + if err != nil { + return "", nil + } + + onValue, exists := result.Frontmatter["on"] + if !exists { + return "", nil + } + + onMap, ok := onValue.(map[string]any) + if !ok { + return "", nil + } + + fieldValue, exists := onMap[fieldName] + if !exists { + return "", nil + } + + jsonData, err := json.Marshal(fieldValue) + if err != nil { + return "", nil + } + + contentExtractorLog.Printf("Successfully extracted on.%s: %d bytes", fieldName, len(jsonData)) + return string(jsonData), nil +} diff --git a/pkg/parser/import_field_extractor.go b/pkg/parser/import_field_extractor.go index 8b868c5931d..5621d441010 100644 --- a/pkg/parser/import_field_extractor.go +++ b/pkg/parser/import_field_extractor.go @@ -47,6 +47,9 @@ type importAccumulator struct { agentImportSpec string repositoryImports []string importInputs map[string]any + // First on.github-token / on.github-app found across all imported files (first-wins strategy) + activationGitHubToken string + activationGitHubApp string // JSON-encoded GitHubAppConfig } // newImportAccumulator creates and initializes a new importAccumulator. @@ -209,6 +212,22 @@ func (acc *importAccumulator) extractAllImportFields(content []byte, item import } } + // Extract on.github-token from imported file (first-wins: only set if not yet populated) + if acc.activationGitHubToken == "" { + if token := extractOnGitHubToken(string(content)); token != "" { + acc.activationGitHubToken = token + log.Printf("Extracted on.github-token from import: %s", item.fullPath) + } + } + + // Extract on.github-app from imported file (first-wins: only set if not yet populated) + if acc.activationGitHubApp == "" { + if appJSON := extractOnGitHubApp(string(content)); appJSON != "" { + acc.activationGitHubApp = appJSON + log.Printf("Extracted on.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", "[]") @@ -282,34 +301,36 @@ func (acc *importAccumulator) toImportsResult(topologicalOrder []string) *Import log.Printf("Building ImportsResult: importedFiles=%d, importPaths=%d, engines=%d, bots=%d, plugins=%d, labels=%d", len(topologicalOrder), len(acc.importPaths), len(acc.engines), len(acc.bots), len(acc.plugins), len(acc.labels)) return &ImportsResult{ - MergedTools: acc.toolsBuilder.String(), - MergedMCPServers: acc.mcpServersBuilder.String(), - MergedEngines: acc.engines, - MergedSafeOutputs: acc.safeOutputs, - MergedMCPScripts: acc.mcpScripts, - MergedMarkdown: acc.markdownBuilder.String(), - ImportPaths: acc.importPaths, - MergedSteps: acc.stepsBuilder.String(), - CopilotSetupSteps: acc.copilotSetupStepsBuilder.String(), - MergedRuntimes: acc.runtimesBuilder.String(), - MergedServices: acc.servicesBuilder.String(), - MergedNetwork: acc.networkBuilder.String(), - MergedPermissions: acc.permissionsBuilder.String(), - MergedSecretMasking: acc.secretMaskingBuilder.String(), - MergedBots: acc.bots, - MergedPlugins: acc.plugins, - MergedSkipRoles: acc.skipRoles, - MergedSkipBots: acc.skipBots, - MergedPostSteps: acc.postStepsBuilder.String(), - MergedLabels: acc.labels, - MergedCaches: acc.caches, - MergedJobs: acc.jobsBuilder.String(), - MergedFeatures: acc.features, - ImportedFiles: topologicalOrder, - AgentFile: acc.agentFile, - AgentImportSpec: acc.agentImportSpec, - RepositoryImports: acc.repositoryImports, - ImportInputs: acc.importInputs, + MergedTools: acc.toolsBuilder.String(), + MergedMCPServers: acc.mcpServersBuilder.String(), + MergedEngines: acc.engines, + MergedSafeOutputs: acc.safeOutputs, + MergedMCPScripts: acc.mcpScripts, + MergedMarkdown: acc.markdownBuilder.String(), + ImportPaths: acc.importPaths, + MergedSteps: acc.stepsBuilder.String(), + CopilotSetupSteps: acc.copilotSetupStepsBuilder.String(), + MergedRuntimes: acc.runtimesBuilder.String(), + MergedServices: acc.servicesBuilder.String(), + MergedNetwork: acc.networkBuilder.String(), + MergedPermissions: acc.permissionsBuilder.String(), + MergedSecretMasking: acc.secretMaskingBuilder.String(), + MergedBots: acc.bots, + MergedPlugins: acc.plugins, + MergedSkipRoles: acc.skipRoles, + MergedSkipBots: acc.skipBots, + MergedPostSteps: acc.postStepsBuilder.String(), + MergedLabels: acc.labels, + MergedCaches: acc.caches, + MergedJobs: acc.jobsBuilder.String(), + MergedFeatures: acc.features, + ImportedFiles: topologicalOrder, + AgentFile: acc.agentFile, + AgentImportSpec: acc.agentImportSpec, + RepositoryImports: acc.repositoryImports, + ImportInputs: acc.importInputs, + MergedActivationGitHubToken: acc.activationGitHubToken, + MergedActivationGitHubApp: acc.activationGitHubApp, } } @@ -334,3 +355,37 @@ func computeImportRelPath(fullPath, importPath string) string { } return importPath } + +// extractOnGitHubToken returns the on.github-token string value from workflow content. +// Returns "" if the field is absent or not a non-empty string. +func extractOnGitHubToken(content string) string { + tokenJSON, err := extractOnSectionAnyField(content, "github-token") + if err != nil || tokenJSON == "" || tokenJSON == "null" { + return "" + } + var token string + if err := json.Unmarshal([]byte(tokenJSON), &token); err != nil { + return "" + } + 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" { + return "" + } + var appMap map[string]any + if err := json.Unmarshal([]byte(appJSON), &appMap); err != nil { + return "" + } + if _, hasID := appMap["app-id"]; !hasID { + return "" + } + if _, hasKey := appMap["private-key"]; !hasKey { + return "" + } + return appJSON +} diff --git a/pkg/parser/import_processor.go b/pkg/parser/import_processor.go index 6742f59f6ab..872afbf88ad 100644 --- a/pkg/parser/import_processor.go +++ b/pkg/parser/import_processor.go @@ -14,33 +14,35 @@ var importLog = logger.New("parser:import_processor") // ImportsResult holds the result of processing imports from frontmatter type ImportsResult struct { - MergedTools string // Merged tools configuration from all imports - MergedMCPServers string // Merged mcp-servers configuration from all imports - MergedEngines []string // Merged engine configurations from all imports - MergedSafeOutputs []string // Merged safe-outputs configurations from all imports - MergedMCPScripts []string // Merged mcp-scripts configurations from all imports - MergedMarkdown string // Only contains imports WITH inputs (for compile-time substitution) - ImportPaths []string // List of import file paths for runtime-import macro generation (replaces MergedMarkdown) - MergedSteps string // Merged steps configuration from all imports (excluding copilot-setup-steps) - CopilotSetupSteps string // Steps from copilot-setup-steps.yml (inserted at start) - MergedRuntimes string // Merged runtimes configuration from all imports - MergedServices string // Merged services configuration from all imports - MergedNetwork string // Merged network configuration from all imports - MergedPermissions string // Merged permissions configuration from all imports - MergedSecretMasking string // Merged secret-masking steps from all imports - MergedBots []string // Merged bots list from all imports (union of bot names) - MergedPlugins []string // Merged plugins list from all imports (union of plugin repos) - MergedSkipRoles []string // Merged skip-roles list from all imports (union of role names) - MergedSkipBots []string // Merged skip-bots list from all imports (union of usernames) - 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) - MergedJobs string // Merged jobs from imported YAML workflows (JSON format) - MergedFeatures []map[string]any // Merged features configuration from all imports (parsed YAML structures) - ImportedFiles []string // List of imported file paths (for manifest) - AgentFile string // Path to custom agent file (if imported) - AgentImportSpec string // Original import specification for agent file (e.g., "owner/repo/path@ref") - RepositoryImports []string // List of repository imports (format: "owner/repo@ref") for .github folder merging + MergedTools string // Merged tools configuration from all imports + MergedMCPServers string // Merged mcp-servers configuration from all imports + MergedEngines []string // Merged engine configurations from all imports + MergedSafeOutputs []string // Merged safe-outputs configurations from all imports + MergedMCPScripts []string // Merged mcp-scripts configurations from all imports + MergedMarkdown string // Only contains imports WITH inputs (for compile-time substitution) + ImportPaths []string // List of import file paths for runtime-import macro generation (replaces MergedMarkdown) + MergedSteps string // Merged steps configuration from all imports (excluding copilot-setup-steps) + CopilotSetupSteps string // Steps from copilot-setup-steps.yml (inserted at start) + MergedRuntimes string // Merged runtimes configuration from all imports + MergedServices string // Merged services configuration from all imports + MergedNetwork string // Merged network configuration from all imports + MergedPermissions string // Merged permissions configuration from all imports + MergedSecretMasking string // Merged secret-masking steps from all imports + MergedBots []string // Merged bots list from all imports (union of bot names) + MergedPlugins []string // Merged plugins list from all imports (union of plugin repos) + MergedSkipRoles []string // Merged skip-roles list from all imports (union of role names) + 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 + 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) + MergedJobs string // Merged jobs from imported YAML workflows (JSON format) + MergedFeatures []map[string]any // Merged features configuration from all imports (parsed YAML structures) + ImportedFiles []string // List of imported file paths (for manifest) + AgentFile string // Path to custom agent file (if imported) + AgentImportSpec string // Original import specification for agent file (e.g., "owner/repo/path@ref") + RepositoryImports []string // List of repository imports (format: "owner/repo@ref") for .github folder merging // ImportInputs uses map[string]any because input values can be different types (string, number, boolean). // This is parsed from YAML frontmatter where the structure is dynamic and not known at compile time. // This is an appropriate use of 'any' for dynamic YAML/JSON data. diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 5bc68765f92..7c812bea099 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -1344,45 +1344,13 @@ "type": "string", "enum": ["none"], "description": "Scope for the search query. Set to 'none' to disable the automatic 'repo:owner/repo' scoping, enabling org-wide or cross-repo queries." - }, - "github-token": { - "type": "string", - "description": "Custom GitHub token to use for the search API call. Useful for cross-repo or org-wide searches that require additional permissions.", - "examples": ["${{ secrets.CROSS_ORG_TOKEN }}"] - }, - "github-app": { - "type": "object", - "description": "GitHub App configuration for minting a token used for the search API call.", - "properties": { - "app-id": { - "type": "string", - "description": "GitHub App ID (e.g., '${{ secrets.APP_ID }}'). Required." - }, - "private-key": { - "type": "string", - "description": "GitHub App private key (e.g., '${{ secrets.APP_PRIVATE_KEY }}'). Required." - }, - "owner": { - "type": "string", - "description": "Optional owner of the GitHub App installation (defaults to current repository owner)." - }, - "repositories": { - "type": "array", - "description": "Optional list of repositories to grant access to. Use ['*'] for org-wide access.", - "items": { - "type": "string" - } - } - }, - "required": ["app-id", "private-key"], - "additionalProperties": false } }, "additionalProperties": false, - "description": "Skip-if-match configuration object with query, maximum match count, and optional scope/auth settings" + "description": "Skip-if-match configuration object with query, maximum match count, and optional scope. For custom authentication use the top-level on.github-token or on.github-app fields." } ], - "description": "Conditionally skip workflow execution when a GitHub search query has matches. Can be a string (query only, implies max=1) or an object with 'query', optional 'max', 'scope', 'github-token', and 'github-app' fields." + "description": "Conditionally skip workflow execution when a GitHub search query has matches. Can be a string (query only, implies max=1) or an object with 'query', optional 'max', and 'scope' fields. Use top-level on.github-token or on.github-app for custom authentication." }, "skip-if-no-match": { "oneOf": [ @@ -1407,45 +1375,13 @@ "type": "string", "enum": ["none"], "description": "Scope for the search query. Set to 'none' to disable the automatic 'repo:owner/repo' scoping, enabling org-wide or cross-repo queries." - }, - "github-token": { - "type": "string", - "description": "Custom GitHub token to use for the search API call. Useful for cross-repo or org-wide searches that require additional permissions.", - "examples": ["${{ secrets.CROSS_ORG_TOKEN }}"] - }, - "github-app": { - "type": "object", - "description": "GitHub App configuration for minting a token used for the search API call.", - "properties": { - "app-id": { - "type": "string", - "description": "GitHub App ID (e.g., '${{ secrets.APP_ID }}'). Required." - }, - "private-key": { - "type": "string", - "description": "GitHub App private key (e.g., '${{ secrets.APP_PRIVATE_KEY }}'). Required." - }, - "owner": { - "type": "string", - "description": "Optional owner of the GitHub App installation (defaults to current repository owner)." - }, - "repositories": { - "type": "array", - "description": "Optional list of repositories to grant access to. Use ['*'] for org-wide access.", - "items": { - "type": "string" - } - } - }, - "required": ["app-id", "private-key"], - "additionalProperties": false } }, "additionalProperties": false, - "description": "Skip-if-no-match configuration object with query, minimum match count, and optional scope/auth settings" + "description": "Skip-if-no-match configuration object with query, minimum match count, and optional scope. For custom authentication use the top-level on.github-token or on.github-app fields." } ], - "description": "Conditionally skip workflow execution when a GitHub search query has no matches (or fewer than minimum). Can be a string (query only, implies min=1) or an object with 'query', optional 'min', 'scope', 'github-token', and 'github-app' fields." + "description": "Conditionally skip workflow execution when a GitHub search query has no matches (or fewer than minimum). Can be a string (query only, implies min=1) or an object with 'query', optional 'min', and 'scope' fields. Use top-level on.github-token or on.github-app for custom authentication." }, "skip-roles": { "oneOf": [ @@ -1538,12 +1474,12 @@ }, "github-token": { "type": "string", - "description": "Custom GitHub token to use for pre-activation reactions and activation status comments. When specified, overrides the default GITHUB_TOKEN for these operations.", + "description": "Custom GitHub token for pre-activation reactions, activation status comments, and skip-if search queries. When specified, overrides the default GITHUB_TOKEN for these operations.", "examples": ["${{ secrets.MY_GITHUB_TOKEN }}"] }, "github-app": { "type": "object", - "description": "GitHub App configuration for minting a token used in pre-activation reactions and activation status comments. When configured, a GitHub App installation access token is minted and used instead of the default GITHUB_TOKEN.", + "description": "GitHub App configuration for minting a token used in pre-activation reactions, activation status comments, and skip-if search queries. When configured, a single GitHub App installation access token is minted and shared across all these operations instead of using the default GITHUB_TOKEN. Can be defined in a shared agentic workflow and inherited by importing workflows.", "properties": { "app-id": { "type": "string", diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 0e8478e0ec2..1da057ff624 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -590,8 +590,8 @@ func (c *Compiler) extractAdditionalConfigurations( workflowData.RateLimit = c.extractRateLimitConfig(frontmatter) workflowData.SkipRoles = c.mergeSkipRoles(c.extractSkipRoles(frontmatter), importsResult.MergedSkipRoles) workflowData.SkipBots = c.mergeSkipBots(c.extractSkipBots(frontmatter), importsResult.MergedSkipBots) - workflowData.ActivationGitHubToken = c.extractActivationGitHubToken(frontmatter) - workflowData.ActivationGitHubApp = c.extractActivationGitHubApp(frontmatter) + workflowData.ActivationGitHubToken = c.resolveActivationGitHubToken(frontmatter, importsResult) + workflowData.ActivationGitHubApp = c.resolveActivationGitHubApp(frontmatter, importsResult) // Use the already extracted output configuration workflowData.SafeOutputs = safeOutputs diff --git a/pkg/workflow/compiler_pre_activation_job.go b/pkg/workflow/compiler_pre_activation_job.go index 19661cabdea..8ebdbddcb6b 100644 --- a/pkg/workflow/compiler_pre_activation_job.go +++ b/pkg/workflow/compiler_pre_activation_job.go @@ -88,16 +88,20 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec steps = append(steps, generateGitHubScriptWithRequire("check_stop_time.cjs")) } + // Emit a single unified GitHub App token mint step if on.github-app is configured + // and any skip-if check is present. Both checks share the same minted token. + hasSkipIfCheck := data.SkipIfMatch != nil || data.SkipIfNoMatch != nil + if hasSkipIfCheck && data.ActivationGitHubApp != nil { + steps = append(steps, c.buildPreActivationAppTokenMintStep(data.ActivationGitHubApp)...) + } + + // Resolve the token expression to use for skip-if checks (app token > custom token > default) + skipIfToken := c.resolvePreActivationSkipIfToken(data) + // Add skip-if-match check if configured if data.SkipIfMatch != nil { - // Extract workflow name for the skip-if-match check workflowName := data.Name - // Mint a GitHub App token for the skip-if-match check if a GitHub App is configured - if data.SkipIfMatch.GitHubApp != nil { - steps = append(steps, c.buildSkipIfAppTokenMintStep(data.SkipIfMatch.GitHubApp, constants.CheckSkipIfMatchStepID)...) - } - steps = append(steps, " - name: Check skip-if-match query\n") steps = append(steps, fmt.Sprintf(" id: %s\n", constants.CheckSkipIfMatchStepID)) steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/github-script"))) @@ -109,10 +113,8 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec steps = append(steps, fmt.Sprintf(" GH_AW_SKIP_SCOPE: %q\n", data.SkipIfMatch.Scope)) } steps = append(steps, " with:\n") - // Use custom token or minted app token if configured - skipIfMatchToken := c.resolveSkipIfToken(data.SkipIfMatch.GitHubToken, data.SkipIfMatch.GitHubApp, constants.CheckSkipIfMatchStepID) - if skipIfMatchToken != "" { - steps = append(steps, fmt.Sprintf(" github-token: %s\n", skipIfMatchToken)) + if skipIfToken != "" { + steps = append(steps, fmt.Sprintf(" github-token: %s\n", skipIfToken)) } steps = append(steps, " script: |\n") steps = append(steps, generateGitHubScriptWithRequire("check_skip_if_match.cjs")) @@ -120,14 +122,8 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec // Add skip-if-no-match check if configured if data.SkipIfNoMatch != nil { - // Extract workflow name for the skip-if-no-match check workflowName := data.Name - // Mint a GitHub App token for the skip-if-no-match check if a GitHub App is configured - if data.SkipIfNoMatch.GitHubApp != nil { - steps = append(steps, c.buildSkipIfAppTokenMintStep(data.SkipIfNoMatch.GitHubApp, constants.CheckSkipIfNoMatchStepID)...) - } - steps = append(steps, " - name: Check skip-if-no-match query\n") steps = append(steps, fmt.Sprintf(" id: %s\n", constants.CheckSkipIfNoMatchStepID)) steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/github-script"))) @@ -139,10 +135,8 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec steps = append(steps, fmt.Sprintf(" GH_AW_SKIP_SCOPE: %q\n", data.SkipIfNoMatch.Scope)) } steps = append(steps, " with:\n") - // Use custom token or minted app token if configured - skipIfNoMatchToken := c.resolveSkipIfToken(data.SkipIfNoMatch.GitHubToken, data.SkipIfNoMatch.GitHubApp, constants.CheckSkipIfNoMatchStepID) - if skipIfNoMatchToken != "" { - steps = append(steps, fmt.Sprintf(" github-token: %s\n", skipIfNoMatchToken)) + if skipIfToken != "" { + steps = append(steps, fmt.Sprintf(" github-token: %s\n", skipIfToken)) } steps = append(steps, " script: |\n") steps = append(steps, generateGitHubScriptWithRequire("check_skip_if_no_match.cjs")) @@ -434,13 +428,14 @@ func (c *Compiler) extractPreActivationCustomFields(jobs map[string]any) ([]stri return customSteps, customOutputs, nil } -// buildSkipIfAppTokenMintStep generates a GitHub App token mint step for use in skip-if-match or skip-if-no-match checks. -// The stepIDPrefix is used to derive a unique step id for the minted token. -func (c *Compiler) buildSkipIfAppTokenMintStep(app *GitHubAppConfig, checkStepID constants.StepID) []string { +// buildPreActivationAppTokenMintStep generates a single GitHub App token mint step for use +// by all skip-if checks in the pre-activation job. The step ID is "pre-activation-app-token". +// Auth configuration comes from the top-level on.github-app field. +func (c *Compiler) buildPreActivationAppTokenMintStep(app *GitHubAppConfig) []string { var steps []string - tokenStepID := string(checkStepID) + "-app-token" + tokenStepID := constants.PreActivationAppTokenStepID - steps = append(steps, " - name: Generate GitHub App token for skip-if check\n") + steps = append(steps, " - name: Generate GitHub App token for skip-if checks\n") steps = append(steps, fmt.Sprintf(" id: %s\n", tokenStepID)) steps = append(steps, fmt.Sprintf(" uses: %s\n", GetActionPin("actions/create-github-app-token"))) steps = append(steps, " with:\n") @@ -453,7 +448,6 @@ func (c *Compiler) buildSkipIfAppTokenMintStep(app *GitHubAppConfig, checkStepID } steps = append(steps, fmt.Sprintf(" owner: %s\n", owner)) - // Repositories field: use configured list, or default to current repo if len(app.Repositories) == 1 && app.Repositories[0] == "*" { // Org-wide access: omit repositories field entirely } else if len(app.Repositories) == 1 { @@ -472,16 +466,15 @@ func (c *Compiler) buildSkipIfAppTokenMintStep(app *GitHubAppConfig, checkStepID return steps } -// resolveSkipIfToken returns the GitHub token expression to use for a skip-if check step. -// Priority: GitHub App minted token > custom github-token > empty (use default GITHUB_TOKEN). -// When a non-empty value is returned, callers should emit `with.github-token: ` in the step. -func (c *Compiler) resolveSkipIfToken(githubToken string, githubApp *GitHubAppConfig, checkStepID constants.StepID) string { - if githubApp != nil { - tokenStepID := string(checkStepID) + "-app-token" - return fmt.Sprintf("${{ steps.%s.outputs.token }}", tokenStepID) +// resolvePreActivationSkipIfToken returns the GitHub token expression to use for skip-if check +// steps in the pre-activation job. Priority: App token > custom github-token > empty (default). +// When non-empty, callers should emit `with.github-token: ` in the step. +func (c *Compiler) resolvePreActivationSkipIfToken(data *WorkflowData) string { + if data.ActivationGitHubApp != nil { + return fmt.Sprintf("${{ steps.%s.outputs.token }}", constants.PreActivationAppTokenStepID) } - if githubToken != "" { - return githubToken + if data.ActivationGitHubToken != "" { + return data.ActivationGitHubToken } return "" } diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index d56d7b6b4a7..45367b4d31e 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -323,20 +323,18 @@ func (c *Compiler) GetSharedActionCache() *ActionCache { // SkipIfMatchConfig holds the configuration for skip-if-match conditions type SkipIfMatchConfig struct { - Query string // GitHub search query to check before running workflow - Max int // Maximum number of matches before skipping (defaults to 1) - Scope string // Scope for the query: "none" disables auto repo:owner/repo scoping - GitHubToken string // Custom GitHub token to use for the search API call - GitHubApp *GitHubAppConfig // GitHub App config for minting a token for the search API call + Query string // GitHub search query to check before running workflow + Max int // Maximum number of matches before skipping (defaults to 1) + Scope string // Scope for the query: "none" disables auto repo:owner/repo scoping + // Auth (github-token / github-app) is taken from on.github-token / on.github-app at the top level. } // SkipIfNoMatchConfig holds the configuration for skip-if-no-match conditions type SkipIfNoMatchConfig struct { - Query string // GitHub search query to check before running workflow - Min int // Minimum number of matches required to proceed (defaults to 1) - Scope string // Scope for the query: "none" disables auto repo:owner/repo scoping - GitHubToken string // Custom GitHub token to use for the search API call - GitHubApp *GitHubAppConfig // GitHub App config for minting a token for the search API call + Query string // GitHub search query to check before running workflow + Min int // Minimum number of matches required to proceed (defaults to 1) + Scope string // Scope for the query: "none" disables auto repo:owner/repo scoping + // Auth (github-token / github-app) is taken from on.github-token / on.github-app at the top level. } // WorkflowData holds all the data needed to generate a GitHub Actions workflow diff --git a/pkg/workflow/frontmatter_extraction_yaml.go b/pkg/workflow/frontmatter_extraction_yaml.go index 512532d39ca..f129087d0b7 100644 --- a/pkg/workflow/frontmatter_extraction_yaml.go +++ b/pkg/workflow/frontmatter_extraction_yaml.go @@ -368,14 +368,14 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat } else if strings.HasPrefix(trimmedLine, "skip-if-match:") { shouldComment = true commentReason = " # Skip-if-match processed as search check in pre-activation job" - } else if inSkipIfMatch && (strings.HasPrefix(trimmedLine, "query:") || strings.HasPrefix(trimmedLine, "max:") || strings.HasPrefix(trimmedLine, "scope:") || strings.HasPrefix(trimmedLine, "github-token:") || strings.HasPrefix(trimmedLine, "github-app:")) { + } else if inSkipIfMatch && (strings.HasPrefix(trimmedLine, "query:") || strings.HasPrefix(trimmedLine, "max:") || strings.HasPrefix(trimmedLine, "scope:")) { // Comment out nested fields in skip-if-match object shouldComment = true commentReason = "" } else if strings.HasPrefix(trimmedLine, "skip-if-no-match:") { shouldComment = true commentReason = " # Skip-if-no-match processed as search check in pre-activation job" - } else if inSkipIfNoMatch && (strings.HasPrefix(trimmedLine, "query:") || strings.HasPrefix(trimmedLine, "min:") || strings.HasPrefix(trimmedLine, "scope:") || strings.HasPrefix(trimmedLine, "github-token:") || strings.HasPrefix(trimmedLine, "github-app:")) { + } else if inSkipIfNoMatch && (strings.HasPrefix(trimmedLine, "query:") || strings.HasPrefix(trimmedLine, "min:") || strings.HasPrefix(trimmedLine, "scope:")) { // Comment out nested fields in skip-if-no-match object shouldComment = true commentReason = "" diff --git a/pkg/workflow/role_checks.go b/pkg/workflow/role_checks.go index bb2611b4d28..03bdc0e8ad0 100644 --- a/pkg/workflow/role_checks.go +++ b/pkg/workflow/role_checks.go @@ -1,6 +1,7 @@ package workflow import ( + "encoding/json" "fmt" "slices" "sort" @@ -8,6 +9,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/parser" ) var roleLog = logger.New("workflow:role_checks") @@ -662,3 +664,41 @@ func (c *Compiler) extractActivationGitHubApp(frontmatter map[string]any) *GitHu } return nil } + +// resolveActivationGitHubToken returns the GitHub token to use for activation operations +// (reactions, status comments, skip-if checks). Precedence: +// 1. Current workflow's on.github-token (explicit override wins) +// 2. First on.github-token found across imported shared workflows +// 3. Empty string (use default GITHUB_TOKEN) +func (c *Compiler) resolveActivationGitHubToken(frontmatter map[string]any, importsResult *parser.ImportsResult) string { + if token := c.extractActivationGitHubToken(frontmatter); token != "" { + return token + } + if importsResult != nil && importsResult.MergedActivationGitHubToken != "" { + roleLog.Print("Using on.github-token from imported shared workflow") + return importsResult.MergedActivationGitHubToken + } + return "" +} + +// resolveActivationGitHubApp returns the GitHub App config to use for activation operations +// (reactions, status comments, skip-if checks). Precedence: +// 1. Current workflow's on.github-app (explicit override wins) +// 2. First on.github-app found across imported shared workflows +// 3. Nil (use default GITHUB_TOKEN) +func (c *Compiler) resolveActivationGitHubApp(frontmatter map[string]any, importsResult *parser.ImportsResult) *GitHubAppConfig { + if app := c.extractActivationGitHubApp(frontmatter); app != nil { + return app + } + if importsResult != nil && importsResult.MergedActivationGitHubApp != "" { + var appMap map[string]any + if err := json.Unmarshal([]byte(importsResult.MergedActivationGitHubApp), &appMap); err == nil { + app := parseAppConfig(appMap) + if app.AppID != "" && app.PrivateKey != "" { + roleLog.Print("Using on.github-app from imported shared workflow") + return app + } + } + } + return nil +} diff --git a/pkg/workflow/skip_if_match_test.go b/pkg/workflow/skip_if_match_test.go index a4c6d313ccf..2d1ffc7a643 100644 --- a/pkg/workflow/skip_if_match_test.go +++ b/pkg/workflow/skip_if_match_test.go @@ -343,7 +343,7 @@ on: skip-if-match: query: "org:myorg label:blocked is:issue is:open" scope: none - github-token: ${{ secrets.CROSS_ORG_TOKEN }} + github-token: ${{ secrets.CROSS_ORG_TOKEN }} engine: claude --- @@ -374,7 +374,7 @@ This workflow uses a custom token for org-wide search. t.Error("Expected skip-if-match check to be present") } - // Verify the custom github-token is passed via with.github-token + // Verify the custom github-token is passed via with.github-token to the skip-if step if !strings.Contains(lockContentStr, "github-token: ${{ secrets.CROSS_ORG_TOKEN }}") { t.Error("Expected github-token to be set in with section for skip-if-match step") } @@ -393,10 +393,10 @@ on: skip-if-match: query: "org:myorg label:blocked is:issue is:open" scope: none - github-app: - app-id: ${{ secrets.WORKFLOW_APP_ID }} - private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} - owner: myorg + github-app: + app-id: ${{ secrets.WORKFLOW_APP_ID }} + private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} + owner: myorg engine: claude --- @@ -422,9 +422,9 @@ This workflow uses a GitHub App token for org-wide search. lockContentStr := string(lockContent) - // Verify the GitHub App token mint step is generated - if !strings.Contains(lockContentStr, "Generate GitHub App token for skip-if check") { - t.Error("Expected GitHub App token mint step to be present") + // Verify the unified GitHub App token mint step is generated + if !strings.Contains(lockContentStr, "Generate GitHub App token for skip-if checks") { + t.Error("Expected unified GitHub App token mint step to be present") } // Verify app-id and private-key are in the mint step @@ -440,9 +440,9 @@ This workflow uses a GitHub App token for org-wide search. t.Error("Expected owner to be set in GitHub App token mint step") } - // Verify the minted token is used in the skip-if step - if !strings.Contains(lockContentStr, "github-token: ${{ steps.check_skip_if_match-app-token.outputs.token }}") { - t.Error("Expected minted app token to be used in skip-if-match step") + // Verify the minted token is used in the skip-if step via the unified step ID + if !strings.Contains(lockContentStr, "github-token: ${{ steps.pre-activation-app-token.outputs.token }}") { + t.Error("Expected minted app token (pre-activation-app-token) to be used in skip-if-match step") } // Verify GH_AW_SKIP_SCOPE is set to "none" diff --git a/pkg/workflow/skip_if_no_match_test.go b/pkg/workflow/skip_if_no_match_test.go index a0f09de52e1..6f4abb5d619 100644 --- a/pkg/workflow/skip_if_no_match_test.go +++ b/pkg/workflow/skip_if_no_match_test.go @@ -402,7 +402,7 @@ on: skip-if-no-match: query: "org:myorg label:agent-fix is:issue is:open" scope: none - github-token: ${{ secrets.CROSS_ORG_TOKEN }} + github-token: ${{ secrets.CROSS_ORG_TOKEN }} engine: claude --- @@ -433,7 +433,7 @@ This workflow uses a custom token for org-wide search. t.Error("Expected skip-if-no-match check to be present") } - // Verify the custom github-token is passed via with.github-token + // Verify the custom github-token is passed via with.github-token to the skip-if step if !strings.Contains(lockContentStr, "github-token: ${{ secrets.CROSS_ORG_TOKEN }}") { t.Error("Expected github-token to be set in with section for skip-if-no-match step") } @@ -452,10 +452,10 @@ on: skip-if-no-match: query: "org:myorg label:agent-fix is:issue is:open" scope: none - github-app: - app-id: ${{ secrets.WORKFLOW_APP_ID }} - private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} - owner: myorg + github-app: + app-id: ${{ secrets.WORKFLOW_APP_ID }} + private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} + owner: myorg engine: claude --- @@ -481,9 +481,9 @@ This workflow uses a GitHub App token for org-wide search. lockContentStr := string(lockContent) - // Verify the GitHub App token mint step is generated before the skip check - if !strings.Contains(lockContentStr, "Generate GitHub App token for skip-if check") { - t.Error("Expected GitHub App token mint step to be present") + // Verify the unified GitHub App token mint step is generated before the skip check + if !strings.Contains(lockContentStr, "Generate GitHub App token for skip-if checks") { + t.Error("Expected unified GitHub App token mint step to be present") } // Verify app-id and private-key are in the mint step @@ -499,9 +499,9 @@ This workflow uses a GitHub App token for org-wide search. t.Error("Expected owner to be set in GitHub App token mint step") } - // Verify the minted token is used in the skip-if step - if !strings.Contains(lockContentStr, "github-token: ${{ steps.check_skip_if_no_match-app-token.outputs.token }}") { - t.Error("Expected minted app token to be used in skip-if-no-match step") + // Verify the minted token is used in the skip-if step via the unified step ID + if !strings.Contains(lockContentStr, "github-token: ${{ steps.pre-activation-app-token.outputs.token }}") { + t.Error("Expected minted app token (pre-activation-app-token) to be used in skip-if-no-match step") } // Verify GH_AW_SKIP_SCOPE is set to "none" @@ -509,4 +509,63 @@ This workflow uses a GitHub App token for org-wide search. t.Error("Expected GH_AW_SKIP_SCOPE environment variable set to none") } }) + + t.Run("unified_app_token_step_for_both_skip_checks", func(t *testing.T) { + workflowContent := `--- +on: + schedule: + - cron: "*/15 * * * *" + skip-if-match: + query: "org:myorg label:blocked is:issue is:open" + scope: none + skip-if-no-match: + query: "org:myorg label:agent-fix is:issue is:open" + scope: none + github-app: + app-id: ${{ secrets.WORKFLOW_APP_ID }} + private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} + owner: myorg +engine: claude +--- + +# Unified App Token For Both Skip Checks + +Both skip-if-match and skip-if-no-match share one mint step. +` + workflowFile := filepath.Join(tmpDir, "unified-app-token-workflow.md") + if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil { + t.Fatal(err) + } + + err := compiler.CompileWorkflow(workflowFile) + if err != nil { + t.Fatalf("Compilation failed: %v", err) + } + + lockFile := stringutil.MarkdownToLockFile(workflowFile) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + lockContentStr := string(lockContent) + + // Exactly ONE unified mint step should be present + mintStepCount := strings.Count(lockContentStr, "Generate GitHub App token for skip-if checks") + if mintStepCount != 1 { + t.Errorf("Expected exactly 1 unified mint step, got %d", mintStepCount) + } + + // Both skip-if checks should reference the same unified token step + if !strings.Contains(lockContentStr, "Check skip-if-match query") { + t.Error("Expected skip-if-match check to be present") + } + if !strings.Contains(lockContentStr, "Check skip-if-no-match query") { + t.Error("Expected skip-if-no-match check to be present") + } + // Both reference the same pre-activation-app-token step + if strings.Count(lockContentStr, "github-token: ${{ steps.pre-activation-app-token.outputs.token }}") != 2 { + t.Error("Expected both skip-if steps to reference the unified pre-activation-app-token step") + } + }) } diff --git a/pkg/workflow/stop_after.go b/pkg/workflow/stop_after.go index 62328996b05..7353297aa7e 100644 --- a/pkg/workflow/stop_after.go +++ b/pkg/workflow/stop_after.go @@ -254,18 +254,16 @@ func (c *Compiler) extractSkipIfMatchFromOn(frontmatter map[string]any, workflow } } - // Extract scope, github-token, and github-app (optional auth/scope overrides) - scope, githubToken, githubApp, err := extractSkipIfAuthConfig(skip, "skip-if-match") + // Extract scope (auth is now configured via top-level on.github-app / on.github-token) + scope, err := extractSkipIfScope(skip, "skip-if-match") if err != nil { return nil, err } return &SkipIfMatchConfig{ - Query: queryStr, - Max: maxVal, - Scope: scope, - GitHubToken: githubToken, - GitHubApp: githubApp, + Query: queryStr, + Max: maxVal, + Scope: scope, }, nil default: return nil, fmt.Errorf("skip-if-match value must be a string or object, got %T. Examples:\n skip-if-match: \"is:issue is:open\"\n skip-if-match:\n query: \"is:pr is:open\"\n max: 3", skipIfMatch) @@ -342,18 +340,16 @@ func (c *Compiler) extractSkipIfNoMatchFromOn(frontmatter map[string]any, workfl } } - // Extract scope, github-token, and github-app (optional auth/scope overrides) - scope, githubToken, githubApp, err := extractSkipIfAuthConfig(skip, "skip-if-no-match") + // Extract scope (auth is now configured via top-level on.github-app / on.github-token) + scope, err := extractSkipIfScope(skip, "skip-if-no-match") if err != nil { return nil, err } return &SkipIfNoMatchConfig{ - Query: queryStr, - Min: minVal, - Scope: scope, - GitHubToken: githubToken, - GitHubApp: githubApp, + Query: queryStr, + Min: minVal, + Scope: scope, }, nil default: return nil, fmt.Errorf("skip-if-no-match value must be a string or object, got %T. Examples:\n skip-if-no-match: \"is:pr is:open\"\n skip-if-no-match:\n query: \"is:pr is:open\"\n min: 3", skipIfNoMatch) @@ -405,46 +401,20 @@ func (c *Compiler) processSkipIfNoMatchConfiguration(frontmatter map[string]any, return nil } -// extractSkipIfAuthConfig extracts the optional scope, github-token, and github-app fields -// shared by both skip-if-match and skip-if-no-match object configurations. +// extractSkipIfScope extracts the optional scope field from a skip-if-match or skip-if-no-match +// object configuration. Auth fields (github-token, github-app) are configured at the top-level +// on: section and are no longer accepted inside skip-if blocks. // conditionName is used only for error messages (e.g. "skip-if-match"). -func extractSkipIfAuthConfig(skip map[string]any, conditionName string) (scope string, githubToken string, githubApp *GitHubAppConfig, err error) { - // Extract scope value (optional) +func extractSkipIfScope(skip map[string]any, conditionName string) (string, error) { if scopeRaw, hasScope := skip["scope"]; hasScope { scopeStr, ok := scopeRaw.(string) if !ok { - return "", "", nil, fmt.Errorf("%s 'scope' field must be a string, got %T. Example: scope: none", conditionName, scopeRaw) + return "", fmt.Errorf("%s 'scope' field must be a string, got %T. Example: scope: none", conditionName, scopeRaw) } if scopeStr != "none" { - return "", "", nil, fmt.Errorf("%s 'scope' field must be \"none\" or omitted, got %q", conditionName, scopeStr) + return "", fmt.Errorf("%s 'scope' field must be \"none\" or omitted, got %q", conditionName, scopeStr) } - scope = scopeStr + return scopeStr, nil } - - // Extract github-token value (optional) - if tokenRaw, hasToken := skip["github-token"]; hasToken { - tokenStr, ok := tokenRaw.(string) - if !ok { - return "", "", nil, fmt.Errorf("%s 'github-token' field must be a string, got %T", conditionName, tokenRaw) - } - githubToken = tokenStr - } - - // Extract github-app value (optional) - if appRaw, hasApp := skip["github-app"]; hasApp { - appMap, ok := appRaw.(map[string]any) - if !ok { - return "", "", nil, fmt.Errorf("%s 'github-app' field must be an object, got %T", conditionName, appRaw) - } - githubApp = parseAppConfig(appMap) - if githubApp.AppID == "" || githubApp.PrivateKey == "" { - return "", "", nil, fmt.Errorf("%s 'github-app' requires both 'app-id' and 'private-key' fields", conditionName) - } - } - - if githubToken != "" && githubApp != nil { - return "", "", nil, fmt.Errorf("%s 'github-token' and 'github-app' cannot both be set. Use one authentication method", conditionName) - } - - return scope, githubToken, githubApp, nil + return "", nil } From ab5c1c41f28c925a1af54d663dd93d9ec456f01d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:19:46 +0000 Subject: [PATCH 7/8] chore: merge main, recompile workflows, update docs for github-app/github-token relocation to top-level on: Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/aw/actions-lock.json | 27 ++---- .github/workflows/daily-doc-healer.lock.yml | 4 +- .github/workflows/daily-doc-updater.lock.yml | 4 +- .../developer-docs-consolidator.lock.yml | 4 +- .github/workflows/dictation-prompt.lock.yml | 4 +- .../workflows/glossary-maintainer.lock.yml | 4 +- .github/workflows/mcp-inspector.lock.yml | 2 +- .github/workflows/qmd-docs-indexer.yml | 8 +- .github/workflows/shared/mcp/qmd-docs.md | 7 +- .../workflows/technical-doc-writer.lock.yml | 4 +- .github/workflows/unbloat-docs.lock.yml | 4 +- .../docs/reference/frontmatter-full.md | 83 ++++++------------- .../src/content/docs/reference/frontmatter.md | 14 ++-- .../reference/safe-outputs-pull-requests.md | 3 + docs/src/content/docs/reference/triggers.md | 60 ++++++++++---- pkg/workflow/data/action_pins.json | 27 ++---- 16 files changed, 113 insertions(+), 146 deletions(-) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index a98a9f788a1..e4dac974767 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -10,21 +10,11 @@ "version": "v4.1.0", "sha": "a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32" }, - "actions/cache/restore@v4": { - "repo": "actions/cache/restore", - "version": "v4", - "sha": "0057852bfaa89a56745cba8c7296529d2fc39830" - }, "actions/cache/restore@v5.0.3": { "repo": "actions/cache/restore", "version": "v5.0.3", "sha": "cdf6c1fa76f9f475f3d7449005a359c84ca0f306" }, - "actions/cache/save@v4": { - "repo": "actions/cache/save", - "version": "v4", - "sha": "0057852bfaa89a56745cba8c7296529d2fc39830" - }, "actions/cache/save@v5.0.3": { "repo": "actions/cache/save", "version": "v5.0.3", @@ -40,10 +30,10 @@ "version": "v6.0.2", "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd" }, - "actions/create-github-app-token@v3.0.0-beta.2": { + "actions/create-github-app-token@v3.0.0-beta.4": { "repo": "actions/create-github-app-token", - "version": "v3.0.0-beta.2", - "sha": "bf559f85448f9380bcfa2899dbdc01eb5b37be3a" + "version": "v3.0.0-beta.4", + "sha": "a7f885bf4560200d03183ed941cb6fb072e4b343" }, "actions/download-artifact@v8.0.1": { "repo": "actions/download-artifact", @@ -70,11 +60,6 @@ "version": "v5.2.0", "sha": "be666c2fcd27ec809703dec50e508c2fdc7f6654" }, - "actions/setup-node@v4": { - "repo": "actions/setup-node", - "version": "v4", - "sha": "49933ea5288caeca8642d1e84afbd3f7d6820020" - }, "actions/setup-node@v6.3.0": { "repo": "actions/setup-node", "version": "v6.3.0", @@ -95,10 +80,10 @@ "version": "v0.23.1", "sha": "57aae528053a48a3f6235f2d9461b05fbcb7366d" }, - "astral-sh/setup-uv@v7.4.0": { + "astral-sh/setup-uv@v7.5.0": { "repo": "astral-sh/setup-uv", - "version": "v7.4.0", - "sha": "6ee6290f1cbc4156c0bdd66691b2c144ef8df19a" + "version": "v7.5.0", + "sha": "e06108dd0aef18192324c70427afc47652e63a82" }, "cli/gh-extension-precompile@v2.1.0": { "repo": "cli/gh-extension-precompile", diff --git a/.github/workflows/daily-doc-healer.lock.yml b/.github/workflows/daily-doc-healer.lock.yml index a394c9fd58e..eac4e7de53d 100644 --- a/.github/workflows/daily-doc-healer.lock.yml +++ b/.github/workflows/daily-doc-healer.lock.yml @@ -28,7 +28,7 @@ # - shared/mcp/qmd-docs.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"3281ba1de9c3a7453fd4e74eb14dcbf06ea83c84ac6709d0dfe024f818dd4a3d","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"cca82b9f3f65097ae34fb56a8ec368b561783febb6499b621d93193dded353bd","strict":true} name: "Daily Documentation Healer" "on": @@ -306,7 +306,7 @@ jobs: - name: Install QMD run: npm install -g @tobilu/qmd - name: Restore QMD index cache - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} path: ~/.cache/qmd diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index bf219fdb3dc..de5f18773b4 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -27,7 +27,7 @@ # Imports: # - shared/mcp/qmd-docs.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"029b4f22798fe4cdef702904b87b929499b2e7faab3fbd4c75fc2d2aa6905d03","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"95f0990cf098c3e39dd22cddc710f93b333bacb0505d40a87f6d27a501e0360f","strict":true} name: "Daily Documentation Updater" "on": @@ -302,7 +302,7 @@ jobs: - name: Install QMD run: npm install -g @tobilu/qmd - name: Restore QMD index cache - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} path: ~/.cache/qmd diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index e17d1f2d752..7f37747fcce 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -29,7 +29,7 @@ # - shared/mcp/serena-go.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"04ab9b346523011c327645a8bfdd1d836c37666cd30bb4509fc9a1973c797275","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"732634914b81e13a23826ef3a151b3fe6c07421c3dd0a1f8f3f4d9b399062031","strict":true} name: "Developer Documentation Consolidator" "on": @@ -325,7 +325,7 @@ jobs: - name: Install QMD run: npm install -g @tobilu/qmd - name: Restore QMD index cache - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} path: ~/.cache/qmd diff --git a/.github/workflows/dictation-prompt.lock.yml b/.github/workflows/dictation-prompt.lock.yml index 3b4f5f33b4e..848c7ef9dec 100644 --- a/.github/workflows/dictation-prompt.lock.yml +++ b/.github/workflows/dictation-prompt.lock.yml @@ -28,7 +28,7 @@ # - shared/mcp/qmd-docs.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"06d87876523bb7d9b4f99d72c34ff0965474e7b4ca8f7e657a276596b119b594","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"72d1d25bc156ccebfac8eee35d8c93d54f73566cf905ede4ad9ef2839d7e9d7c","strict":true} name: "Dictation Prompt Generator" "on": @@ -293,7 +293,7 @@ jobs: - name: Install QMD run: npm install -g @tobilu/qmd - name: Restore QMD index cache - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} path: ~/.cache/qmd diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml index 1f599ea6e23..b7d09fd3764 100644 --- a/.github/workflows/glossary-maintainer.lock.yml +++ b/.github/workflows/glossary-maintainer.lock.yml @@ -30,7 +30,7 @@ # - shared/mcp/qmd-docs.md # - shared/mcp/serena-go.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"474cc67d7db6b1b65413c80bc89f19fabc09cb1b40bf4a639e4826ad1e628578","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"7f2246e2c4bb32e00a75bf67d0b216159280d626fd0480644002345bbc1b6ba7","strict":true} name: "Glossary Maintainer" "on": @@ -339,7 +339,7 @@ jobs: - name: Install QMD run: npm install -g @tobilu/qmd - name: Restore QMD index cache - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} path: ~/.cache/qmd diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index b924918d3f3..6aacc72bf4a 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -390,7 +390,7 @@ jobs: with: python-version: '3.12' - name: Setup uv - uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v7.4.0 + uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 - name: Create gh-aw temp directory run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh # Cache memory file share configuration from frontmatter processed below diff --git a/.github/workflows/qmd-docs-indexer.yml b/.github/workflows/qmd-docs-indexer.yml index 8aacb8c912a..f29df50dca4 100644 --- a/.github/workflows/qmd-docs-indexer.yml +++ b/.github/workflows/qmd-docs-indexer.yml @@ -23,12 +23,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@v6.0.2 with: persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + uses: actions/setup-node@v6.3.0 with: node-version: "24" @@ -37,7 +37,7 @@ jobs: - name: Restore QMD index cache id: qmd-cache - uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache/restore@v5.0.2 with: path: ~/.cache/qmd key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} @@ -77,7 +77,7 @@ jobs: - name: Save QMD index cache if: steps.qmd-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache/save@v5.0.2 with: path: ~/.cache/qmd key: ${{ steps.qmd-cache.outputs.cache-primary-key }} diff --git a/.github/workflows/shared/mcp/qmd-docs.md b/.github/workflows/shared/mcp/qmd-docs.md index eaf761b52c5..27b0bf82ff4 100644 --- a/.github/workflows/shared/mcp/qmd-docs.md +++ b/.github/workflows/shared/mcp/qmd-docs.md @@ -24,15 +24,18 @@ mcp-servers: - multi_get - status +resources: + - .github/workflows/qmd-docs-indexer.yml + steps: - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6.3.0 with: node-version: "24" - name: Install QMD run: npm install -g @tobilu/qmd - name: Restore QMD index cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5.0.3 with: path: ~/.cache/qmd key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 2b5987846da..8d2d7f95f33 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -29,7 +29,7 @@ # - ../skills/documentation/SKILL.md # - shared/mcp/qmd-docs.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"ff83f6edccccc4e75b6305b5374e23aba06ee6be27e5dd9fd6cb4cc33839f052","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"0ac7791428d6c94f2d22b6c851aafc12a712559a3b70350b0832fce5c56805b8","strict":true} name: "Rebuild the documentation after making changes" "on": @@ -343,7 +343,7 @@ jobs: - name: Install QMD run: npm install -g @tobilu/qmd - name: Restore QMD index cache - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} path: ~/.cache/qmd diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 7f91ea04f35..17f5bbef129 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -29,7 +29,7 @@ # - shared/mcp/qmd-docs.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"f80a0d009fbcd00205e16fe0c698bd48fd56c0dfbf5a0130608b7e7e99b7adc3","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"ed026f4f498e95b8d244af2d6ad2feb3a1d2c4d00f2aa3ab3eb2f92d5249605d","strict":true} name: "Documentation Unbloat" "on": @@ -345,7 +345,7 @@ jobs: - name: Install QMD run: npm install -g @tobilu/qmd - name: Restore QMD index cache - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: key: qmd-docs-${{ hashFiles('docs/src/content/docs/**', '.github/agents/**', '.github/aw/**') }} path: ~/.cache/qmd diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index f1edc143c0a..9f3853e0a7e 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -558,7 +558,8 @@ on: # Conditionally skip workflow execution when a GitHub search query has matches. # Can be a string (query only, implies max=1) or an object with 'query', optional - # 'max', 'scope', 'github-token', and 'github-app' fields. + # 'max', and 'scope' fields. Use top-level on.github-token or on.github-app for + # custom authentication. # (optional) # This field supports multiple formats (oneOf): @@ -569,7 +570,8 @@ on: skip-if-match: "example-value" # Option 2: Skip-if-match configuration object with query, maximum match count, - # and optional scope/auth settings + # and optional scope. For custom authentication use the top-level on.github-token + # or on.github-app fields. skip-if-match: # GitHub search query string to check before running workflow. Query is # automatically scoped to the current repository. @@ -592,34 +594,10 @@ on: # (optional) scope: "none" - # Custom GitHub token to use for the search API call. Useful for cross-repo or - # org-wide searches that require additional permissions. - # (optional) - github-token: "${{ secrets.GITHUB_TOKEN }}" - - # GitHub App configuration for minting a token used for the search API call. - # (optional) - github-app: - # GitHub App ID (e.g., '${{ secrets.APP_ID }}'). Required. - app-id: "example-value" - - # GitHub App private key (e.g., '${{ secrets.APP_PRIVATE_KEY }}'). Required. - private-key: "example-value" - - # Optional owner of the GitHub App installation (defaults to current repository - # owner). - # (optional) - owner: "example-value" - - # Optional list of repositories to grant access to. Use ['*'] for org-wide access. - # (optional) - repositories: [] - # Array of strings - # Conditionally skip workflow execution when a GitHub search query has no matches # (or fewer than minimum). Can be a string (query only, implies min=1) or an - # object with 'query', optional 'min', 'scope', 'github-token', and 'github-app' - # fields. + # object with 'query', optional 'min', and 'scope' fields. Use top-level + # on.github-token or on.github-app for custom authentication. # (optional) # This field supports multiple formats (oneOf): @@ -630,7 +608,8 @@ on: skip-if-no-match: "example-value" # Option 2: Skip-if-no-match configuration object with query, minimum match count, - # and optional scope/auth settings + # and optional scope. For custom authentication use the top-level on.github-token + # or on.github-app fields. skip-if-no-match: # GitHub search query string to check before running workflow. Query is # automatically scoped to the current repository. @@ -646,30 +625,6 @@ on: # (optional) scope: "none" - # Custom GitHub token to use for the search API call. Useful for cross-repo or - # org-wide searches that require additional permissions. - # (optional) - github-token: "${{ secrets.GITHUB_TOKEN }}" - - # GitHub App configuration for minting a token used for the search API call. - # (optional) - github-app: - # GitHub App ID (e.g., '${{ secrets.APP_ID }}'). Required. - app-id: "example-value" - - # GitHub App private key (e.g., '${{ secrets.APP_PRIVATE_KEY }}'). Required. - private-key: "example-value" - - # Optional owner of the GitHub App installation (defaults to current repository - # owner). - # (optional) - owner: "example-value" - - # Optional list of repositories to grant access to. Use ['*'] for org-wide access. - # (optional) - repositories: [] - # Array of strings - # Skip workflow execution for users with specific repository roles. Useful for # workflows that should only run for external contributors or specific permission # levels. @@ -752,15 +707,17 @@ on: # (optional) status-comment: true - # Custom GitHub token to use for pre-activation reactions and activation status - # comments. When specified, overrides the default GITHUB_TOKEN for these - # operations. + # Custom GitHub token for pre-activation reactions, activation status comments, + # and skip-if search queries. When specified, overrides the default GITHUB_TOKEN + # for these operations. # (optional) github-token: "${{ secrets.GITHUB_TOKEN }}" - # GitHub App configuration for minting a token used in pre-activation reactions - # and activation status comments. When configured, a GitHub App installation - # access token is minted and used instead of the default GITHUB_TOKEN. + # GitHub App configuration for minting a token used in pre-activation reactions, + # activation status comments, and skip-if search queries. When configured, a + # single GitHub App installation access token is minted and shared across all + # these operations instead of using the default GITHUB_TOKEN. Can be defined in a + # shared agentic workflow and inherited by importing workflows. # (optional) github-app: # GitHub App ID (e.g., '${{ vars.APP_ID }}'). Required to mint a GitHub App token. @@ -3246,6 +3203,14 @@ safe-outputs: allowed-files: [] # Array of strings + # When true, the random salt suffix is not appended to the agent-specified branch + # name. Invalid characters are still replaced for security, and casing is always + # preserved regardless of this setting. Useful when the target repository enforces + # branch naming conventions (e.g. Jira keys in uppercase such as + # 'bugfix/BR-329-red'). Defaults to false. + # (optional) + preserve-branch-name: true + # Option 2: Enable pull request creation with default configuration create-pull-request: null diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index eea336cbd3c..2800250c447 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -35,10 +35,10 @@ The `on:` section uses standard GitHub Actions syntax to define workflow trigger - `forks:` - Configure fork filtering for pull_request triggers - `skip-roles:` - Skip workflow execution for specific repository roles - `skip-bots:` - Skip workflow execution for specific GitHub actors -- `skip-if-match:` - Skip execution when a search query has matches (supports `scope`, `github-token`, `github-app`) -- `skip-if-no-match:` - Skip execution when a search query has no matches (supports `scope`, `github-token`, `github-app`) -- `github-token:` - Custom token for activation job reactions and status comments -- `github-app:` - GitHub App for minting a short-lived token used by the activation job +- `skip-if-match:` - Skip execution when a search query has matches (supports `scope: none`; use top-level `on.github-token` / `on.github-app` for custom auth) +- `skip-if-no-match:` - Skip execution when a search query has no matches (supports `scope: none`; use top-level `on.github-token` / `on.github-app` for custom auth) +- `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 See [Trigger Events](/gh-aw/reference/triggers/) for complete documentation. @@ -419,7 +419,7 @@ features: #### Action Mode (`features.action-mode`) -Controls how the workflow compiler generates custom action references in compiled workflows. Can be set to `"dev"`, `"release"`, or `"script"`. +Controls how the workflow compiler generates custom action references in compiled workflows. Can be set to `"dev"`, `"release"`, `"action"`, or `"script"`. ```yaml wrap features: @@ -430,7 +430,9 @@ features: - **`dev`** (default): References custom actions using local paths (e.g., `uses: ./actions/setup`). Best for development and testing workflows in the gh-aw repository. -- **`release`**: References custom actions using SHA-pinned remote paths (e.g., `uses: github/gh-aw/actions/setup@sha`). Used for production workflows with version pinning. +- **`release`**: References custom actions using SHA-pinned remote paths within `github/gh-aw` (e.g., `uses: github/gh-aw/actions/setup@sha`). Used for production workflows with version pinning. + +- **`action`**: References custom actions from the `github/gh-aw-actions` external repository at the same release version (e.g., `uses: github/gh-aw-actions/setup@sha`). Uses SHA pinning when available, with a version-tag fallback. Use this when deploying workflows from the `github/gh-aw-actions` distribution repository. - **`script`**: Generates direct shell script calls instead of using GitHub Actions `uses:` syntax. The compiler: 1. Checks out the `github/gh-aw` repository's `actions` folder to `/tmp/gh-aw/actions-source` diff --git a/docs/src/content/docs/reference/safe-outputs-pull-requests.md b/docs/src/content/docs/reference/safe-outputs-pull-requests.md index 1c3b4961b74..e76e4b32540 100644 --- a/docs/src/content/docs/reference/safe-outputs-pull-requests.md +++ b/docs/src/content/docs/reference/safe-outputs-pull-requests.md @@ -29,6 +29,7 @@ safe-outputs: allowed-repos: ["org/repo1", "org/repo2"] # additional allowed repositories base-branch: "vnext" # target branch for PR (default: github.base_ref || github.ref_name) fallback-as-issue: false # disable issue fallback (default: true) + preserve-branch-name: true # omit random salt suffix from branch name (default: false) github-token: ${{ secrets.SOME_CUSTOM_TOKEN }} # optional custom token for permissions github-token-for-extra-empty-commit: ${{ secrets.CI_TOKEN }} # optional token to push empty commit triggering CI protected-files: fallback-to-issue # push branch, create review issue if protected files modified @@ -47,6 +48,8 @@ safe-outputs: github-token: ${{ secrets.SOME_CUSTOM_TOKEN }} # optional custom token for permissions ``` +The `preserve-branch-name` field, when set to `true`, omits the random hex salt suffix that is normally appended to the agent-specified branch name. This is useful when the target repository enforces branch naming conventions such as Jira keys in uppercase (e.g., `bugfix/BR-329-red` instead of `bugfix/br-329-red-cde2a954`). Invalid characters are always replaced for security, and casing is always preserved regardless of this setting. Defaults to `false`. + The `draft` field is a **configuration policy**, not a default. Whatever value is set in the workflow frontmatter is always used — the agent cannot override it at runtime. PR creation may fail if "Allow GitHub Actions to create and approve pull requests" is disabled in Organization Settings. By default (`fallback-as-issue: true`), fallback creates an issue with branch link and requires `issues: write` permission. Set `fallback-as-issue: false` to disable fallback and only require `contents: write` + `pull-requests: write`. diff --git a/docs/src/content/docs/reference/triggers.md b/docs/src/content/docs/reference/triggers.md index 3b0fbb03551..b1fead53ad6 100644 --- a/docs/src/content/docs/reference/triggers.md +++ b/docs/src/content/docs/reference/triggers.md @@ -320,7 +320,7 @@ The reaction is added to the triggering item. For issues/PRs, a comment with the ### Activation Token (`on.github-token:`, `on.github-app:`) -Configure a custom GitHub token or GitHub App for the activation job. The activation job posts the initial reaction and status comment on the triggering item. By default it uses the workflow's `GITHUB_TOKEN`. +Configure a custom GitHub token or GitHub App for the activation job **and all skip-if search checks**. The activation job posts the initial reaction and status comment on the triggering item, and skip-if checks use the same token to query the GitHub Search API. By default all of these operations use the workflow's `GITHUB_TOKEN`. Use `github-token:` to supply a PAT or custom token: @@ -344,10 +344,34 @@ on: private-key: ${{ secrets.APP_KEY }} ``` -The `github-app` object accepts the same fields as the GitHub App configuration used elsewhere in the framework (`app-id`, `private-key`, and optionally `owner` and `repositories`). The token is minted once for the activation job and covers both the reaction step and the status comment step. +The `github-app` object accepts the same fields as the GitHub App configuration used elsewhere in the framework (`app-id`, `private-key`, and optionally `owner` and `repositories`). The token is minted once in the pre-activation job and is shared across the reaction step, the status comment step, and any skip-if search steps. + +Both `github-token` and `github-app` can be defined in a **shared agentic workflow** and will be automatically inherited by any workflow that imports it (first-wins strategy). This means a central CentralRepoOps shared workflow can define the app config once and all importing workflows benefit automatically: + +```yaml wrap +# shared-ops.md - define app config once +on: + workflow_call: + github-app: + app-id: ${{ secrets.ORG_APP_ID }} + private-key: ${{ secrets.ORG_APP_PRIVATE_KEY }} + owner: myorg +``` + +```yaml wrap +# any-workflow.md - inherits github-app from the import +imports: + - .github/workflows/shared/shared-ops.md +on: + schedule: + - cron: "*/30 * * * *" + skip-if-no-match: + query: "org:myorg label:agent-fix is:issue is:open" + scope: none +``` > [!NOTE] -> `github-token` and `github-app` affect only the activation job. For the agent job, configure tokens via `tools.github.github-token`/`tools.github.github-app` or `safe-outputs.github-token`/`safe-outputs.github-app`. See [Authentication](/gh-aw/reference/auth/) for a full overview. +> `github-token` and `github-app` affect only the activation job (reactions, status comments, and skip-if searches). For the agent job, configure tokens via `tools.github.github-token`/`tools.github.github-app` or `safe-outputs.github-token`/`safe-outputs.github-app`. See [Authentication](/gh-aw/reference/auth/) for a full overview. ### Stop After Configuration (`stop-after:`) @@ -392,7 +416,7 @@ A pre-activation check runs the search query against the current repository. If #### Cross-Repo and Org-Wide Queries -By default the query is scoped to the current repository. Use `scope: none` to disable this and search across an entire org. Combine with `github-token` or `github-app` to provide a token with the required permissions: +By default the query is scoped to the current repository. Use `scope: none` to disable this and search across an entire org. For cross-repo or org-wide searches that require elevated permissions, configure `github-token` or `github-app` at the top-level `on:` section — the same token is shared across all skip-if checks and the activation job: ```yaml wrap on: @@ -401,17 +425,17 @@ on: skip-if-match: query: "org:myorg label:ops:in-progress is:issue is:open" scope: none - github-app: - app-id: ${{ secrets.WORKFLOW_APP_ID }} - private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} - owner: myorg + github-app: + app-id: ${{ secrets.WORKFLOW_APP_ID }} + private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} + owner: myorg ``` -| Field | Description | -|-------|-------------| -| `scope: none` | Disables the automatic `repo:owner/repo` qualifier | -| `github-token` | Custom PAT or token for the search step (e.g. `${{ secrets.CROSS_ORG_TOKEN }}`) | -| `github-app` | Mints a short-lived installation token; requires `app-id` and `private-key` | +| Field | Location | Description | +|-------|----------|-------------| +| `scope: none` | inside `skip-if-match` | Disables the automatic `repo:owner/repo` qualifier | +| `github-token` | top-level `on:` | Custom PAT or token for all skip-if searches (e.g. `${{ secrets.CROSS_ORG_TOKEN }}`) | +| `github-app` | top-level `on:` | Mints a short-lived installation token shared across all skip-if steps; requires `app-id` and `private-key` | `github-token` and `github-app` are mutually exclusive. String shorthand always uses the default `GITHUB_TOKEN` scoped to the current repository. @@ -434,7 +458,7 @@ on: A pre-activation check runs the search query against the current repository. If matches are below the threshold (default `min: 1`), the workflow is skipped. Can be combined with `skip-if-match` for complex conditions. -The same `scope`, `github-token`, and `github-app` fields available on `skip-if-match` work identically here: +The same `scope: none` field available on `skip-if-match` works identically here. Authentication (`github-token` / `github-app`) is configured at the top-level `on:` section and is shared across all skip-if checks — a single mint step is emitted for both: ```yaml wrap on: @@ -443,10 +467,10 @@ on: skip-if-no-match: query: "org:myorg label:agent-fix -label:ops:agentic is:issue is:open" scope: none - github-app: - app-id: ${{ secrets.WORKFLOW_APP_ID }} - private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} - owner: myorg + github-app: + app-id: ${{ secrets.WORKFLOW_APP_ID }} + private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} + owner: myorg ``` ## Trigger Shorthands diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index a98a9f788a1..e4dac974767 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -10,21 +10,11 @@ "version": "v4.1.0", "sha": "a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32" }, - "actions/cache/restore@v4": { - "repo": "actions/cache/restore", - "version": "v4", - "sha": "0057852bfaa89a56745cba8c7296529d2fc39830" - }, "actions/cache/restore@v5.0.3": { "repo": "actions/cache/restore", "version": "v5.0.3", "sha": "cdf6c1fa76f9f475f3d7449005a359c84ca0f306" }, - "actions/cache/save@v4": { - "repo": "actions/cache/save", - "version": "v4", - "sha": "0057852bfaa89a56745cba8c7296529d2fc39830" - }, "actions/cache/save@v5.0.3": { "repo": "actions/cache/save", "version": "v5.0.3", @@ -40,10 +30,10 @@ "version": "v6.0.2", "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd" }, - "actions/create-github-app-token@v3.0.0-beta.2": { + "actions/create-github-app-token@v3.0.0-beta.4": { "repo": "actions/create-github-app-token", - "version": "v3.0.0-beta.2", - "sha": "bf559f85448f9380bcfa2899dbdc01eb5b37be3a" + "version": "v3.0.0-beta.4", + "sha": "a7f885bf4560200d03183ed941cb6fb072e4b343" }, "actions/download-artifact@v8.0.1": { "repo": "actions/download-artifact", @@ -70,11 +60,6 @@ "version": "v5.2.0", "sha": "be666c2fcd27ec809703dec50e508c2fdc7f6654" }, - "actions/setup-node@v4": { - "repo": "actions/setup-node", - "version": "v4", - "sha": "49933ea5288caeca8642d1e84afbd3f7d6820020" - }, "actions/setup-node@v6.3.0": { "repo": "actions/setup-node", "version": "v6.3.0", @@ -95,10 +80,10 @@ "version": "v0.23.1", "sha": "57aae528053a48a3f6235f2d9461b05fbcb7366d" }, - "astral-sh/setup-uv@v7.4.0": { + "astral-sh/setup-uv@v7.5.0": { "repo": "astral-sh/setup-uv", - "version": "v7.4.0", - "sha": "6ee6290f1cbc4156c0bdd66691b2c144ef8df19a" + "version": "v7.5.0", + "sha": "e06108dd0aef18192324c70427afc47652e63a82" }, "cli/gh-extension-precompile@v2.1.0": { "repo": "cli/gh-extension-precompile", From 4219e43e05f28c1c1445f8b52c99d2b04d269159 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:40:01 +0000 Subject: [PATCH 8/8] feat: add shared activation-app.md workflow and import in skip-if-match workflows Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../breaking-change-checker.lock.yml | 6 +- .github/workflows/breaking-change-checker.md | 1 + .../workflows/code-scanning-fixer.lock.yml | 9 ++- .github/workflows/code-scanning-fixer.md | 2 + .github/workflows/code-simplifier.lock.yml | 6 +- .github/workflows/code-simplifier.md | 1 + .github/workflows/daily-file-diet.lock.yml | 6 +- .github/workflows/daily-file-diet.md | 1 + .../daily-rendering-scripts-verifier.lock.yml | 6 +- .../daily-rendering-scripts-verifier.md | 1 + .../daily-safe-output-optimizer.lock.yml | 6 +- .../workflows/daily-safe-output-optimizer.md | 1 + .../daily-testify-uber-super-expert.lock.yml | 6 +- .../daily-testify-uber-super-expert.md | 1 + .github/workflows/dead-code-remover.lock.yml | 9 ++- .github/workflows/dead-code-remover.md | 2 + .github/workflows/issue-monster.lock.yml | 9 ++- .github/workflows/issue-monster.md | 3 + .github/workflows/shared/activation-app.md | 62 +++++++++++++++++++ .../workflows/slide-deck-maintainer.lock.yml | 9 ++- .github/workflows/slide-deck-maintainer.md | 2 + .../workflows/ubuntu-image-analyzer.lock.yml | 9 ++- .github/workflows/ubuntu-image-analyzer.md | 2 + 23 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/shared/activation-app.md diff --git a/.github/workflows/breaking-change-checker.lock.yml b/.github/workflows/breaking-change-checker.lock.yml index 42a24242394..9404c3fc576 100644 --- a/.github/workflows/breaking-change-checker.lock.yml +++ b/.github/workflows/breaking-change-checker.lock.yml @@ -25,9 +25,10 @@ # # Resolved workflow manifest: # Imports: +# - shared/activation-app.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"298055babdc6d453ead986983230edf4cbb2c74f82c5a110850af9b8a64e89c2","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"d30dc29a4e3303a626094750fe8c3efecfbbe25ab6b39a475ab3217871047ed4","strict":true} name: "Breaking Change Checker" "on": @@ -167,6 +168,9 @@ jobs: GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/activation-app.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' {{#runtime-import .github/workflows/shared/reporting.md}} GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' diff --git a/.github/workflows/breaking-change-checker.md b/.github/workflows/breaking-change-checker.md index bc272600d1d..61843fca16a 100644 --- a/.github/workflows/breaking-change-checker.md +++ b/.github/workflows/breaking-change-checker.md @@ -34,6 +34,7 @@ safe-outputs: run-failure: "🔬 Analysis interrupted! [{workflow_name}]({run_url}) {status}. Compatibility status unknown..." timeout-minutes: 10 imports: + - shared/activation-app.md - shared/reporting.md features: copilot-requests: true diff --git a/.github/workflows/code-scanning-fixer.lock.yml b/.github/workflows/code-scanning-fixer.lock.yml index 9005ab4f233..a931a29de6d 100644 --- a/.github/workflows/code-scanning-fixer.lock.yml +++ b/.github/workflows/code-scanning-fixer.lock.yml @@ -23,7 +23,11 @@ # # Automatically fixes code scanning alerts by creating pull requests with remediation # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"356d127ca6b12cd898b6897498aa23822d2a75188b7e7564bc8b833190056a4b","strict":true} +# Resolved workflow manifest: +# Imports: +# - shared/activation-app.md +# +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"c86a8c8936d098ce2da4bf71678186f60eb9aaed93086ab0692137208bd21398","strict":true} name: "Code Scanning Fixer" "on": @@ -166,6 +170,9 @@ jobs: GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/activation-app.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' {{#runtime-import .github/workflows/code-scanning-fixer.md}} GH_AW_PROMPT_EOF } > "$GH_AW_PROMPT" diff --git a/.github/workflows/code-scanning-fixer.md b/.github/workflows/code-scanning-fixer.md index a8deaa3ee53..9291478ae99 100644 --- a/.github/workflows/code-scanning-fixer.md +++ b/.github/workflows/code-scanning-fixer.md @@ -9,6 +9,8 @@ permissions: pull-requests: read security-events: read engine: copilot +imports: + - shared/activation-app.md tools: github: github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/code-simplifier.lock.yml b/.github/workflows/code-simplifier.lock.yml index 09c37ead1a5..6d4d0b0fff2 100644 --- a/.github/workflows/code-simplifier.lock.yml +++ b/.github/workflows/code-simplifier.lock.yml @@ -25,9 +25,10 @@ # # Resolved workflow manifest: # Imports: +# - shared/activation-app.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"6ba60c66818393095f34e20338d7b05c7e2cf5f3cc398105e210b2d12622b7fa","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"46fcb0db5d0eb563835594c8aa08fa38761aa25be0bc7d3dae4293d504a5754e","strict":true} name: "Code Simplifier" "on": @@ -177,6 +178,9 @@ jobs: GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/activation-app.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' {{#runtime-import .github/workflows/shared/reporting.md}} GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' diff --git a/.github/workflows/code-simplifier.md b/.github/workflows/code-simplifier.md index 2adc3f3ce82..cfe59a43a1a 100644 --- a/.github/workflows/code-simplifier.md +++ b/.github/workflows/code-simplifier.md @@ -13,6 +13,7 @@ permissions: tracker-id: code-simplifier imports: + - shared/activation-app.md - shared/reporting.md safe-outputs: diff --git a/.github/workflows/daily-file-diet.lock.yml b/.github/workflows/daily-file-diet.lock.yml index a42dd95fb43..af2f611797d 100644 --- a/.github/workflows/daily-file-diet.lock.yml +++ b/.github/workflows/daily-file-diet.lock.yml @@ -25,11 +25,12 @@ # # Resolved workflow manifest: # Imports: +# - shared/activation-app.md # - shared/mcp/serena-go.md # - shared/reporting.md # - shared/safe-output-app.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"70afbdd1e3c59b27fde620365bdd2f0f14030571674bb7ed196cb3c56bf34979","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"2bf96ef660042f766ad92ab43f4a2b2e74bc1b62ad74c95679bed28e6fe4ce75","strict":true} name: "Daily File Diet" "on": @@ -169,6 +170,9 @@ jobs: GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/activation-app.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' {{#runtime-import .github/workflows/shared/reporting.md}} GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' diff --git a/.github/workflows/daily-file-diet.md b/.github/workflows/daily-file-diet.md index 85c9164df74..50771201019 100644 --- a/.github/workflows/daily-file-diet.md +++ b/.github/workflows/daily-file-diet.md @@ -16,6 +16,7 @@ tracker-id: daily-file-diet engine: copilot imports: + - shared/activation-app.md - shared/reporting.md - shared/safe-output-app.md - shared/mcp/serena-go.md diff --git a/.github/workflows/daily-rendering-scripts-verifier.lock.yml b/.github/workflows/daily-rendering-scripts-verifier.lock.yml index 8a2fa00a2d3..dded9b85f1c 100644 --- a/.github/workflows/daily-rendering-scripts-verifier.lock.yml +++ b/.github/workflows/daily-rendering-scripts-verifier.lock.yml @@ -25,9 +25,10 @@ # # Resolved workflow manifest: # Imports: +# - shared/activation-app.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"381a6c01f344b342056507311653cad014f3159ed676431b41d1357e1c9fd3be","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"a7011f09224b79475d25a6867214db655a751d141f5c99b9344f89befa74976a","strict":true} name: "Daily Rendering Scripts Verifier" "on": @@ -178,6 +179,9 @@ jobs: GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/activation-app.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' {{#runtime-import .github/workflows/shared/reporting.md}} GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' diff --git a/.github/workflows/daily-rendering-scripts-verifier.md b/.github/workflows/daily-rendering-scripts-verifier.md index 7f3e69538e1..f87ed1a5127 100644 --- a/.github/workflows/daily-rendering-scripts-verifier.md +++ b/.github/workflows/daily-rendering-scripts-verifier.md @@ -45,6 +45,7 @@ safe-outputs: timeout-minutes: 30 imports: + - shared/activation-app.md - shared/reporting.md --- diff --git a/.github/workflows/daily-safe-output-optimizer.lock.yml b/.github/workflows/daily-safe-output-optimizer.lock.yml index c63a241358f..886fa8b9dbe 100644 --- a/.github/workflows/daily-safe-output-optimizer.lock.yml +++ b/.github/workflows/daily-safe-output-optimizer.lock.yml @@ -25,10 +25,11 @@ # # Resolved workflow manifest: # Imports: +# - shared/activation-app.md # - shared/jqschema.md # - shared/reporting.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"34459ba98cad0356b507423708958b0455022e2797d063650cc338a06efe8309","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"4dfd985f4be0de9f2d4f273608bccc24442362bc90d685918b77fe649be45793","strict":true} name: "Daily Safe Output Tool Optimizer" "on": @@ -176,6 +177,9 @@ jobs: GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/activation-app.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' {{#runtime-import .github/workflows/shared/jqschema.md}} GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' diff --git a/.github/workflows/daily-safe-output-optimizer.md b/.github/workflows/daily-safe-output-optimizer.md index c56c918c952..f25f9963c82 100644 --- a/.github/workflows/daily-safe-output-optimizer.md +++ b/.github/workflows/daily-safe-output-optimizer.md @@ -35,6 +35,7 @@ timeout-minutes: 30 strict: true imports: + - shared/activation-app.md - shared/jqschema.md - shared/reporting.md --- diff --git a/.github/workflows/daily-testify-uber-super-expert.lock.yml b/.github/workflows/daily-testify-uber-super-expert.lock.yml index 28a56c82497..56d6975bf21 100644 --- a/.github/workflows/daily-testify-uber-super-expert.lock.yml +++ b/.github/workflows/daily-testify-uber-super-expert.lock.yml @@ -25,11 +25,12 @@ # # Resolved workflow manifest: # Imports: +# - shared/activation-app.md # - shared/mcp/serena-go.md # - shared/reporting.md # - shared/safe-output-app.md # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"0935d96e21c4e3fcee9b2a941f983c92b12d0ea27c07d196b6d43a60eb7e482f","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"395963d5f4f9f6fd0b3bcfde48ac88bd0e446e310cb08788e1a59c20bf43ff44","strict":true} name: "Daily Testify Uber Super Expert" "on": @@ -172,6 +173,9 @@ jobs: GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/activation-app.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' {{#runtime-import .github/workflows/shared/reporting.md}} GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' diff --git a/.github/workflows/daily-testify-uber-super-expert.md b/.github/workflows/daily-testify-uber-super-expert.md index 9397631cdea..b3da4c376a1 100644 --- a/.github/workflows/daily-testify-uber-super-expert.md +++ b/.github/workflows/daily-testify-uber-super-expert.md @@ -15,6 +15,7 @@ tracker-id: daily-testify-uber-super-expert engine: copilot imports: + - shared/activation-app.md - shared/reporting.md - shared/safe-output-app.md - shared/mcp/serena-go.md diff --git a/.github/workflows/dead-code-remover.lock.yml b/.github/workflows/dead-code-remover.lock.yml index 1d0907a1592..534a9f3678a 100644 --- a/.github/workflows/dead-code-remover.lock.yml +++ b/.github/workflows/dead-code-remover.lock.yml @@ -23,7 +23,11 @@ # # Daily dead code assessment and removal — identifies unreachable Go functions using static analysis and creates a PR to remove a batch each day # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"fa086fa48d23515e37fdf92ef825e11e376fcedf5ffe99f7e5d9ce5164deb071","strict":true} +# Resolved workflow manifest: +# Imports: +# - shared/activation-app.md +# +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"00f650dd3f0d6abf021fc2e0e683c25aa13ee283334637072f1d6def43026a6a","strict":true} name: "Dead Code Removal Agent" "on": @@ -169,6 +173,9 @@ jobs: GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/activation-app.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' {{#runtime-import .github/workflows/dead-code-remover.md}} GH_AW_PROMPT_EOF } > "$GH_AW_PROMPT" diff --git a/.github/workflows/dead-code-remover.md b/.github/workflows/dead-code-remover.md index 725588a9b18..871c47015ba 100644 --- a/.github/workflows/dead-code-remover.md +++ b/.github/workflows/dead-code-remover.md @@ -9,6 +9,8 @@ permissions: pull-requests: read issues: read engine: copilot +imports: + - shared/activation-app.md network: allowed: - defaults diff --git a/.github/workflows/issue-monster.lock.yml b/.github/workflows/issue-monster.lock.yml index 158b97b89e2..c92c3c50008 100644 --- a/.github/workflows/issue-monster.lock.yml +++ b/.github/workflows/issue-monster.lock.yml @@ -23,7 +23,11 @@ # # The Cookie Monster of issues - assigns issues to Copilot coding agent one at a time # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"2ababe1bdc7d094c401b9491ec0f362786fabd31e1e5fc1025db33681912136a","strict":true} +# Resolved workflow manifest: +# Imports: +# - shared/activation-app.md +# +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"429c9553c1b8706ab97bd48fe931129d4fdfe815c78402277377062529800d8a","strict":true} name: "Issue Monster" "on": @@ -181,6 +185,9 @@ jobs: GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/activation-app.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' {{#runtime-import .github/workflows/issue-monster.md}} GH_AW_PROMPT_EOF } > "$GH_AW_PROMPT" diff --git a/.github/workflows/issue-monster.md b/.github/workflows/issue-monster.md index 0615f917266..da0c5212fcb 100644 --- a/.github/workflows/issue-monster.md +++ b/.github/workflows/issue-monster.md @@ -18,6 +18,9 @@ engine: id: copilot model: gpt-5.1-codex-mini +imports: + - shared/activation-app.md + timeout-minutes: 30 tools: diff --git a/.github/workflows/shared/activation-app.md b/.github/workflows/shared/activation-app.md new file mode 100644 index 00000000000..7f3d19321a5 --- /dev/null +++ b/.github/workflows/shared/activation-app.md @@ -0,0 +1,62 @@ +--- +#on: +# github-app: +# app-id: ${{ vars.APP_ID }} +# private-key: ${{ secrets.APP_PRIVATE_KEY }} +--- + + diff --git a/.github/workflows/slide-deck-maintainer.lock.yml b/.github/workflows/slide-deck-maintainer.lock.yml index 86626eacd9b..5323d3dae77 100644 --- a/.github/workflows/slide-deck-maintainer.lock.yml +++ b/.github/workflows/slide-deck-maintainer.lock.yml @@ -23,7 +23,11 @@ # # Maintains the gh-aw slide deck by scanning repository content and detecting layout issues using Playwright # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"a11988b356c426b5f0adcc819e558e7758398d328b7f90aa5173a4bd639bfdc9","strict":true} +# Resolved workflow manifest: +# Imports: +# - shared/activation-app.md +# +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"417e7c5f2ba13e6dcc483c5e726130f0b838707263a6dd784ad55a3fb80f1e83","strict":true} name: "Slide Deck Maintainer" "on": @@ -181,6 +185,9 @@ jobs: GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/activation-app.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' {{#runtime-import .github/workflows/slide-deck-maintainer.md}} GH_AW_PROMPT_EOF } > "$GH_AW_PROMPT" diff --git a/.github/workflows/slide-deck-maintainer.md b/.github/workflows/slide-deck-maintainer.md index 3e5c77a856a..0ce343ff1c1 100644 --- a/.github/workflows/slide-deck-maintainer.md +++ b/.github/workflows/slide-deck-maintainer.md @@ -19,6 +19,8 @@ concurrency: job-discriminator: ${{ inputs.focus || github.run_id }} tracker-id: slide-deck-maintainer engine: copilot +imports: + - shared/activation-app.md timeout-minutes: 45 tools: cache-memory: true diff --git a/.github/workflows/ubuntu-image-analyzer.lock.yml b/.github/workflows/ubuntu-image-analyzer.lock.yml index 3b8bb9bd230..cf49fdd546f 100644 --- a/.github/workflows/ubuntu-image-analyzer.lock.yml +++ b/.github/workflows/ubuntu-image-analyzer.lock.yml @@ -23,7 +23,11 @@ # # Weekly analysis of the default Ubuntu Actions runner image and guidance for creating Docker mimics # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"c193dd6ba034f16860806d18b40a9d2afbe981db46a99a273e4b1f0ab4c7e182","strict":true} +# Resolved workflow manifest: +# Imports: +# - shared/activation-app.md +# +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"262aa2c38c59cd857d3031db4ff424e381cd83850ea0b9170f07e52e5fa75e70","strict":true} name: "Ubuntu Actions Image Analyzer" "on": @@ -173,6 +177,9 @@ jobs: GH_AW_PROMPT_EOF cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/shared/activation-app.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' {{#runtime-import .github/workflows/ubuntu-image-analyzer.md}} GH_AW_PROMPT_EOF } > "$GH_AW_PROMPT" diff --git a/.github/workflows/ubuntu-image-analyzer.md b/.github/workflows/ubuntu-image-analyzer.md index 1adbb010836..600b9509a4a 100644 --- a/.github/workflows/ubuntu-image-analyzer.md +++ b/.github/workflows/ubuntu-image-analyzer.md @@ -14,6 +14,8 @@ permissions: tracker-id: ubuntu-image-analyzer engine: copilot +imports: + - shared/activation-app.md strict: true network: