Compare commits

...

2 Commits

Author SHA1 Message Date
Salman Chishti b500dbdf2d Remove duplicate test file and fix expression bypass
- Remove duplicate test file (errors-job-environment-deployment-disabled-feature.yml)
- Move deployment feature flag check before expression skip so
  deployment: ${{ ... }} is still gated by allowDeploymentKeyword
2026-03-18 15:51:16 +00:00
Salman Chishti 64aae8a102 Add deployment field to job environment
Add support for the 'deployment' boolean property under 'environment:' in
workflow YAML. When set to false, the job accesses environment protection
rules and secrets without creating a deployment record.

Changes:
- Add 'deployment' to job-environment-mapping schema (workflow-v1.0.json)
- Add 'deployment' to ActionsEnvironmentReference type
- Add feature-gated parsing in environment converter
- Add 'allowDeploymentKeyword' experimental feature flag
- Add xlang test data for enabled/disabled feature flag scenarios
2026-03-18 15:33:51 +00:00
9 changed files with 172 additions and 3 deletions
+2 -1
View File
@@ -56,7 +56,8 @@ describe("FeatureFlags", () => {
"blockScalarChompingWarning",
"allowCaseFunction",
"allowCronTimezone",
"allowCopilotRequestsPermission"
"allowCopilotRequestsPermission",
"allowDeploymentKeyword"
]);
});
});
+8 -1
View File
@@ -46,6 +46,12 @@ export interface ExperimentalFeatures {
* @default false
*/
allowCopilotRequestsPermission?: boolean;
/**
* Enable the deployment keyword in workflow job environment.
* @default false
*/
allowDeploymentKeyword?: boolean;
}
/**
@@ -62,7 +68,8 @@ const allFeatureKeys: ExperimentalFeatureKey[] = [
"blockScalarChompingWarning",
"allowCaseFunction",
"allowCronTimezone",
"allowCopilotRequestsPermission"
"allowCopilotRequestsPermission",
"allowDeploymentKeyword"
];
export class FeatureFlags {
@@ -1,3 +1,4 @@
import {FeatureFlags} from "@actions/expressions/features";
import {TemplateContext} from "../../../templates/template-context.js";
import {TemplateToken} from "../../../templates/tokens/template-token.js";
import {isScalar} from "../../../templates/tokens/type-guards.js";
@@ -22,6 +23,18 @@ export function convertToActionsEnvironmentRef(
for (const property of environmentMapping) {
const propertyName = property.key.assertString("job environment key");
// Check deployment feature flag before skipping expressions,
// so deployment: ${{ ... }} is still gated by the flag
if (propertyName.value === "deployment") {
const featureFlags = context.state["featureFlags"] as FeatureFlags | undefined;
const flags = featureFlags ?? new FeatureFlags();
if (!flags.isEnabled("allowDeploymentKeyword")) {
context.error(property.key, `The key 'deployment' is not allowed`);
continue;
}
}
if (property.key.isExpression || property.value.isExpression) {
continue;
}
@@ -34,6 +47,10 @@ export function convertToActionsEnvironmentRef(
case "url":
result.url = property.value;
break;
case "deployment":
result.deployment = property.value;
break;
}
}
@@ -26,6 +26,7 @@ export type ConcurrencySetting = {
export type ActionsEnvironmentReference = {
name?: TemplateToken;
url?: TemplateToken;
deployment?: TemplateToken;
};
export type WorkflowJob = Job | ReusableWorkflowJob;
+4
View File
@@ -2079,6 +2079,10 @@
"url": {
"type": "string-runner-context-no-secrets",
"description": "The environment URL, which maps to `environment_url` in the deployments API."
},
"deployment": {
"type": "boolean",
"description": "Whether to create a deployment for this environment. Set to `false` to access environment secrets and variables without creating a deployment record. Defaults to `true`."
}
}
}
+6 -1
View File
@@ -1,6 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import * as YAML from "yaml";
import {FeatureFlags} from "@actions/expressions/features";
import {convertWorkflowTemplate} from "./model/convert.js";
import {NoOperationTraceWriter} from "./templates/trace-writer.js";
import {File} from "./workflows/file.js";
@@ -10,6 +11,7 @@ import {parseWorkflow} from "./workflows/workflow-parser.js";
interface TestOptions {
"include-source"?: boolean;
"allow-deployment-keyword"?: boolean;
skip?: string[];
}
@@ -85,7 +87,10 @@ describe("x-lang tests", () => {
parseResult.value!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
testFileProvider,
{
fetchReusableWorkflowDepth: 1
fetchReusableWorkflowDepth: 1,
featureFlags: new FeatureFlags({
allowDeploymentKeyword: testOptions["allow-deployment-keyword"]
})
}
);
@@ -0,0 +1,21 @@
include-source: false # Drop file/line/col from output
skip:
- C#
---
on: push
jobs:
build:
environment:
name: production
deployment: false
runs-on: ubuntu-latest
steps:
- run: echo hi
---
{
"errors": [
{
"Message": ".github/workflows/errors-job-environment-deployment-disabled-feature-default-go.yml (Line: 6, Col: 7): The key 'deployment' is not allowed"
}
]
}
@@ -0,0 +1,21 @@
include-source: false # Drop file/line/col from output
skip:
- C#
---
on: push
jobs:
build:
environment:
name: production
deployment: false
runs-on: ubuntu-latest
steps:
- run: echo hi
---
{
"errors": [
{
"Message": ".github/workflows/errors-job-environment-deployment-disabled-feature.yml (Line: 6, Col: 7): The key 'deployment' is not allowed"
}
]
}
@@ -0,0 +1,92 @@
include-source: false # Drop file/line/col from output
skip:
- C#
allow-deployment-keyword: true
---
on: push
jobs:
build:
environment:
name: production
deployment: false
runs-on: ubuntu-latest
steps:
- run: echo hi
build2:
environment:
name: staging
deployment: true
runs-on: ubuntu-latest
steps:
- run: echo hi
---
{
"jobs": [
{
"type": "job",
"id": "build",
"name": "build",
"if": {
"type": 3,
"expr": "success()"
},
"environment": {
"type": 2,
"map": [
{
"Key": "name",
"Value": "production"
},
{
"Key": "deployment",
"Value": false
}
]
},
"runs-on": "ubuntu-latest",
"steps": [
{
"id": "__run",
"if": {
"type": 3,
"expr": "success()"
},
"run": "echo hi"
}
]
},
{
"type": "job",
"id": "build2",
"name": "build2",
"if": {
"type": 3,
"expr": "success()"
},
"environment": {
"type": 2,
"map": [
{
"Key": "name",
"Value": "staging"
},
{
"Key": "deployment",
"Value": true
}
]
},
"runs-on": "ubuntu-latest",
"steps": [
{
"id": "__run",
"if": {
"type": 3,
"expr": "success()"
},
"run": "echo hi"
}
]
}
]
}