From 3de9820cd8a6612c038575bfc880080079e8aeaa Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Tue, 10 Mar 2026 14:48:54 +0000 Subject: [PATCH] Add `copilot-requests` permission, gated by feature flag (#335) * Add copilot-requests permission gated by feature flag This add a new 'copilot-requests' permission to the workflow schema, gated behind the 'allowCopilotRequestsPermission' experimental feature flag. When the flag is disabled (default), `copilot-requests` is filtered out of autocomplete suggestions. When enabled, it appears alongside other permissions like actions, contents, pull-requests, etc. * Update workflow-parser/src/workflow-v1.0.json * Add additional unit test coverage * Fix formatting --- expressions/src/features.test.ts | 3 +- expressions/src/features.ts | 9 ++- languageservice/src/complete.test.ts | 76 ++++++++++++++++++++++++++ languageservice/src/complete.ts | 8 +++ workflow-parser/src/workflow-v1.0.json | 4 ++ 5 files changed, 98 insertions(+), 2 deletions(-) diff --git a/expressions/src/features.test.ts b/expressions/src/features.test.ts index 0ff1de7..dcb029f 100644 --- a/expressions/src/features.test.ts +++ b/expressions/src/features.test.ts @@ -55,7 +55,8 @@ describe("FeatureFlags", () => { "missingInputsQuickfix", "blockScalarChompingWarning", "allowCaseFunction", - "allowCronTimezone" + "allowCronTimezone", + "allowCopilotRequestsPermission" ]); }); }); diff --git a/expressions/src/features.ts b/expressions/src/features.ts index 163629c..c2bcdd5 100644 --- a/expressions/src/features.ts +++ b/expressions/src/features.ts @@ -40,6 +40,12 @@ export interface ExperimentalFeatures { * @default false */ allowCronTimezone?: boolean; + + /** + * Enable the copilot-requests permission in workflow permissions. + * @default false + */ + allowCopilotRequestsPermission?: boolean; } /** @@ -55,7 +61,8 @@ const allFeatureKeys: ExperimentalFeatureKey[] = [ "missingInputsQuickfix", "blockScalarChompingWarning", "allowCaseFunction", - "allowCronTimezone" + "allowCronTimezone", + "allowCopilotRequestsPermission" ]; export class FeatureFlags { diff --git a/languageservice/src/complete.test.ts b/languageservice/src/complete.test.ts index aa7384b..b33ad66 100644 --- a/languageservice/src/complete.test.ts +++ b/languageservice/src/complete.test.ts @@ -967,3 +967,79 @@ describe("schedule timezone completion", () => { expect(labels).not.toContain("timezone"); }); }); + +describe("permissions copilot-requests completion", () => { + it("includes copilot-requests when allowCopilotRequestsPermission is enabled", async () => { + const input = `on: push +permissions: + |`; + const result = await complete(...getPositionFromCursor(input), { + featureFlags: new FeatureFlags({allowCopilotRequestsPermission: true}) + }); + + expect(result).not.toBeUndefined(); + const labels = result.map(x => x.label); + expect(labels).toContain("actions"); + expect(labels).toContain("copilot-requests"); + }); + + it("excludes copilot-requests when allowCopilotRequestsPermission is disabled", async () => { + const input = `on: push +permissions: + |`; + const result = await complete(...getPositionFromCursor(input), { + featureFlags: new FeatureFlags({allowCopilotRequestsPermission: false}) + }); + + expect(result).not.toBeUndefined(); + const labels = result.map(x => x.label); + expect(labels).toContain("actions"); + expect(labels).not.toContain("copilot-requests"); + }); + + it("excludes copilot-requests when no feature flags are provided", async () => { + const input = `on: push +permissions: + |`; + const result = await complete(...getPositionFromCursor(input)); + + expect(result).not.toBeUndefined(); + const labels = result.map(x => x.label); + expect(labels).toContain("actions"); + expect(labels).not.toContain("copilot-requests"); + }); + + it("includes copilot-requests in job-level permissions when allowCopilotRequestsPermission is enabled", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + permissions: + |`; + const result = await complete(...getPositionFromCursor(input), { + featureFlags: new FeatureFlags({allowCopilotRequestsPermission: true}) + }); + + expect(result).not.toBeUndefined(); + const labels = result.map(x => x.label); + expect(labels).toContain("actions"); + expect(labels).toContain("copilot-requests"); + }); + + it("excludes copilot-requests from job-level permissions when allowCopilotRequestsPermission is disabled", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + permissions: + |`; + const result = await complete(...getPositionFromCursor(input), { + featureFlags: new FeatureFlags({allowCopilotRequestsPermission: false}) + }); + + expect(result).not.toBeUndefined(); + const labels = result.map(x => x.label); + expect(labels).toContain("actions"); + expect(labels).not.toContain("copilot-requests"); + }); +}); diff --git a/languageservice/src/complete.ts b/languageservice/src/complete.ts index a861c75..650ff1b 100644 --- a/languageservice/src/complete.ts +++ b/languageservice/src/complete.ts @@ -168,6 +168,14 @@ export async function complete( values = values.filter(v => v.label !== "timezone"); } + // Filter `copilot-requests` from permissions completions when the feature flag is disabled + if ( + !config?.featureFlags?.isEnabled("allowCopilotRequestsPermission") && + parent?.definition?.key === "permissions-mapping" + ) { + values = values.filter(v => v.label !== "copilot-requests"); + } + // Offer "(switch to list)" / "(switch to mapping)" when the schema allows alternative forms const escapeHatches = getEscapeHatchCompletions(token, keyToken, indentString, newPos, schema); values.push(...escapeHatches); diff --git a/workflow-parser/src/workflow-v1.0.json b/workflow-parser/src/workflow-v1.0.json index cd63c12..1b7e214 100644 --- a/workflow-parser/src/workflow-v1.0.json +++ b/workflow-parser/src/workflow-v1.0.json @@ -1602,6 +1602,10 @@ "type": "permission-level-any", "description": "Repository contents, commits, branches, downloads, releases, and merges." }, + "copilot-requests": { + "type": "permission-level-write-or-no-access", + "description": "GitHub Copilot requests." + }, "deployments": { "type": "permission-level-any", "description": "Deployments and deployment statuses."