Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
588 changes: 456 additions & 132 deletions .github/workflows/issue-monster.lock.yml

Large diffs are not rendered by default.

669 changes: 334 additions & 335 deletions .github/workflows/issue-monster.md

Large diffs are not rendered by default.

73 changes: 71 additions & 2 deletions docs/src/content/docs/guides/deterministic-agentic-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,74 @@ Pass data between jobs via artifacts, job outputs, or environment variables.

## Custom Trigger Filtering

Use a deterministic job to compute whether the agent should run, expose the result as a job output, and reference it with `if:`. The compiler automatically adds the filter job as a dependency of the activation job, so when the condition is false the workflow run is **skipped** (not failed), keeping the Actions tab clean.
### Inline Steps (`on.steps:`) — Preferred

Inject deterministic steps directly into the pre-activation job using `on.steps:`. This saves **one workflow job** compared to the multi-job pattern and is the recommended approach for lightweight filtering:

```yaml wrap title=".github/workflows/smart-responder.md"
---
on:
issues:
types: [opened]
steps:
- id: check
env:
LABELS: ${{ toJSON(github.event.issue.labels.*.name) }}
run: echo "$LABELS" | grep -q '"bug"'
# exits 0 (outcome: success) if the label is found, 1 (outcome: failure) if not
engine: copilot
safe-outputs:
add-comment:

if: needs.pre_activation.outputs.check_result == 'success'
---

# Bug Issue Responder

Triage bug report: "${{ github.event.issue.title }}" and add-comment with a summary of the next steps.
```

Each step with an `id` gets an auto-wired output `<id>_result` set to `${{ steps.<id>.outcome }}` — `success` when the step's exit code is 0, `failure` when non-zero. Gate the workflow by checking `needs.pre_activation.outputs.<id>_result == 'success'`.

To pass an explicit value rather than relying on exit codes, set a step output and re-expose it via `jobs.pre-activation.outputs`:

```yaml wrap
jobs:
pre-activation:
outputs:
has_bug_label: ${{ steps.check.outputs.has_bug_label }}

if: needs.pre_activation.outputs.has_bug_label == 'true'
```

When `on.steps:` need GitHub API access, use `on.permissions:` to grant the required scopes to the pre-activation job:

```yaml wrap
on:
schedule: every 30m
permissions:
issues: read
steps:
- id: search
uses: actions/github-script@v8
with:
script: |
const open = await github.rest.issues.listForRepo({ ...context.repo, state: 'open' });
core.setOutput('has_work', open.data.length > 0 ? 'true' : 'false');

jobs:
pre-activation:
outputs:
has_work: ${{ steps.search.outputs.has_work }}

if: needs.pre_activation.outputs.has_work == 'true'
```

See [Pre-Activation Steps](/gh-aw/reference/triggers/#pre-activation-steps-onsteps) and [Pre-Activation Permissions](/gh-aw/reference/triggers/#pre-activation-permissions-onpermissions) for full documentation.

### Multi-Job Pattern — For Complex Cases

Use a separate `jobs:` entry when filtering requires heavy tooling (checkout, compiled tools, multiple runners):

```yaml wrap title=".github/workflows/smart-responder.md"
---
Expand Down Expand Up @@ -129,7 +196,7 @@ if: needs.filter.outputs.should-run == 'true'
Triage bug report: "${{ github.event.issue.title }}" and add-comment with a summary of the next steps.
```

When `should-run` is `false`, GitHub marks the dependent jobs as **skipped** rather than failed, so no red X appears in the Actions tab and the Workflow Failure issue mechanism is not triggered.
The compiler automatically adds the filter job as a dependency of the activation job, so when the condition is false the workflow run is **skipped** (not failed), keeping the Actions tab clean.

### Simple Context Conditions

Expand Down Expand Up @@ -220,6 +287,8 @@ Reference in prompts: "Analyze issues in `/tmp/gh-aw/agent/issues.json` and PRs

## Related Documentation

- [Pre-Activation Steps](/gh-aw/reference/triggers/#pre-activation-steps-onsteps) - Inline step injection into the pre-activation job
- [Pre-Activation Permissions](/gh-aw/reference/triggers/#pre-activation-permissions-onpermissions) - Grant additional scopes for `on.steps:` API calls
- [Custom Safe Outputs](/gh-aw/reference/custom-safe-outputs/) - Custom post-processing jobs
- [Frontmatter Reference](/gh-aw/reference/frontmatter/) - Configuration options
- [Compilation Process](/gh-aw/reference/compilation-process/) - How jobs are orchestrated
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/reference/frontmatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ The `on:` section uses standard GitHub Actions syntax to define workflow trigger
- `skip-bots:` - Skip workflow execution for specific GitHub actors
- `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)
- `steps:` - Inject custom deterministic steps into the pre-activation job (saves one workflow job vs. multi-job pattern)
- `permissions:` - Grant additional GitHub token scopes to the pre-activation job (for use with `on.steps:` API calls)
- `github-token:` - Custom token for activation job reactions, status comments, and skip-if search queries
- `github-app:` - GitHub App for minting a short-lived token used by the activation job and all skip-if search steps

Expand Down
80 changes: 80 additions & 0 deletions docs/src/content/docs/reference/triggers.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,86 @@ on:
owner: myorg
```

### Pre-Activation Steps (`on.steps:`)

Inject custom deterministic steps directly into the pre-activation job. Steps run after all built-in checks (membership, stop-time, skip-if, etc.) and **before** agent execution. This saves one workflow job compared to the multi-job pattern and keeps filtering logic co-located with the trigger configuration.

```yaml wrap
on:
issues:
types: [opened]
steps:
- name: Check issue label
id: label_check
env:
LABELS: ${{ toJSON(github.event.issue.labels.*.name) }}
run: echo "$LABELS" | grep -q '"bug"'
# exits 0 (outcome: success) if the label is found, 1 (outcome: failure) if not

if: needs.pre_activation.outputs.label_check_result == 'success'
```

Each step with an `id` automatically gets an output `<id>_result` wired to `${{ steps.<id>.outcome }}` (values: `success`, `failure`, `cancelled`, `skipped`). This lets you gate the workflow on whether the step **succeeded or failed** via its exit code.

To pass an explicit value rather than relying on exit codes, set a step output and re-expose it via `jobs.pre-activation.outputs`:

```yaml wrap
on:
issues:
types: [opened]
steps:
- name: Check issue label
id: label_check
env:
LABELS: ${{ toJSON(github.event.issue.labels.*.name) }}
run: |
if echo "$LABELS" | grep -q '"bug"'; then
echo "has_bug_label=true" >> "$GITHUB_OUTPUT"
else
echo "has_bug_label=false" >> "$GITHUB_OUTPUT"
fi

jobs:
pre-activation:
outputs:
has_bug_label: ${{ steps.label_check.outputs.has_bug_label }}

if: needs.pre_activation.outputs.has_bug_label == 'true'
```

Explicit outputs defined in `jobs.pre-activation.outputs` take precedence over auto-wired `<id>_result` outputs on key collision.

### Pre-Activation Permissions (`on.permissions:`)

Grant additional GitHub token permission scopes to the pre-activation job. Use when `on.steps:` make GitHub API calls that require permissions beyond the defaults.

```yaml wrap
on:
schedule: every 30m
permissions:
issues: read
pull-requests: read
steps:
- name: Search for candidate issues
id: search
uses: actions/github-script@v8
with:
script: |
const issues = await github.rest.issues.listForRepo(context.repo);
core.setOutput('has_issues', issues.data.length > 0 ? 'true' : 'false');

jobs:
pre-activation:
outputs:
has_issues: ${{ steps.search.outputs.has_issues }}

if: needs.pre_activation.outputs.has_issues == 'true'
```

Supported permission scopes: `actions`, `checks`, `contents`, `deployments`, `discussions`, `issues`, `packages`, `pages`, `pull-requests`, `repository-projects`, `security-events`, `statuses`.

`on.permissions` is merged on top of any permissions already required by the pre-activation job (e.g., `contents: read` for dev-mode checkout, `actions: read` for rate limiting).

## 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.
Expand Down
129 changes: 127 additions & 2 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@
{
"type": "array",
"minItems": 1,
"description": "Array of label names any of these labels will trigger the workflow.",
"description": "Array of label names \u2014 any of these labels will trigger the workflow.",
"items": {
"type": "string",
"minLength": 1,
Expand All @@ -343,7 +343,7 @@
{
"type": "array",
"minItems": 1,
"description": "Array of label names any of these labels will trigger the workflow.",
"description": "Array of label names \u2014 any of these labels will trigger the workflow.",
"items": {
"type": "string",
"minLength": 1,
Expand Down Expand Up @@ -1589,6 +1589,131 @@
"private-key": "${{ secrets.APP_PRIVATE_KEY }}"
}
]
},
"steps": {
"type": "array",
"description": "Steps to inject into the pre-activation job. These steps run after all built-in checks (membership, stop-time, skip-if, etc.) and their results are exposed as pre-activation outputs. Use 'id' on steps to reference their results via needs.pre_activation.outputs.<id>_result.",
"items": {
"type": "object",
"description": "A GitHub Actions step to inject into the pre-activation job",
"properties": {
"name": {
"type": "string",
"description": "Optional name for the step"
},
"id": {
"type": "string",
"description": "Optional step ID. When set, the step result is exposed as needs.pre_activation.outputs.<id>_result"
},
"run": {
"type": "string",
"description": "Shell command to run"
},
"uses": {
"type": "string",
"description": "Action to use (e.g., 'actions/checkout@v4')"
},
"with": {
"type": "object",
"description": "Input parameters for the action",
"additionalProperties": true
},
"env": {
"type": "object",
"description": "Environment variables for the step",
"additionalProperties": {
"type": "string"
}
},
"if": {
"type": "string",
"description": "Conditional expression for the step"
},
"continue-on-error": {
"type": "boolean",
"description": "Whether to continue if the step fails"
}
},
"additionalProperties": true
},
"examples": [
[
{
"name": "Gate check",
"id": "gate",
"run": "echo 'Checking gate...' && exit 0"
}
]
]
},
"permissions": {
"description": "Additional permissions for the pre-activation job. Use to declare extra scopes required by on.steps (e.g., issues: read for GitHub API calls in steps).",
"oneOf": [
{
"type": "object",
"description": "Map of permission scope to level",
"properties": {
"actions": {
"type": "string",
"enum": ["read", "write", "none"]
},
"checks": {
"type": "string",
"enum": ["read", "write", "none"]
},
"contents": {
"type": "string",
"enum": ["read", "write", "none"]
},
"deployments": {
"type": "string",
"enum": ["read", "write", "none"]
},
"discussions": {
"type": "string",
"enum": ["read", "write", "none"]
},
"issues": {
"type": "string",
"enum": ["read", "write", "none"]
},
"packages": {
"type": "string",
"enum": ["read", "write", "none"]
},
"pages": {
"type": "string",
"enum": ["read", "write", "none"]
},
"pull-requests": {
"type": "string",
"enum": ["read", "write", "none"]
},
"repository-projects": {
"type": "string",
"enum": ["read", "write", "none"]
},
"security-events": {
"type": "string",
"enum": ["read", "write", "none"]
},
"statuses": {
"type": "string",
"enum": ["read", "write", "none"]
}
},
"additionalProperties": false
}
],
"examples": [
{
"issues": "read"
},
{
"issues": "read",
"pull-requests": "read"
}
]
}
},
"additionalProperties": false,
Expand Down
7 changes: 4 additions & 3 deletions pkg/workflow/compiler_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,11 @@ func (c *Compiler) buildPreActivationAndActivationJobs(data *WorkflowData, front
hasSkipBots := len(data.SkipBots) > 0
hasCommandTrigger := len(data.Command) > 0
hasRateLimit := data.RateLimit != nil
compilerJobsLog.Printf("Job configuration: needsPermissionCheck=%v, hasStopTime=%v, hasSkipIfMatch=%v, hasSkipIfNoMatch=%v, hasSkipRoles=%v, hasSkipBots=%v, hasCommand=%v, hasRateLimit=%v", needsPermissionCheck, hasStopTime, hasSkipIfMatch, hasSkipIfNoMatch, hasSkipRoles, hasSkipBots, hasCommandTrigger, hasRateLimit)
hasOnSteps := len(data.OnSteps) > 0
compilerJobsLog.Printf("Job configuration: needsPermissionCheck=%v, hasStopTime=%v, hasSkipIfMatch=%v, hasSkipIfNoMatch=%v, hasSkipRoles=%v, hasSkipBots=%v, hasCommand=%v, hasRateLimit=%v, hasOnSteps=%v", needsPermissionCheck, hasStopTime, hasSkipIfMatch, hasSkipIfNoMatch, hasSkipRoles, hasSkipBots, hasCommandTrigger, hasRateLimit, hasOnSteps)

// Build pre-activation job if needed (combines membership checks, stop-time validation, skip-if-match check, skip-if-no-match check, skip-roles check, skip-bots check, rate limit check, and command position check)
if needsPermissionCheck || hasStopTime || hasSkipIfMatch || hasSkipIfNoMatch || hasSkipRoles || hasSkipBots || hasCommandTrigger || hasRateLimit {
// Build pre-activation job if needed (combines membership checks, stop-time validation, skip-if-match check, skip-if-no-match check, skip-roles check, skip-bots check, rate limit check, command position check, and on.steps injection)
if needsPermissionCheck || hasStopTime || hasSkipIfMatch || hasSkipIfNoMatch || hasSkipRoles || hasSkipBots || hasCommandTrigger || hasRateLimit || hasOnSteps {
compilerJobsLog.Print("Building pre-activation job")
preActivationJob, err := c.buildPreActivationJob(data, needsPermissionCheck)
if err != nil {
Expand Down
10 changes: 10 additions & 0 deletions pkg/workflow/compiler_orchestrator_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -728,5 +728,15 @@ func (c *Compiler) processOnSectionAndFilters(
// Apply label filter if specified
c.applyLabelFilter(workflowData, frontmatter)

// Extract on.steps for pre-activation step injection
onSteps, err := extractOnSteps(frontmatter)
if err != nil {
return err
}
workflowData.OnSteps = onSteps

// Extract on.permissions for pre-activation job permissions
workflowData.OnPermissions = extractOnPermissions(frontmatter)

return nil
}
Loading
Loading