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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -939,7 +939,7 @@ download-github-actions-schema:
patch-github-actions-schema:
@echo "Patching GitHub Actions schema with custom permissions..."
@tmpfile=$$(mktemp) && \
jq 'def append_missing($$items): reduce $$items[] as $$item (. ; if index($$item) then . else . + [$$item] end); .definitions["permissions-event"].properties += {"copilot-requests": {"type": "string", "enum": ["write", "none"]}, "drives": {"$$ref": "#/definitions/permissions-level"}, "vulnerability-alerts": {"type": "string", "enum": ["read", "none"]}} | (.properties.on.oneOf[] | select(.properties? and .properties.issues? and .properties.issues.properties? and .properties.issues.properties.types?).properties.issues.properties.types.items.enum) |= append_missing(["typed", "untyped", "field_added", "field_removed"]) | (.properties.on.oneOf[] | select(.properties? and .properties.issues? and .properties.issues.properties? and .properties.issues.properties.types?).properties.issues.properties.types.default) |= append_missing(["typed", "untyped", "field_added", "field_removed"])' \
jq 'def append_missing($$items): reduce $$items[] as $$item (. ; if index($$item) then . else . + [$$item] end); def issue_types: [.properties.on.oneOf[] | select(.properties?.issues?.oneOf?) | .properties.issues.oneOf[] | select(.properties?.types?.items?.enum? and .properties?.types?.default?)]; if (issue_types | length) == 0 then error("issues types selector matched nothing") else . end | .definitions["permissions-event"].properties += {"copilot-requests": {"type": "string", "enum": ["write", "none"]}, "drives": {"$$ref": "#/definitions/permissions-level"}, "vulnerability-alerts": {"type": "string", "enum": ["read", "none"]}} | (.properties.on.oneOf[] | select(.properties?.issues?.oneOf?) | .properties.issues.oneOf[] | select(.properties?.types?).properties.types.items.enum) |= append_missing(["typed", "untyped", "field_added", "field_removed", "status_changed"]) | (.properties.on.oneOf[] | select(.properties?.issues?.oneOf?) | .properties.issues.oneOf[] | select(.properties?.types?).properties.types.default) |= append_missing(["typed", "untyped", "field_added", "field_removed", "status_changed"]) | if ([issue_types[] | select((.properties.types.items.enum | index("status_changed")) != null and (.properties.types.default | index("status_changed")) != null)] | length) != (issue_types | length) then error("issues types patch did not add status_changed") else . end' \
pkg/workflow/schemas/github-workflow.json > "$$tmpfile" && \
mv "$$tmpfile" pkg/workflow/schemas/github-workflow.json
@cd actions/setup/js && npm run format:schema >/dev/null 2>&1
Expand Down
3 changes: 2 additions & 1 deletion actions/setup/js/github_notification_webhooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"typed",
"untyped",
"field_added",
"field_removed"
"field_removed",
"status_changed"
],
"description": "Issue lifecycle and metadata notifications, excluding pull requests.",
"path": "issues",
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/specs/webhook-gateway-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ The shared compiler/runtime catalog is `actions/setup/js/github_notification_web

| Nested trigger | Semantic `event` / native event | Exact supported actions | Canonical HTML path and original URL field |
| --- | --- | --- | --- |
| `issue` | `issues` / `issues` | `opened`, `edited`, `deleted`, `transferred`, `pinned`, `unpinned`, `closed`, `reopened`, `assigned`, `unassigned`, `labeled`, `unlabeled`, `locked`, `unlocked`, `milestoned`, `demilestoned`, `typed`, `untyped`, `field_added`, `field_removed` | `/OWNER/REPO/issues/N`, `issue.html_url` |
| `issue` | `issues` / `issues` | `opened`, `edited`, `deleted`, `transferred`, `pinned`, `unpinned`, `closed`, `reopened`, `assigned`, `unassigned`, `labeled`, `unlabeled`, `locked`, `unlocked`, `milestoned`, `demilestoned`, `typed`, `untyped`, `field_added`, `field_removed`, `status_changed` | `/OWNER/REPO/issues/N`, `issue.html_url` |
| `pull-request` | `pull_request` / `pull_request` | `assigned`, `unassigned`, `labeled`, `unlabeled`, `opened`, `edited`, `closed`, `reopened`, `synchronize`, `converted_to_draft`, `locked`, `unlocked`, `enqueued`, `dequeued`, `milestoned`, `demilestoned`, `ready_for_review`, `review_requested`, `review_request_removed`, `auto_merge_enabled`, `auto_merge_disabled` | `/OWNER/REPO/pull/N`, `pull_request.html_url` |
| `discussion` | `discussion` / `discussion` | `created`, `edited`, `deleted`, `transferred`, `pinned`, `unpinned`, `labeled`, `unlabeled`, `locked`, `unlocked`, `category_changed`, `answered`, `unanswered` | `/OWNER/REPO/discussions/N`, `discussion.html_url` |
| `issue-comment` | `issue_comment` / `issue_comment` | `created`, `edited`, `deleted` | `/OWNER/REPO/issues/N#issuecomment-C`, `comment.html_url` |
Expand Down Expand Up @@ -832,7 +832,7 @@ The receiver checks the original `projects_v2_item.project_node_id` and delivere
These complete examples use the exact family/URL/anchor forms from the notification catalog. Each GitHub notification carries exactly one original action. Only the last example includes a declared matching selector.

```json title="issues.payload.json"
{"url":"https://github.com/owner/repo/issues/42","type":["opened"]}
{"url":"https://github.com/owner/repo/issues/42","type":["status_changed"]}
```

```json title="pull_request.payload.json"
Expand Down
13 changes: 12 additions & 1 deletion pkg/parser/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func TestValidateMainWorkflowFrontmatter_IssueFieldActivityTypes(t *testing.T) {
frontmatter := map[string]any{
"on": map[string]any{
"issues": map[string]any{
"types": []any{"typed", "untyped", "field_added", "field_removed"},
"types": []any{"typed", "untyped", "field_added", "field_removed", "status_changed"},
},
},
"engine": "copilot",
Expand All @@ -119,6 +119,17 @@ func TestValidateMainWorkflowFrontmatter_IssueFieldActivityTypes(t *testing.T) {
t.Fatalf("expected issue field activity types to validate: %v", err)
}

invalidFrontmatter := map[string]any{
"on": map[string]any{
"issues": map[string]any{
"types": []any{"status_change"},
},
},
"engine": "copilot",
}
if err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(invalidFrontmatter, "workflow.md"); err == nil {
t.Fatal("expected misspelled issue activity type to fail validation")
}
}

func TestValidateMainWorkflowFrontmatter_MaxToolCalls(t *testing.T) {
Expand Down
3 changes: 2 additions & 1 deletion pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1543,7 +1543,8 @@
"typed",
"untyped",
"field_added",
"field_removed"
"field_removed",
"status_changed"
]
}
},
Expand Down
8 changes: 6 additions & 2 deletions pkg/workflow/actionslanguageservice/compiler_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"sync"
)

// ValidateWorkflowWithCacheMode validates compiler output with the cache-mode
// field supported by GitHub Actions but absent from the pinned upstream schema.
// ValidateWorkflowWithCacheMode validates compiler output with cache-mode and
// status_changed support absent from the pinned upstream schema.
// The ordinary parser and its conformance schema remain unchanged.
func ValidateWorkflowWithCacheMode(name, content string) error {
schema, err := loadCacheModeSchema()
Expand All @@ -27,6 +27,10 @@ var loadCacheModeSchema = sync.OnceValues(func() (map[string]definition, error)
}
schema := maps.Clone(upstream)
schema["cache-mode"] = definition{AllowedValues: []string{"read", "write", "write-only", "none"}}
issuesActivityType := schema["issues-activity-type"]
// Copy the slice before appending so the cached upstream schema remains unchanged.
issuesActivityType.AllowedValues = append(append([]string(nil), issuesActivityType.AllowedValues...), "status_changed")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] Copying AllowedValues before appending correctly avoids mutating the cached upstream slice. That aliasing risk is non-obvious, so a short comment would help future edits.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment explaining that the slice is copied before append to preserve the cached upstream schema.

schema["issues-activity-type"] = issuesActivityType
for _, name := range []string{"workflow-root", "workflow-root-strict", "job-factory", "workflow-job"} {
d := schema[name]
mapping := *d.Mapping
Expand Down
13 changes: 13 additions & 0 deletions pkg/workflow/actionslanguageservice/compiler_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ func TestCompilerCacheModeSchema(t *testing.T) {
}
}

func TestCompilerIssuesStatusChangedSchema(t *testing.T) {
t.Parallel()
statusChanged := "on:\n issues:\n types: [status_changed]\njobs:\n build:\n runs-on: ubuntu-latest\n steps: [{run: echo hi}]\n"
require.NoError(t, ValidateWorkflowWithCacheMode("issues.yml", statusChanged))

// The pinned upstream schema must still reject status_changed until it includes the value.
_, err := ParseWorkflow("upstream.yml", statusChanged)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] This assertion intentionally pins the upstream schema to reject status_changed, which proves the overlay is needed. That intent is not obvious, and the test will start failing once the pinned schema adds the value.

💡 Suggested clarification

Add a one-line comment above the ParseWorkflow call, for example: // Pinned upstream still rejects status_changed; this fails when the overlay is no longer needed.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test comment clarifying that the pinned upstream schema must reject status_changed until it includes the value.

require.ErrorContains(t, err, "status_changed")

statusChange := "on:\n issues:\n types: [status_change]\njobs:\n build:\n runs-on: ubuntu-latest\n steps: [{run: echo hi}]\n"
require.Error(t, ValidateWorkflowWithCacheMode("issues.yml", statusChange))
}

func TestCompilerCacheModePreservesValidation(t *testing.T) {
t.Parallel()
for _, test := range []struct {
Expand Down
17 changes: 16 additions & 1 deletion pkg/workflow/schema_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ jobs:
name: Test
on:
issues:
types: [typed, untyped, field_added, field_removed]
types: [typed, untyped, field_added, field_removed, status_changed]
jobs:
test:
runs-on: ubuntu-latest
Expand All @@ -258,6 +258,21 @@ jobs:
`,
expectError: false,
},
{
name: "invalid issue activity type fails",
yamlContent: `
name: Test
on:
issues:
types: [status_change]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo hello
`,
expectError: true,
},
// Note: We can't easily test invalid YAML here because the schema validation
// happens after YAML parsing, and most errors would be caught by the parser first.
// The enhancement is verified through the unit tests above.
Expand Down
6 changes: 4 additions & 2 deletions pkg/workflow/schemas/github-workflow.json
Original file line number Diff line number Diff line change
Expand Up @@ -1449,7 +1449,8 @@
"typed",
"untyped",
"field_added",
"field_removed"
"field_removed",
"status_changed"
]
},
"default": [
Expand All @@ -1472,7 +1473,8 @@
"typed",
"untyped",
"field_added",
"field_removed"
"field_removed",
"status_changed"
]
}
}
Expand Down
Loading