Compare commits

..
Author SHA1 Message Date
Salman Muin Kayser Chishti 18f53d3c9e Include workflows in write-all when feature flag is enabled
The write-all permission level should include workflows:write when the
AllowWorkflowsPermission feature flag is enabled, matching the behavior
of other gated permissions like copilot-requests. Previously workflows
was unconditionally excluded from write-all. This aligns with the ADR
decision that write-all means permissive access.
2026-04-11 08:49:41 +01:00
Salman Muin Kayser Chishti 273538003e Add workflows permission scope to WorkflowParser
Add 'workflows' as a recognized permission scope for GITHUB_TOKEN,
gated behind AllowWorkflowsPermission feature flag.

Changes:
- Permissions.cs: Add Workflows property, copy constructor, comparison
  key mapping. Excluded from write-all/read-all bulk constructors.
- WorkflowTemplateConverter.cs: Parse 'workflows' permission with
  feature flag guard. Read downgrades to NoAccess (write-only scope).
- WorkflowFeatures.cs: Add AllowWorkflowsPermission flag, default false.
2026-04-10 23:33:55 +01:00
Salman Chishti 4a587ada27 feat: add job.workflow_* typed accessors to JobContext (#4335) 2026-04-10 19:39:33 +01:00
182a433782 Bump System.Formats.Asn1, Cryptography.Pkcs, ProtectedData, ServiceController, CodePages, Threading.Channels, @actions/glob, @typescript-eslint/parser, lint-staged, picomatch (#4333)
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: Salman Chishti <[email protected]>
2026-04-10 12:40:28 +01:00
15 changed files with 676 additions and 484 deletions
File diff suppressed because it is too large Load Diff
@@ -32,19 +32,19 @@
"author": "GitHub Actions", "author": "GitHub Actions",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/glob": "^0.4.0" "@actions/glob": "^0.6.1"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin": "^5.10.0", "@stylistic/eslint-plugin": "^5.10.0",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"@typescript-eslint/eslint-plugin": "^8.58.1", "@typescript-eslint/eslint-plugin": "^8.58.1",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.58.1",
"@vercel/ncc": "^0.38.3", "@vercel/ncc": "^0.38.3",
"eslint": "^8.47.0", "eslint": "^8.47.0",
"eslint-plugin-github": "^4.10.2", "eslint-plugin-github": "^4.10.2",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^15.5.0", "lint-staged": "^16.4.0",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
} }
+3 -3
View File
@@ -17,9 +17,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" /> <PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.3" />
<PackageReference Include="System.Threading.Channels" Version="8.0.0" /> <PackageReference Include="System.Threading.Channels" Version="10.0.3" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+2 -2
View File
@@ -22,8 +22,8 @@
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" /> <PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" /> <PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" /> <PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.3" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+2 -2
View File
@@ -15,9 +15,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.3" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="System.Threading.Channels" Version="8.0.0" /> <PackageReference Include="System.Threading.Channels" Version="10.0.3" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+4 -7
View File
@@ -892,15 +892,12 @@ namespace GitHub.Runner.Worker
Trace.Info("Initializing Job context"); Trace.Info("Initializing Job context");
var jobContext = new JobContext(); var jobContext = new JobContext();
if (Global.Variables.GetBoolean(Constants.Runner.Features.AddCheckRunIdToJobContext) ?? false) ExpressionValues.TryGetValue("job", out var jobDictionary);
if (jobDictionary != null)
{ {
ExpressionValues.TryGetValue("job", out var jobDictionary); foreach (var pair in jobDictionary.AssertDictionary("job"))
if (jobDictionary != null)
{ {
foreach (var pair in jobDictionary.AssertDictionary("job")) jobContext[pair.Key] = pair.Value;
{
jobContext[pair.Key] = pair.Value;
}
} }
} }
ExpressionValues["job"] = jobContext; ExpressionValues["job"] = jobContext;
+64
View File
@@ -82,5 +82,69 @@ namespace GitHub.Runner.Worker
} }
} }
} }
public string WorkflowRef
{
get
{
if (this.TryGetValue("workflow_ref", out var value) && value is StringContextData str)
{
return str.Value;
}
return null;
}
set
{
this["workflow_ref"] = value != null ? new StringContextData(value) : null;
}
}
public string WorkflowSha
{
get
{
if (this.TryGetValue("workflow_sha", out var value) && value is StringContextData str)
{
return str.Value;
}
return null;
}
set
{
this["workflow_sha"] = value != null ? new StringContextData(value) : null;
}
}
public string WorkflowRepository
{
get
{
if (this.TryGetValue("workflow_repository", out var value) && value is StringContextData str)
{
return str.Value;
}
return null;
}
set
{
this["workflow_repository"] = value != null ? new StringContextData(value) : null;
}
}
public string WorkflowFilePath
{
get
{
if (this.TryGetValue("workflow_file_path", out var value) && value is StringContextData str)
{
return str.Value;
}
return null;
}
set
{
this["workflow_file_path"] = value != null ? new StringContextData(value) : null;
}
}
} }
} }
+3 -3
View File
@@ -19,9 +19,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" /> <PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" /> <PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.3" />
<PackageReference Include="System.Threading.Channels" Version="8.0.0" /> <PackageReference Include="System.Threading.Channels" Version="10.0.3" />
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" /> <PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
<PackageReference Include="Microsoft.DevTunnels.Connections" Version="1.3.16" /> <PackageReference Include="Microsoft.DevTunnels.Connections" Version="1.3.16" />
</ItemGroup> </ItemGroup>
+3 -3
View File
@@ -23,14 +23,14 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" /> <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" /> <PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.2" /> <PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.3" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" /> <PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
<PackageReference Include="Minimatch" Version="2.0.0" /> <PackageReference Include="Minimatch" Version="2.0.0" />
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" /> <PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" /> <PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" /> <PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="System.Private.Uri" Version="4.3.2" /> <PackageReference Include="System.Private.Uri" Version="4.3.2" />
<PackageReference Include="System.Formats.Asn1" Version="10.0.2" /> <PackageReference Include="System.Formats.Asn1" Version="10.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -32,7 +32,7 @@ namespace GitHub.Actions.WorkflowParser.Conversion
return; return;
} }
var effectiveMax = explicitMax ?? CreatePermissionsFromPolicy(context, permissionsPolicy, includeIdToken: isTrusted, includeModels: context.GetFeatures().AllowModelsPermission); var effectiveMax = explicitMax ?? CreatePermissionsFromPolicy(context, permissionsPolicy, includeIdToken: isTrusted, includeModels: context.GetFeatures().AllowModelsPermission, includeWorkflows: context.GetFeatures().AllowWorkflowsPermission);
if (requested.ViolatesMaxPermissions(effectiveMax, out var permissionLevelViolations)) if (requested.ViolatesMaxPermissions(effectiveMax, out var permissionLevelViolations))
{ {
@@ -59,7 +59,8 @@ namespace GitHub.Actions.WorkflowParser.Conversion
TemplateContext context, TemplateContext context,
string permissionsPolicy, string permissionsPolicy,
bool includeIdToken, bool includeIdToken,
bool includeModels) bool includeModels,
bool includeWorkflows)
{ {
switch (permissionsPolicy) switch (permissionsPolicy)
{ {
@@ -70,7 +71,7 @@ namespace GitHub.Actions.WorkflowParser.Conversion
Packages = PermissionLevel.Read, Packages = PermissionLevel.Read,
}; };
case WorkflowConstants.PermissionsPolicy.Write: case WorkflowConstants.PermissionsPolicy.Write:
return new Permissions(PermissionLevel.Write, includeIdToken: includeIdToken, includeAttestations: true, includeModels: includeModels); return new Permissions(PermissionLevel.Write, includeIdToken: includeIdToken, includeAttestations: true, includeModels: includeModels, includeWorkflows: includeWorkflows);
default: default:
throw new ArgumentException($"Unexpected permission policy: '{permissionsPolicy}'"); throw new ArgumentException($"Unexpected permission policy: '{permissionsPolicy}'");
} }
@@ -1877,7 +1877,7 @@ namespace GitHub.Actions.WorkflowParser.Conversion
permissionsStr.AssertUnexpectedValue(permissionsStr.Value); permissionsStr.AssertUnexpectedValue(permissionsStr.Value);
break; break;
} }
return new Permissions(permissionLevel, includeIdToken: true, includeAttestations: true, includeModels: context.GetFeatures().AllowModelsPermission); return new Permissions(permissionLevel, includeIdToken: true, includeAttestations: true, includeModels: context.GetFeatures().AllowModelsPermission, includeWorkflows: context.GetFeatures().AllowWorkflowsPermission);
} }
var mapping = token.AssertMapping("permissions"); var mapping = token.AssertMapping("permissions");
@@ -1957,6 +1957,24 @@ namespace GitHub.Actions.WorkflowParser.Conversion
context.Error(key, $"The permission 'models' is not allowed"); context.Error(key, $"The permission 'models' is not allowed");
} }
break; break;
case "workflows":
if (context.GetFeatures().AllowWorkflowsPermission)
{
// Workflows only supports write; downgrade read to none
if (permissionLevel == PermissionLevel.Read)
{
permissions.Workflows = PermissionLevel.NoAccess;
}
else
{
permissions.Workflows = permissionLevel;
}
}
else
{
context.Error(key, $"The permission 'workflows' is not allowed");
}
break;
default: default:
break; break;
} }
+18 -4
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using GitHub.Actions.WorkflowParser.Conversion; using GitHub.Actions.WorkflowParser.Conversion;
@@ -17,7 +17,7 @@ namespace GitHub.Actions.WorkflowParser
public Permissions(Permissions copy) public Permissions(Permissions copy)
{ {
Actions = copy.Actions; Actions = copy.Actions;
ArtifactMetadata = copy.ArtifactMetadata; ArtifactMetadata = copy.ArtifactMetadata;
Attestations = copy.Attestations; Attestations = copy.Attestations;
Checks = copy.Checks; Checks = copy.Checks;
Contents = copy.Contents; Contents = copy.Contents;
@@ -32,16 +32,18 @@ namespace GitHub.Actions.WorkflowParser
SecurityEvents = copy.SecurityEvents; SecurityEvents = copy.SecurityEvents;
IdToken = copy.IdToken; IdToken = copy.IdToken;
Models = copy.Models; Models = copy.Models;
Workflows = copy.Workflows;
} }
public Permissions( public Permissions(
PermissionLevel permissionLevel, PermissionLevel permissionLevel,
bool includeIdToken, bool includeIdToken,
bool includeAttestations, bool includeAttestations,
bool includeModels) bool includeModels,
bool includeWorkflows = false)
{ {
Actions = permissionLevel; Actions = permissionLevel;
ArtifactMetadata = permissionLevel; ArtifactMetadata = permissionLevel;
Attestations = includeAttestations ? permissionLevel : PermissionLevel.NoAccess; Attestations = includeAttestations ? permissionLevel : PermissionLevel.NoAccess;
Checks = permissionLevel; Checks = permissionLevel;
Contents = permissionLevel; Contents = permissionLevel;
@@ -59,6 +61,10 @@ namespace GitHub.Actions.WorkflowParser
Models = includeModels Models = includeModels
? (permissionLevel == PermissionLevel.Write ? PermissionLevel.Read : permissionLevel) ? (permissionLevel == PermissionLevel.Write ? PermissionLevel.Read : permissionLevel)
: PermissionLevel.NoAccess; : PermissionLevel.NoAccess;
// Workflows is write-only, so only grant it when permissionLevel is Write
Workflows = includeWorkflows && permissionLevel == PermissionLevel.Write
? PermissionLevel.Write
: PermissionLevel.NoAccess;
} }
private static KeyValuePair<string, (PermissionLevel, PermissionLevel)>[] ComparisonKeyMapping(Permissions left, Permissions right) private static KeyValuePair<string, (PermissionLevel, PermissionLevel)>[] ComparisonKeyMapping(Permissions left, Permissions right)
@@ -81,6 +87,7 @@ namespace GitHub.Actions.WorkflowParser
new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("security-events", (left.SecurityEvents, right.SecurityEvents)), new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("security-events", (left.SecurityEvents, right.SecurityEvents)),
new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("id-token", (left.IdToken, right.IdToken)), new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("id-token", (left.IdToken, right.IdToken)),
new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("models", (left.Models, right.Models)), new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("models", (left.Models, right.Models)),
new KeyValuePair<string, (PermissionLevel, PermissionLevel)>("workflows", (left.Workflows, right.Workflows)),
}; };
} }
@@ -196,6 +203,13 @@ namespace GitHub.Actions.WorkflowParser
set; set;
} }
[DataMember(Name = "workflows", EmitDefaultValue = false)]
public PermissionLevel Workflows
{
get;
set;
}
public Permissions Clone() public Permissions Clone()
{ {
return new Permissions(this); return new Permissions(this);
+9 -1
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@@ -41,6 +41,13 @@ namespace GitHub.Actions.WorkflowParser
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public bool AllowModelsPermission { get; set; } public bool AllowModelsPermission { get; set; }
/// <summary>
/// Gets or sets a value indicating whether users may use the "workflows" permission.
/// Used during parsing only.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public bool AllowWorkflowsPermission { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the expression function fromJson performs strict JSON parsing. /// Gets or sets a value indicating whether the expression function fromJson performs strict JSON parsing.
/// Used during evaluation only. /// Used during evaluation only.
@@ -67,6 +74,7 @@ namespace GitHub.Actions.WorkflowParser
Snapshot = false, // Default to false since this feature is still in an experimental phase Snapshot = false, // Default to false since this feature is still in an experimental phase
StrictJsonParsing = false, // Default to false since this is temporary for telemetry purposes only StrictJsonParsing = false, // Default to false since this is temporary for telemetry purposes only
AllowModelsPermission = false, // Default to false since we want this to be disabled for all non-production environments AllowModelsPermission = false, // Default to false since we want this to be disabled for all non-production environments
AllowWorkflowsPermission = false, // Default to false; gated by feature flag for controlled rollout
AllowServiceContainerCommand = false, // Default to false since this feature is gated by actions_service_container_command AllowServiceContainerCommand = false, // Default to false since this feature is gated by actions_service_container_command
}; };
} }
+80 -9
View File
@@ -1203,19 +1203,19 @@ namespace GitHub.Runner.Common.Tests.Worker
} }
} }
// TODO: this test can be deleted when `AddCheckRunIdToJobContext` is fully rolled out // AddCheckRunIdToJobContext is now permanently enabled server-side (hardcoded to "true"
// in acquirejobhandler.go). The runner always copies ContextData["job"] entries, so the
// flag-disabled test is no longer applicable. Replaced with a test that verifies
// check_run_id is always hydrated regardless of the flag value.
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void InitializeJob_HydratesJobContextWithCheckRunId_FeatureFlagDisabled() public void InitializeJob_HydratesJobContextWithCheckRunId_AlwaysCopied()
{ {
using (TestHostContext hc = CreateTestContext()) using (TestHostContext hc = CreateTestContext())
{ {
// Arrange: Create a job request message and make sure the feature flag is disabled // Arrange: No feature flag set at all
var variables = new Dictionary<string, VariableValue>() var variables = new Dictionary<string, VariableValue>();
{
[Constants.Runner.Features.AddCheckRunIdToJobContext] = new VariableValue("false"),
};
var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null); var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
var pagingLogger = new Moq.Mock<IPagingLogger>(); var pagingLogger = new Moq.Mock<IPagingLogger>();
var jobServerQueue = new Moq.Mock<IJobServerQueue>(); var jobServerQueue = new Moq.Mock<IJobServerQueue>();
@@ -1233,9 +1233,80 @@ namespace GitHub.Runner.Common.Tests.Worker
// Act // Act
ec.InitializeJob(jobRequest, CancellationToken.None); ec.InitializeJob(jobRequest, CancellationToken.None);
// Assert // Assert: check_run_id is always copied regardless of flag
Assert.NotNull(ec.JobContext); Assert.NotNull(ec.JobContext);
Assert.Null(ec.JobContext.CheckRunId); // with the feature flag disabled we should not have added a CheckRunId to the JobContext Assert.Equal(123456, ec.JobContext.CheckRunId);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void InitializeJob_HydratesJobContextWithWorkflowIdentity()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange
var variables = new Dictionary<string, VariableValue>();
var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
var pagingLogger = new Moq.Mock<IPagingLogger>();
var jobServerQueue = new Moq.Mock<IJobServerQueue>();
hc.EnqueueInstance(pagingLogger.Object);
hc.SetSingleton(jobServerQueue.Object);
var ec = new Runner.Worker.ExecutionContext();
ec.Initialize(hc);
// Arrange: Server sends all 4 workflow identity fields
var jobContext = new Pipelines.ContextData.DictionaryContextData();
jobContext["workflow_ref"] = new StringContextData("my-org/my-repo/.github/workflows/reusable.yml@refs/heads/main");
jobContext["workflow_sha"] = new StringContextData("abc123def456");
jobContext["workflow_repository"] = new StringContextData("my-org/my-repo");
jobContext["workflow_file_path"] = new StringContextData(".github/workflows/reusable.yml");
jobRequest.ContextData["job"] = jobContext;
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
// Act
ec.InitializeJob(jobRequest, CancellationToken.None);
// Assert: all properties hydrated from server
Assert.NotNull(ec.JobContext);
Assert.Equal("my-org/my-repo/.github/workflows/reusable.yml@refs/heads/main", ec.JobContext.WorkflowRef);
Assert.Equal("abc123def456", ec.JobContext.WorkflowSha);
Assert.Equal("my-org/my-repo", ec.JobContext.WorkflowRepository);
Assert.Equal(".github/workflows/reusable.yml", ec.JobContext.WorkflowFilePath);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void InitializeJob_WorkflowIdentityNotSet_WhenServerSendsNoData()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange: Server sends no workflow identity in job context
var variables = new Dictionary<string, VariableValue>();
var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
var pagingLogger = new Moq.Mock<IPagingLogger>();
var jobServerQueue = new Moq.Mock<IJobServerQueue>();
hc.EnqueueInstance(pagingLogger.Object);
hc.SetSingleton(jobServerQueue.Object);
var ec = new Runner.Worker.ExecutionContext();
ec.Initialize(hc);
// Arrange: empty job context
jobRequest.ContextData["job"] = new Pipelines.ContextData.DictionaryContextData();
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
// Act
ec.InitializeJob(jobRequest, CancellationToken.None);
// Assert: no workflow identity
Assert.NotNull(ec.JobContext);
Assert.Null(ec.JobContext.WorkflowRef);
Assert.Null(ec.JobContext.WorkflowSha);
Assert.Null(ec.JobContext.WorkflowRepository);
Assert.Null(ec.JobContext.WorkflowFilePath);
} }
} }
+104
View File
@@ -34,5 +34,109 @@ namespace GitHub.Runner.Common.Tests.Worker
ctx.CheckRunId = null; ctx.CheckRunId = null;
Assert.Null(ctx.CheckRunId); Assert.Null(ctx.CheckRunId);
} }
[Fact]
public void WorkflowRef_SetAndGet_WorksCorrectly()
{
var ctx = new JobContext();
ctx.WorkflowRef = "owner/repo/.github/workflows/ci.yml@refs/heads/main";
Assert.Equal("owner/repo/.github/workflows/ci.yml@refs/heads/main", ctx.WorkflowRef);
Assert.True(ctx.TryGetValue("workflow_ref", out var value));
Assert.IsType<StringContextData>(value);
}
[Fact]
public void WorkflowRef_NotSet_ReturnsNull()
{
var ctx = new JobContext();
Assert.Null(ctx.WorkflowRef);
}
[Fact]
public void WorkflowRef_SetNull_ClearsValue()
{
var ctx = new JobContext();
ctx.WorkflowRef = "owner/repo/.github/workflows/ci.yml@refs/heads/main";
ctx.WorkflowRef = null;
Assert.Null(ctx.WorkflowRef);
}
[Fact]
public void WorkflowSha_SetAndGet_WorksCorrectly()
{
var ctx = new JobContext();
ctx.WorkflowSha = "abc123def456";
Assert.Equal("abc123def456", ctx.WorkflowSha);
Assert.True(ctx.TryGetValue("workflow_sha", out var value));
Assert.IsType<StringContextData>(value);
}
[Fact]
public void WorkflowSha_NotSet_ReturnsNull()
{
var ctx = new JobContext();
Assert.Null(ctx.WorkflowSha);
}
[Fact]
public void WorkflowSha_SetNull_ClearsValue()
{
var ctx = new JobContext();
ctx.WorkflowSha = "abc123def456";
ctx.WorkflowSha = null;
Assert.Null(ctx.WorkflowSha);
}
[Fact]
public void WorkflowRepository_SetAndGet_WorksCorrectly()
{
var ctx = new JobContext();
ctx.WorkflowRepository = "owner/repo";
Assert.Equal("owner/repo", ctx.WorkflowRepository);
Assert.True(ctx.TryGetValue("workflow_repository", out var value));
Assert.IsType<StringContextData>(value);
}
[Fact]
public void WorkflowRepository_NotSet_ReturnsNull()
{
var ctx = new JobContext();
Assert.Null(ctx.WorkflowRepository);
}
[Fact]
public void WorkflowRepository_SetNull_ClearsValue()
{
var ctx = new JobContext();
ctx.WorkflowRepository = "owner/repo";
ctx.WorkflowRepository = null;
Assert.Null(ctx.WorkflowRepository);
}
[Fact]
public void WorkflowFilePath_SetAndGet_WorksCorrectly()
{
var ctx = new JobContext();
ctx.WorkflowFilePath = ".github/workflows/ci.yml";
Assert.Equal(".github/workflows/ci.yml", ctx.WorkflowFilePath);
Assert.True(ctx.TryGetValue("workflow_file_path", out var value));
Assert.IsType<StringContextData>(value);
}
[Fact]
public void WorkflowFilePath_NotSet_ReturnsNull()
{
var ctx = new JobContext();
Assert.Null(ctx.WorkflowFilePath);
}
[Fact]
public void WorkflowFilePath_SetNull_ClearsValue()
{
var ctx = new JobContext();
ctx.WorkflowFilePath = ".github/workflows/ci.yml";
ctx.WorkflowFilePath = null;
Assert.Null(ctx.WorkflowFilePath);
}
} }
} }