feat: propagate actions dependencies (#4372)
Code Scanning - Action / CodeQL-Build (push) Has been cancelled
Runner CI / build (./dev, windows-latest, win-arm64) (push) Has been cancelled
Runner CI / build (./dev, windows-latest, win-x64) (push) Has been cancelled
Runner CI / build (./dev.sh, macOS-latest, osx-arm64) (push) Has been cancelled
Runner CI / build (./dev.sh, macOS-latest, osx-x64) (push) Has been cancelled
Runner CI / build (./dev.sh, ubuntu-latest, linux-arm) (push) Has been cancelled
Runner CI / build (./dev.sh, ubuntu-latest, linux-arm64) (push) Has been cancelled
Runner CI / build (./dev.sh, ubuntu-latest, linux-x64) (push) Has been cancelled
Runner CI / docker (linux/amd64, ubuntu-latest) (push) Has been cancelled
Runner CI / docker (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Code Scanning - Action / CodeQL-Build (push) Has been cancelled
Runner CI / build (./dev, windows-latest, win-arm64) (push) Has been cancelled
Runner CI / build (./dev, windows-latest, win-x64) (push) Has been cancelled
Runner CI / build (./dev.sh, macOS-latest, osx-arm64) (push) Has been cancelled
Runner CI / build (./dev.sh, macOS-latest, osx-x64) (push) Has been cancelled
Runner CI / build (./dev.sh, ubuntu-latest, linux-arm) (push) Has been cancelled
Runner CI / build (./dev.sh, ubuntu-latest, linux-arm64) (push) Has been cancelled
Runner CI / build (./dev.sh, ubuntu-latest, linux-x64) (push) Has been cancelled
Runner CI / docker (linux/amd64, ubuntu-latest) (push) Has been cancelled
Runner CI / docker (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
This commit is contained in:
@@ -880,6 +880,11 @@ namespace GitHub.Runner.Worker
|
|||||||
return new Dictionary<string, WebApi.ActionDownloadInfo>();
|
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
|
// Resolve download info
|
||||||
var launchServer = HostContext.GetService<ILaunchServer>();
|
var launchServer = HostContext.GetService<ILaunchServer>();
|
||||||
var jobServer = HostContext.GetService<IJobServer>();
|
var jobServer = HostContext.GetService<IJobServer>();
|
||||||
@@ -891,7 +896,7 @@ namespace GitHub.Runner.Worker
|
|||||||
if (MessageUtil.IsRunServiceJob(executionContext.Global.Variables.Get(Constants.Variables.System.JobRequestType)))
|
if (MessageUtil.IsRunServiceJob(executionContext.Global.Variables.Get(Constants.Variables.System.JobRequestType)))
|
||||||
{
|
{
|
||||||
var displayHelpfulActionsDownloadErrors = executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.DisplayHelpfulActionsDownloadErrors) ?? false;
|
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);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -875,6 +875,9 @@ namespace GitHub.Runner.Worker
|
|||||||
// File table
|
// File table
|
||||||
Global.FileTable = new List<String>(message.FileTable ?? new string[0]);
|
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)
|
// What type of job request is running (i.e. Run Service vs. pipelines)
|
||||||
Global.Variables.Set(Constants.Variables.System.JobRequestType, message.MessageType);
|
Global.Variables.Set(Constants.Variables.System.JobRequestType, message.MessageType);
|
||||||
|
|
||||||
|
|||||||
@@ -38,5 +38,6 @@ namespace GitHub.Runner.Worker
|
|||||||
public HashSet<string> DeprecatedNode20Actions { get; set; }
|
public HashSet<string> DeprecatedNode20Actions { get; set; }
|
||||||
public HashSet<string> UpgradedToNode24Actions { get; set; }
|
public HashSet<string> UpgradedToNode24Actions { get; set; }
|
||||||
public HashSet<string> Arm32Node20Actions { get; set; }
|
public HashSet<string> Arm32Node20Actions { get; set; }
|
||||||
|
public IList<String> ActionsDependencies { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,6 +267,21 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
set;
|
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>
|
/// <summary>
|
||||||
/// Gets the collection of variables associated with the current context.
|
/// Gets the collection of variables associated with the current context.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -441,6 +456,11 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
m_variables = null;
|
m_variables = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_actionsDependencies?.Count == 0)
|
||||||
|
{
|
||||||
|
m_actionsDependencies = null;
|
||||||
|
}
|
||||||
|
|
||||||
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
||||||
if (!string.IsNullOrEmpty(m_jobContainerResourceAlias))
|
if (!string.IsNullOrEmpty(m_jobContainerResourceAlias))
|
||||||
{
|
{
|
||||||
@@ -466,6 +486,9 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
[DataMember(Name = "Variables", EmitDefaultValue = false)]
|
[DataMember(Name = "Variables", EmitDefaultValue = false)]
|
||||||
private IDictionary<String, VariableValue> m_variables;
|
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
|
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
||||||
[DataMember(Name = "JobSidecarContainers", EmitDefaultValue = false)]
|
[DataMember(Name = "JobSidecarContainers", EmitDefaultValue = false)]
|
||||||
private IDictionary<String, String> m_jobSidecarContainers;
|
private IDictionary<String, String> m_jobSidecarContainers;
|
||||||
|
|||||||
@@ -12,5 +12,12 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public IList<string> Dependencies
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ namespace GitHub.Services.Launch.Contracts
|
|||||||
{
|
{
|
||||||
[DataMember(EmitDefaultValue = false, Name = "actions")]
|
[DataMember(EmitDefaultValue = false, Name = "actions")]
|
||||||
public IList<ActionReferenceRequest> Actions { get; set; }
|
public IList<ActionReferenceRequest> Actions { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false, Name = "actions_dependencies")]
|
||||||
|
public IList<string> ActionsDependencies { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ namespace GitHub.Services.Launch.Client
|
|||||||
{
|
{
|
||||||
return new ActionReferenceRequestList
|
return new ActionReferenceRequestList
|
||||||
{
|
{
|
||||||
Actions = actionReferenceList.Actions?.Select(ToGitHubData).ToList()
|
Actions = actionReferenceList.Actions?.Select(ToGitHubData).ToList(),
|
||||||
|
ActionsDependencies = actionReferenceList.Dependencies
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,48 @@ public sealed class AgentJobRequestMessageL0
|
|||||||
Assert.Null(recoveredMessage.DebuggerTunnel);
|
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)
|
private static string DoubleQuotify(string text)
|
||||||
{
|
{
|
||||||
return text.Replace('\'', '"');
|
return text.Replace('\'', '"');
|
||||||
|
|||||||
@@ -3283,5 +3283,141 @@ runs:
|
|||||||
Directory.Delete(_workFolder, recursive: true);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user