Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/Runner.Worker/ActionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,11 @@ private async Task BuildActionContainerAsync(IExecutionContext executionContext,
return new Dictionary<string, WebApi.ActionDownloadInfo>();
}

// Pass lockfile dependencies to Launch when present, so it can
// perform ref-scoped policy matching with the original refs.
var deps = executionContext.Global.ActionsDependencies;
IList<string> dependencies = (deps != null && deps.Count > 0) ? deps : null;

// Resolve download info
var launchServer = HostContext.GetService<ILaunchServer>();
var jobServer = HostContext.GetService<IJobServer>();
Expand All @@ -891,7 +896,7 @@ private async Task BuildActionContainerAsync(IExecutionContext executionContext,
if (MessageUtil.IsRunServiceJob(executionContext.Global.Variables.Get(Constants.Variables.System.JobRequestType)))
{
var displayHelpfulActionsDownloadErrors = executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.DisplayHelpfulActionsDownloadErrors) ?? false;
actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken, displayHelpfulActionsDownloadErrors);
actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences, Dependencies = dependencies }, executionContext.CancellationToken, displayHelpfulActionsDownloadErrors);
}
Comment thread
nodeselector marked this conversation as resolved.
else
{
Expand Down
3 changes: 3 additions & 0 deletions src/Runner.Worker/ExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,9 @@ public void InitializeJob(Pipelines.AgentJobRequestMessage message, Cancellation
// File table
Global.FileTable = new List<String>(message.FileTable ?? new string[0]);

// Workflow dependencies (lockfile pins)
Global.ActionsDependencies = message.ActionsDependencies;

// What type of job request is running (i.e. Run Service vs. pipelines)
Global.Variables.Set(Constants.Variables.System.JobRequestType, message.MessageType);

Expand Down
1 change: 1 addition & 0 deletions src/Runner.Worker/GlobalContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ public sealed class GlobalContext
public HashSet<string> DeprecatedNode20Actions { get; set; }
public HashSet<string> UpgradedToNode24Actions { get; set; }
public HashSet<string> Arm32Node20Actions { get; set; }
public IList<String> ActionsDependencies { get; set; }
}
}
23 changes: 23 additions & 0 deletions src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,21 @@ public DebuggerTunnelInfo DebuggerTunnel
set;
}

/// <summary>
/// Gets the workflow-level action dependencies (lockfile entries)
/// </summary>
public IList<String> ActionsDependencies
{
get
{
if (m_actionsDependencies == null)
{
m_actionsDependencies = new List<String>();
}
return m_actionsDependencies;
}
}

/// <summary>
/// Gets the collection of variables associated with the current context.
/// </summary>
Expand Down Expand Up @@ -441,6 +456,11 @@ private void OnSerializing(StreamingContext context)
m_variables = null;
}

if (m_actionsDependencies?.Count == 0)
{
m_actionsDependencies = null;
}

// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
if (!string.IsNullOrEmpty(m_jobContainerResourceAlias))
{
Expand All @@ -466,6 +486,9 @@ private void OnSerializing(StreamingContext context)
[DataMember(Name = "Variables", EmitDefaultValue = false)]
private IDictionary<String, VariableValue> m_variables;

[DataMember(Name = "dependencies", EmitDefaultValue = false)]
private List<String> m_actionsDependencies;

// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
[DataMember(Name = "JobSidecarContainers", EmitDefaultValue = false)]
private IDictionary<String, String> m_jobSidecarContainers;
Expand Down
7 changes: 7 additions & 0 deletions src/Sdk/DTWebApi/WebApi/ActionReferenceList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,12 @@ public IList<ActionReference> Actions
get;
set;
}

[DataMember(EmitDefaultValue = false)]
public IList<string> Dependencies
{
get;
set;
}
}
}
3 changes: 3 additions & 0 deletions src/Sdk/WebApi/WebApi/LaunchContracts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public class ActionReferenceRequestList
{
[DataMember(EmitDefaultValue = false, Name = "actions")]
public IList<ActionReferenceRequest> Actions { get; set; }

[DataMember(EmitDefaultValue = false, Name = "actions_dependencies")]
public IList<string> ActionsDependencies { get; set; }
}

[DataContract]
Expand Down
3 changes: 2 additions & 1 deletion src/Sdk/WebApi/WebApi/LaunchHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ private static ActionReferenceRequestList ToGitHubData(ActionReferenceList actio
{
return new ActionReferenceRequestList
{
Actions = actionReferenceList.Actions?.Select(ToGitHubData).ToList()
Actions = actionReferenceList.Actions?.Select(ToGitHubData).ToList(),
ActionsDependencies = actionReferenceList.Dependencies
};
}

Expand Down
42 changes: 42 additions & 0 deletions src/Test/L0/Sdk/RSWebApi/AgentJobRequestMessageL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,48 @@ public void VerifyDebuggerTunnelDeserialization_WithoutTunnel()
Assert.Null(recoveredMessage.DebuggerTunnel);
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyActionsDependenciesDeserialization_WithDependencies()
{
// Arrange
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
string json = DoubleQuotify("{'dependencies': ['actions/checkout@v4:sha256-abc123', 'actions/setup-node@v4:sha256-def456']}");

// Act
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes(json));
stream.Position = 0;
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;

// Assert
Assert.NotNull(recoveredMessage);
Assert.Equal(2, recoveredMessage.ActionsDependencies.Count);
Assert.Equal("actions/checkout@v4:sha256-abc123", recoveredMessage.ActionsDependencies[0]);
Assert.Equal("actions/setup-node@v4:sha256-def456", recoveredMessage.ActionsDependencies[1]);
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyActionsDependenciesDeserialization_DefaultsToEmpty()
{
// Arrange
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
string json = DoubleQuotify("{'messageType': 'PipelineAgentJobRequest'}");

// Act
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes(json));
stream.Position = 0;
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;

// Assert
Assert.NotNull(recoveredMessage);
Assert.Empty(recoveredMessage.ActionsDependencies);
}

private static string DoubleQuotify(string text)
{
return text.Replace('\'', '"');
Expand Down
136 changes: 136 additions & 0 deletions src/Test/L0/Worker/ActionManagerL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3283,5 +3283,141 @@ private void Teardown()
Directory.Delete(_workFolder, recursive: true);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task GetDownloadInfoAsync_PropagatesDependencies_WhenPresent()
{
try
{
// Arrange
Setup();

// Set RunServiceJob so we hit the Launch path
_ec.Object.Global.Variables.Set(Constants.Variables.System.JobRequestType, "RunnerJobRequest");

// Populate lockfile dependencies
_ec.Object.Global.ActionsDependencies = new List<string>
{
"github.com/actions/checkout@v4:sha256-abc123",
"github.com/actions/setup-node@v4:sha256-def456"
};

// Capture the ActionReferenceList passed to Launch
ActionReferenceList capturedList = null;
_launchServer
.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>(), It.IsAny<bool>()))
.Callback<Guid, Guid, ActionReferenceList, CancellationToken, bool>((planId, jobId, list, ct, display) => capturedList = list)
.Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken ct, bool display) =>
{
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
foreach (var action in actions.Actions)
{
var key = $"{action.NameWithOwner}@{action.Ref}";
result.Actions[key] = new ActionDownloadInfo
{
NameWithOwner = action.NameWithOwner,
Ref = action.Ref,
ResolvedNameWithOwner = action.NameWithOwner,
ResolvedSha = $"{action.Ref}-sha",
TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}",
ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}",
};
}
return Task.FromResult(result);
});

var actionStep = new Pipelines.ActionStep()
{
Name = "action",
Id = Guid.NewGuid(),
Reference = new Pipelines.RepositoryPathReference()
{
Name = "actions/checkout",
Ref = "v4",
RepositoryType = "GitHub"
}
};

// Act
var result = await _actionManager.PrepareActionsAsync(_ec.Object, new List<Pipelines.JobStep> { actionStep }, default);

// Assert
Assert.NotNull(capturedList);
Assert.NotNull(capturedList.Dependencies);
Assert.Equal(2, capturedList.Dependencies.Count);
Assert.Equal("github.com/actions/checkout@v4:sha256-abc123", capturedList.Dependencies[0]);
Assert.Equal("github.com/actions/setup-node@v4:sha256-def456", capturedList.Dependencies[1]);
}
finally
{
Teardown();
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task GetDownloadInfoAsync_OmitsDependencies_WhenEmpty()
{
try
{
// Arrange
Setup();

// Set RunServiceJob so we hit the Launch path
_ec.Object.Global.Variables.Set(Constants.Variables.System.JobRequestType, "RunnerJobRequest");

// No dependencies set (default empty list from GlobalContext)

// Capture the ActionReferenceList passed to Launch
ActionReferenceList capturedList = null;
_launchServer
.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>(), It.IsAny<bool>()))
.Callback<Guid, Guid, ActionReferenceList, CancellationToken, bool>((planId, jobId, list, ct, display) => capturedList = list)
.Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken ct, bool display) =>
{
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
foreach (var action in actions.Actions)
{
var key = $"{action.NameWithOwner}@{action.Ref}";
result.Actions[key] = new ActionDownloadInfo
{
NameWithOwner = action.NameWithOwner,
Ref = action.Ref,
ResolvedNameWithOwner = action.NameWithOwner,
ResolvedSha = $"{action.Ref}-sha",
TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}",
ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}",
};
}
return Task.FromResult(result);
});

var actionStep = new Pipelines.ActionStep()
{
Name = "action",
Id = Guid.NewGuid(),
Reference = new Pipelines.RepositoryPathReference()
{
Name = "actions/checkout",
Ref = "v4",
RepositoryType = "GitHub"
}
};

// Act
var result = await _actionManager.PrepareActionsAsync(_ec.Object, new List<Pipelines.JobStep> { actionStep }, default);

// Assert
Assert.NotNull(capturedList);
Assert.Null(capturedList.Dependencies);
}
finally
{
Teardown();
}
}
}
}
Loading