diff --git a/.github/workflows/breaking-change-checker.lock.yml b/.github/workflows/breaking-change-checker.lock.yml
index 42a2424239..9404c3fc57 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 bc272600d1..61843fca16 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 9005ab4f23..a931a29de6 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 a8deaa3ee5..9291478ae9 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 09c37ead1a..6d4d0b0fff 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 2adc3f3ce8..cfe59a43a1 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 a42dd95fb4..af2f611797 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 85c9164df7..5077120101 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 8a2fa00a2d..dded9b85f1 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 7f3e69538e..f87ed1a512 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 c63a241358..886fa8b9db 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 c56c918c95..f25f9963c8 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 28a56c8249..56d6975bf2 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 9397631cde..b3da4c376a 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 1d0907a159..534a9f3678 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 725588a9b1..871c47015b 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 158b97b89e..c92c3c5000 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 0615f91726..da0c5212fc 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 0000000000..7f3d19321a
--- /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 86626eacd9..5323d3dae7 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 3e5c77a856..0ce343ff1c 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 3b8bb9bd23..cf49fdd546 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 1adbb01083..600b9509a4 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:
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 0000000000..1ede805da6
--- /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 d646d03c93..6385262ac5 100644
--- a/actions/setup/js/check_skip_if_match.cjs
+++ b/actions/setup/js/check_skip_if_match.cjs
@@ -3,11 +3,13 @@
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;
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 +30,11 @@ 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}`);
+ const searchQuery = buildSearchQuery(skipQuery, skipScope);
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 92713d1446..88faea8bf7 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 083ea68eb1..8d08906832 100644
--- a/actions/setup/js/check_skip_if_no_match.cjs
+++ b/actions/setup/js/check_skip_if_no_match.cjs
@@ -3,9 +3,10 @@
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" } = 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 +27,13 @@ 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}`);
+ const searchQuery = buildSearchQuery(skipQuery, skipScope);
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 449010078a..8d89d7bfbe 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/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md
index ba038de022..9f3853e0a7 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,9 @@ 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', and 'scope' fields. Use top-level on.github-token or on.github-app for
+ # custom authentication.
# (optional)
# This field supports multiple formats (oneOf):
@@ -558,7 +569,9 @@ 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. 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.
@@ -576,9 +589,15 @@ 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"
+
# 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', and 'scope' fields. Use top-level
+ # on.github-token or on.github-app for custom authentication.
# (optional)
# This field supports multiple formats (oneOf):
@@ -588,8 +607,9 @@ 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. 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.
@@ -600,6 +620,11 @@ 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"
+
# Skip workflow execution for users with specific repository roles. Useful for
# workflows that should only run for external contributors or specific permission
# levels.
@@ -682,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.
@@ -895,6 +922,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 +933,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 +1335,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 +1451,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 +1463,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 +1721,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 +2086,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 +2099,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 +3182,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
@@ -3085,6 +3307,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)
@@ -3906,19 +4141,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
@@ -4043,6 +4282,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: []
@@ -4497,6 +4748,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 }}').
@@ -4526,6 +4789,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
@@ -4638,6 +4920,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 73c0233cd1..2800250c44 100644
--- a/docs/src/content/docs/reference/frontmatter.md
+++ b/docs/src/content/docs/reference/frontmatter.md
@@ -35,8 +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
-- `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.
diff --git a/docs/src/content/docs/reference/triggers.md b/docs/src/content/docs/reference/triggers.md
index b6450f9b20..b1fead53ad 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:`)
@@ -390,6 +414,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. 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:
+ 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 | 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.
+
### 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 +458,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: 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:
+ 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.
diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go
index 887256f76e..01944ff92b 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 f71addd6d7..4cd08ca98a 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 8b868c5931..5621d44101 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 6742f59f6a..872afbf88a 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 cad4debbfd..7c812bea09 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -1339,13 +1339,18 @@
"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."
}
},
"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. 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' 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', and 'scope' fields. Use top-level on.github-token or on.github-app for custom authentication."
},
"skip-if-no-match": {
"oneOf": [
@@ -1365,13 +1370,18 @@
"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."
}
},
"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. 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' 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', and 'scope' fields. Use top-level on.github-token or on.github-app for custom authentication."
},
"skip-roles": {
"oneOf": [
@@ -1464,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 0e8478e0ec..1da057ff62 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 0d42c7a588..8ebdbddcb6 100644
--- a/pkg/workflow/compiler_pre_activation_job.go
+++ b/pkg/workflow/compiler_pre_activation_job.go
@@ -88,9 +88,18 @@ 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
steps = append(steps, " - name: Check skip-if-match query\n")
@@ -100,14 +109,19 @@ 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")
+ 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"))
}
// 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
steps = append(steps, " - name: Check skip-if-no-match query\n")
@@ -117,7 +131,13 @@ 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")
+ 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"))
}
@@ -407,3 +427,54 @@ func (c *Compiler) extractPreActivationCustomFields(jobs map[string]any) ([]stri
return customSteps, customOutputs, nil
}
+
+// 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 := constants.PreActivationAppTokenStepID
+
+ 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")
+ 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))
+
+ 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
+}
+
+// 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 data.ActivationGitHubToken != "" {
+ return data.ActivationGitHubToken
+ }
+ return ""
+}
diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go
index 802042d46f..45367b4d31 100644
--- a/pkg/workflow/compiler_types.go
+++ b/pkg/workflow/compiler_types.go
@@ -325,12 +325,16 @@ func (c *Compiler) GetSharedActionCache() *ActionCache {
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
+ // 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
+ // 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 7d47dbd755..f129087d0b 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:")) {
// 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:")) {
// 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 bb2611b4d2..03bdc0e8ad 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 51a679cfa5..2d1ffc7a64 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 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")
+ }
+
+ // 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 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
+ 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 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"
+ 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 a51657d1fc..6f4abb5d61 100644
--- a/pkg/workflow/skip_if_no_match_test.go
+++ b/pkg/workflow/skip_if_no_match_test.go
@@ -339,4 +339,233 @@ 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 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")
+ }
+
+ // 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 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
+ 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 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"
+ if !strings.Contains(lockContentStr, `GH_AW_SKIP_SCOPE: "none"`) {
+ 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 0f781beb50..7353297aa7 100644
--- a/pkg/workflow/stop_after.go
+++ b/pkg/workflow/stop_after.go
@@ -254,9 +254,16 @@ func (c *Compiler) extractSkipIfMatchFromOn(frontmatter map[string]any, workflow
}
}
+ // 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,
}, 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 +340,16 @@ func (c *Compiler) extractSkipIfNoMatchFromOn(frontmatter map[string]any, workfl
}
}
+ // 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,
}, 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 +400,21 @@ func (c *Compiler) processSkipIfNoMatchConfiguration(frontmatter map[string]any,
return nil
}
+
+// 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 extractSkipIfScope(skip map[string]any, conditionName string) (string, error) {
+ if scopeRaw, hasScope := skip["scope"]; hasScope {
+ scopeStr, ok := scopeRaw.(string)
+ if !ok {
+ return "", fmt.Errorf("%s 'scope' field must be a string, got %T. Example: scope: none", conditionName, scopeRaw)
+ }
+ if scopeStr != "none" {
+ return "", fmt.Errorf("%s 'scope' field must be \"none\" or omitted, got %q", conditionName, scopeStr)
+ }
+ return scopeStr, nil
+ }
+ return "", nil
+}