diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index 6acb3e385d5..4bdf3baf975 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -892,15 +892,12 @@ public void InitializeJob(Pipelines.AgentJobRequestMessage message, Cancellation Trace.Info("Initializing Job context"); 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); - if (jobDictionary != null) + foreach (var pair in jobDictionary.AssertDictionary("job")) { - foreach (var pair in jobDictionary.AssertDictionary("job")) - { - jobContext[pair.Key] = pair.Value; - } + jobContext[pair.Key] = pair.Value; } } ExpressionValues["job"] = jobContext; diff --git a/src/Runner.Worker/JobContext.cs b/src/Runner.Worker/JobContext.cs index 09f3296de9c..7150f2e8bc1 100644 --- a/src/Runner.Worker/JobContext.cs +++ b/src/Runner.Worker/JobContext.cs @@ -82,5 +82,69 @@ public double? CheckRunId } } } + + 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; + } + } } } diff --git a/src/Test/L0/Worker/ExecutionContextL0.cs b/src/Test/L0/Worker/ExecutionContextL0.cs index 2f28f797fb5..d35be3accb8 100644 --- a/src/Test/L0/Worker/ExecutionContextL0.cs +++ b/src/Test/L0/Worker/ExecutionContextL0.cs @@ -1203,19 +1203,19 @@ public void InitializeJob_HydratesJobContextWithCheckRunId() } } - // 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] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public void InitializeJob_HydratesJobContextWithCheckRunId_FeatureFlagDisabled() + public void InitializeJob_HydratesJobContextWithCheckRunId_AlwaysCopied() { using (TestHostContext hc = CreateTestContext()) { - // Arrange: Create a job request message and make sure the feature flag is disabled - var variables = new Dictionary() - { - [Constants.Runner.Features.AddCheckRunIdToJobContext] = new VariableValue("false"), - }; + // Arrange: No feature flag set at all + var variables = new Dictionary(); var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); var pagingLogger = new Moq.Mock(); var jobServerQueue = new Moq.Mock(); @@ -1233,9 +1233,80 @@ public void InitializeJob_HydratesJobContextWithCheckRunId_FeatureFlagDisabled() // Act ec.InitializeJob(jobRequest, CancellationToken.None); - // Assert + // Assert: check_run_id is always copied regardless of flag + Assert.NotNull(ec.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(); + var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); + var pagingLogger = new Moq.Mock(); + var jobServerQueue = new Moq.Mock(); + 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(); + var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); + var pagingLogger = new Moq.Mock(); + var jobServerQueue = new Moq.Mock(); + 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.CheckRunId); // with the feature flag disabled we should not have added a CheckRunId to the JobContext + Assert.Null(ec.JobContext.WorkflowRef); + Assert.Null(ec.JobContext.WorkflowSha); + Assert.Null(ec.JobContext.WorkflowRepository); + Assert.Null(ec.JobContext.WorkflowFilePath); } } diff --git a/src/Test/L0/Worker/JobContextL0.cs b/src/Test/L0/Worker/JobContextL0.cs index 87e33437914..a99d841a626 100644 --- a/src/Test/L0/Worker/JobContextL0.cs +++ b/src/Test/L0/Worker/JobContextL0.cs @@ -34,5 +34,109 @@ public void CheckRunId_SetNull_RemovesKey() ctx.CheckRunId = null; 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(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(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(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(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); + } } }