From d166cee3f040af6ea045e04929c419c08f2f9bad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:03:18 +0000 Subject: [PATCH 1/3] Initial plan From ef05fd91bd88bf964aed523d8f3789ece2f632ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:14:10 +0000 Subject: [PATCH 2/3] Accept status_changed issue activity type Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- Makefile | 2 +- pkg/parser/schema_test.go | 13 ++++++++++++- pkg/parser/schemas/main_workflow_schema.json | 3 ++- .../actionslanguageservice/compiler_schema.go | 7 +++++-- .../compiler_schema_test.go | 12 ++++++++++++ pkg/workflow/schema_validation_test.go | 17 ++++++++++++++++- pkg/workflow/schemas/github-workflow.json | 6 ++++-- 7 files changed, 52 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 98bc0d3faaa..e90177926f9 100644 --- a/Makefile +++ b/Makefile @@ -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); .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"])' \ 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 diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index ba4176c00d2..ad2c27e8671 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -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", @@ -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) { diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index cace01f5bb4..d7086993f33 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -1543,7 +1543,8 @@ "typed", "untyped", "field_added", - "field_removed" + "field_removed", + "status_changed" ] } }, diff --git a/pkg/workflow/actionslanguageservice/compiler_schema.go b/pkg/workflow/actionslanguageservice/compiler_schema.go index 4a32feb5c0c..a65f21da969 100644 --- a/pkg/workflow/actionslanguageservice/compiler_schema.go +++ b/pkg/workflow/actionslanguageservice/compiler_schema.go @@ -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() @@ -27,6 +27,9 @@ 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"] + issuesActivityType.AllowedValues = append(append([]string(nil), issuesActivityType.AllowedValues...), "status_changed") + schema["issues-activity-type"] = issuesActivityType for _, name := range []string{"workflow-root", "workflow-root-strict", "job-factory", "workflow-job"} { d := schema[name] mapping := *d.Mapping diff --git a/pkg/workflow/actionslanguageservice/compiler_schema_test.go b/pkg/workflow/actionslanguageservice/compiler_schema_test.go index bad6dfd5a95..d29ae802b9a 100644 --- a/pkg/workflow/actionslanguageservice/compiler_schema_test.go +++ b/pkg/workflow/actionslanguageservice/compiler_schema_test.go @@ -37,6 +37,18 @@ 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)) + + _, err := ParseWorkflow("upstream.yml", statusChanged) + 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 { diff --git a/pkg/workflow/schema_validation_test.go b/pkg/workflow/schema_validation_test.go index 94f9af369cc..9571bf30c75 100644 --- a/pkg/workflow/schema_validation_test.go +++ b/pkg/workflow/schema_validation_test.go @@ -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 @@ -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. diff --git a/pkg/workflow/schemas/github-workflow.json b/pkg/workflow/schemas/github-workflow.json index e33b54a8c74..728038528b6 100644 --- a/pkg/workflow/schemas/github-workflow.json +++ b/pkg/workflow/schemas/github-workflow.json @@ -1449,7 +1449,8 @@ "typed", "untyped", "field_added", - "field_removed" + "field_removed", + "status_changed" ] }, "default": [ @@ -1472,7 +1473,8 @@ "typed", "untyped", "field_added", - "field_removed" + "field_removed", + "status_changed" ] } } From f716b5ff58edad0be1f5a4f300947ee0fdff52af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:17:54 +0000 Subject: [PATCH 3/3] Address status_changed review feedback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- Makefile | 2 +- actions/setup/js/github_notification_webhooks.json | 3 ++- docs/src/content/docs/specs/webhook-gateway-specification.md | 4 ++-- pkg/workflow/actionslanguageservice/compiler_schema.go | 1 + pkg/workflow/actionslanguageservice/compiler_schema_test.go | 1 + 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index e90177926f9..7fd2149b8ea 100644 --- a/Makefile +++ b/Makefile @@ -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?.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"])' \ + 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 diff --git a/actions/setup/js/github_notification_webhooks.json b/actions/setup/js/github_notification_webhooks.json index a4dc1671f66..9d7c8ffda80 100644 --- a/actions/setup/js/github_notification_webhooks.json +++ b/actions/setup/js/github_notification_webhooks.json @@ -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", diff --git a/docs/src/content/docs/specs/webhook-gateway-specification.md b/docs/src/content/docs/specs/webhook-gateway-specification.md index e5edb758021..236933fa681 100644 --- a/docs/src/content/docs/specs/webhook-gateway-specification.md +++ b/docs/src/content/docs/specs/webhook-gateway-specification.md @@ -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` | @@ -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" diff --git a/pkg/workflow/actionslanguageservice/compiler_schema.go b/pkg/workflow/actionslanguageservice/compiler_schema.go index a65f21da969..6c3a1a92287 100644 --- a/pkg/workflow/actionslanguageservice/compiler_schema.go +++ b/pkg/workflow/actionslanguageservice/compiler_schema.go @@ -28,6 +28,7 @@ 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") schema["issues-activity-type"] = issuesActivityType for _, name := range []string{"workflow-root", "workflow-root-strict", "job-factory", "workflow-job"} { diff --git a/pkg/workflow/actionslanguageservice/compiler_schema_test.go b/pkg/workflow/actionslanguageservice/compiler_schema_test.go index d29ae802b9a..47adc5dc8f2 100644 --- a/pkg/workflow/actionslanguageservice/compiler_schema_test.go +++ b/pkg/workflow/actionslanguageservice/compiler_schema_test.go @@ -42,6 +42,7 @@ func TestCompilerIssuesStatusChangedSchema(t *testing.T) { 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) require.ErrorContains(t, err, "status_changed")