mirror of
https://github.com/actions/runner.git
synced 2026-07-04 19:45:31 +08:00
Compare commits
8 Commits
dependabot
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dde968bf57 | ||
|
|
0e31cd5ff7 | ||
|
|
4c6d85cfc0 | ||
|
|
1ed4f70ee9 | ||
|
|
c814d7ca46 | ||
|
|
302ff10861 | ||
|
|
74aa458a12 | ||
|
|
c057cc3886 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# Build runner layout
|
||||
- name: Build & Layout Release
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
docker_platform: linux/arm64
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Get latest runner version
|
||||
id: latest_runner
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
2
.github/workflows/dependency-check.yml
vendored
2
.github/workflows/dependency-check.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
npm-vulnerabilities: ${{ steps.check-versions.outputs.npm-vulnerabilities }}
|
||||
open-dependency-prs: ${{ steps.check-prs.outputs.open-dependency-prs }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
|
||||
4
.github/workflows/docker-buildx-upgrade.yml
vendored
4
.github/workflows/docker-buildx-upgrade.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
BUILDX_CURRENT_VERSION: ${{ steps.check_buildx_version.outputs.CURRENT_VERSION }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Check Docker version
|
||||
id: check_docker_version
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Update Docker version
|
||||
shell: bash
|
||||
|
||||
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.inputs.releaseBranch }}
|
||||
|
||||
|
||||
4
.github/workflows/dotnet-upgrade.yml
vendored
4
.github/workflows/dotnet-upgrade.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
DOTNET_CURRENT_MAJOR_MINOR_VERSION: ${{ steps.fetch_current_version.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Get current major minor version
|
||||
id: fetch_current_version
|
||||
shell: bash
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
if: ${{ needs.dotnet-update.outputs.SHOULD_UPDATE == 1 && needs.dotnet-update.outputs.BRANCH_EXISTS == 0 }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
|
||||
- name: Create Pull Request
|
||||
|
||||
2
.github/workflows/node-upgrade.yml
vendored
2
.github/workflows/node-upgrade.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
update-node:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Get latest Node versions
|
||||
id: node-versions
|
||||
run: |
|
||||
|
||||
2
.github/workflows/npm-audit-typescript.yml
vendored
2
.github/workflows/npm-audit-typescript.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
npm-audit-with-ts-fix:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
|
||||
2
.github/workflows/npm-audit.yml
vendored
2
.github/workflows/npm-audit.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
npm-audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# Make sure ./releaseVersion match ./src/runnerversion
|
||||
# Query GitHub release ensure version is not used
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# Build runner layout
|
||||
- name: Build & Layout Release
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# Download runner package tar.gz/zip produced by 'build' job
|
||||
- name: Download Artifact (win-x64)
|
||||
@@ -296,7 +296,7 @@ jobs:
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Compute image version
|
||||
id: image
|
||||
|
||||
@@ -5,8 +5,8 @@ ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG RUNNER_VERSION
|
||||
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
||||
ARG DOCKER_VERSION=29.5.3
|
||||
ARG BUILDX_VERSION=0.34.1
|
||||
ARG DOCKER_VERSION=29.6.1
|
||||
ARG BUILDX_VERSION=0.35.0
|
||||
|
||||
RUN apt update -y && apt install curl unzip -y
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
|
||||
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
|
||||
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
|
||||
NODE20_VERSION="20.20.2"
|
||||
NODE24_VERSION="24.16.0"
|
||||
NODE24_VERSION="24.18.0"
|
||||
|
||||
get_abs_path() {
|
||||
# exploits the fact that pwd will print abs path when no args
|
||||
|
||||
@@ -180,6 +180,7 @@ namespace GitHub.Runner.Common
|
||||
public static readonly string BatchActionResolution = "actions_batch_action_resolution";
|
||||
public static readonly string UseBearerTokenForCodeload = "actions_use_bearer_token_for_codeload";
|
||||
public static readonly string OverrideDebuggerWelcomeMessage = "actions_runner_override_debugger_welcome_message";
|
||||
public static readonly string SelfRepository = "actions_self_repository";
|
||||
}
|
||||
|
||||
// Node version migration related constants
|
||||
|
||||
@@ -178,7 +178,7 @@ namespace GitHub.Runner.Worker
|
||||
return new PrepareResult(containerSetupSteps, result.PreStepTracker);
|
||||
}
|
||||
|
||||
private async Task<PrepareActionsState> PrepareActionsRecursiveAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable<Pipelines.ActionStep> actions, Dictionary<string, WebApi.ActionDownloadInfo> resolvedDownloadInfos, Int32 depth = 0, Guid parentStepId = default(Guid))
|
||||
private async Task<PrepareActionsState> PrepareActionsRecursiveAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable<Pipelines.ActionStep> actions, Dictionary<string, WebApi.ActionDownloadInfo> resolvedDownloadInfos, Int32 depth = 0, Guid parentStepId = default(Guid), string selfRepoName = null, string selfRepoRef = null)
|
||||
{
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
if (depth > Constants.CompositeActionsMaxDepth)
|
||||
@@ -186,6 +186,21 @@ namespace GitHub.Runner.Worker
|
||||
throw new Exception($"Composite action depth exceeded max depth {Constants.CompositeActionsMaxDepth}");
|
||||
}
|
||||
|
||||
// Resolve self-repository ($/) references before processing
|
||||
if (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SelfRepository) == true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(selfRepoName))
|
||||
{
|
||||
// job.workflow_repository/workflow_sha point to the repo
|
||||
// containing the workflow file — correct for both regular
|
||||
// and reusable workflows. Always present when the server
|
||||
// supports $/. See: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#github-context
|
||||
selfRepoName = executionContext.JobContext?.WorkflowRepository;
|
||||
selfRepoRef = executionContext.JobContext?.WorkflowSha;
|
||||
}
|
||||
ResolveSelfRepositoryReferences(executionContext, actions, selfRepoName, selfRepoRef);
|
||||
}
|
||||
|
||||
var repositoryActions = new List<Pipelines.ActionStep>();
|
||||
|
||||
foreach (var action in actions)
|
||||
@@ -228,7 +243,30 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
throw new Exception($"Missing download info for {lookupKey}");
|
||||
}
|
||||
await DownloadRepositoryActionAsync(executionContext, downloadInfo);
|
||||
|
||||
Exception downloadFailure = null;
|
||||
try
|
||||
{
|
||||
await DownloadRepositoryActionAsync(executionContext, downloadInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// record the exception for telemetry, and rethrow the original exception to fail the step.
|
||||
downloadFailure = ex;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
|
||||
{
|
||||
Type = JobTelemetryType.General,
|
||||
Message = $"resolve_download_actions_telemetry:{StringUtil.ConvertToJson(new ActionTelemetryPayload
|
||||
{
|
||||
Operation = "download_action",
|
||||
Result = downloadFailure == null ? "succeeded" : downloadFailure.GetType().Name
|
||||
}, Newtonsoft.Json.Formatting.None)}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Parse action.yml and collect composite sub-actions for batched
|
||||
@@ -278,16 +316,53 @@ namespace GitHub.Runner.Worker
|
||||
// then recurse per parent (which hits the cache, not the API).
|
||||
if (nextLevel.Count > 0)
|
||||
{
|
||||
var nextLevelRepoActions = nextLevel
|
||||
.Where(x => x.action.Reference.Type == Pipelines.ActionSourceType.Repository)
|
||||
.Select(x => x.action)
|
||||
.ToList();
|
||||
await ResolveNewActionsAsync(executionContext, nextLevelRepoActions, resolvedDownloadInfos);
|
||||
|
||||
foreach (var group in nextLevel.GroupBy(x => x.parentId))
|
||||
if (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SelfRepository) == true)
|
||||
{
|
||||
var groupActions = group.Select(x => x.action).ToList();
|
||||
state = await PrepareActionsRecursiveAsync(executionContext, state, groupActions, resolvedDownloadInfos, depth + 1, group.Key);
|
||||
// Self-repository path: group by parent so each group's
|
||||
// $/ refs resolve against the correct parent repo context.
|
||||
var groups = nextLevel.GroupBy(x => x.parentId).Select(group =>
|
||||
{
|
||||
string childRepoName = selfRepoName;
|
||||
string childRepoRef = selfRepoRef;
|
||||
var parentAction = repositoryActions.FirstOrDefault(a => a.Id == group.Key);
|
||||
if (parentAction?.Reference is Pipelines.RepositoryPathReference parentRef &&
|
||||
string.Equals(parentRef.RepositoryType, Pipelines.RepositoryTypes.GitHub, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
childRepoName = parentRef.Name;
|
||||
childRepoRef = parentRef.Ref;
|
||||
}
|
||||
return new { ParentId = group.Key, Actions = group.Select(x => x.action).ToList(), RepoName = childRepoName, RepoRef = childRepoRef };
|
||||
}).ToList();
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
ResolveSelfRepositoryReferences(executionContext, group.Actions, group.RepoName, group.RepoRef);
|
||||
}
|
||||
|
||||
var nextLevelRepoActions = nextLevel
|
||||
.Where(x => x.action.Reference.Type == Pipelines.ActionSourceType.Repository)
|
||||
.Select(x => x.action)
|
||||
.ToList();
|
||||
await ResolveNewActionsAsync(executionContext, nextLevelRepoActions, resolvedDownloadInfos);
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
state = await PrepareActionsRecursiveAsync(executionContext, state, group.Actions, resolvedDownloadInfos, depth + 1, group.ParentId, group.RepoName, group.RepoRef);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Original path: no self-repository resolution needed.
|
||||
var nextLevelActions = nextLevel.Select(x => x.action).ToList();
|
||||
var nextLevelRepoActions = nextLevelActions
|
||||
.Where(x => x.Reference.Type == Pipelines.ActionSourceType.Repository)
|
||||
.ToList();
|
||||
await ResolveNewActionsAsync(executionContext, nextLevelRepoActions, resolvedDownloadInfos);
|
||||
|
||||
foreach (var grp in nextLevel.GroupBy(x => x.parentId))
|
||||
{
|
||||
state = await PrepareActionsRecursiveAsync(executionContext, state, grp.Select(x => x.action).ToList(), resolvedDownloadInfos, depth + 1, grp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,13 +438,25 @@ namespace GitHub.Runner.Worker
|
||||
/// sub-actions individually, with no cross-depth deduplication.
|
||||
/// Used when the BatchActionResolution feature flag is disabled.
|
||||
/// </summary>
|
||||
private async Task<PrepareActionsState> PrepareActionsRecursiveLegacyAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable<Pipelines.ActionStep> actions, Int32 depth = 0, Guid parentStepId = default(Guid))
|
||||
private async Task<PrepareActionsState> PrepareActionsRecursiveLegacyAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable<Pipelines.ActionStep> actions, Int32 depth = 0, Guid parentStepId = default(Guid), string selfRepoName = null, string selfRepoRef = null)
|
||||
{
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
if (depth > Constants.CompositeActionsMaxDepth)
|
||||
{
|
||||
throw new Exception($"Composite action depth exceeded max depth {Constants.CompositeActionsMaxDepth}");
|
||||
}
|
||||
|
||||
// Resolve self-repository ($/) references before processing
|
||||
if (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SelfRepository) == true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(selfRepoName))
|
||||
{
|
||||
selfRepoName = executionContext.JobContext?.WorkflowRepository;
|
||||
selfRepoRef = executionContext.JobContext?.WorkflowSha;
|
||||
}
|
||||
ResolveSelfRepositoryReferences(executionContext, actions, selfRepoName, selfRepoRef);
|
||||
}
|
||||
|
||||
var repositoryActions = new List<Pipelines.ActionStep>();
|
||||
|
||||
foreach (var action in actions)
|
||||
@@ -398,7 +485,30 @@ namespace GitHub.Runner.Worker
|
||||
if (repositoryActions.Count > 0)
|
||||
{
|
||||
// Get the download info
|
||||
var downloadInfos = await GetDownloadInfoAsync(executionContext, repositoryActions);
|
||||
IDictionary<string, WebApi.ActionDownloadInfo> downloadInfos = null;
|
||||
Exception resolveFailure = null;
|
||||
try
|
||||
{
|
||||
downloadInfos = await GetDownloadInfoAsync(executionContext, repositoryActions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// record the exception for telemetry, and rethrow the original exception to fail the step.
|
||||
resolveFailure = ex;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
|
||||
{
|
||||
Type = JobTelemetryType.General,
|
||||
Message = $"resolve_download_actions_telemetry:{StringUtil.ConvertToJson(new ActionTelemetryPayload
|
||||
{
|
||||
Operation = "resolve_actions",
|
||||
Result = resolveFailure == null ? "succeeded" : resolveFailure.GetType().Name
|
||||
}, Newtonsoft.Json.Formatting.None)}"
|
||||
});
|
||||
}
|
||||
|
||||
// Download each action
|
||||
foreach (var action in repositoryActions)
|
||||
@@ -414,7 +524,29 @@ namespace GitHub.Runner.Worker
|
||||
throw new Exception($"Missing download info for {lookupKey}");
|
||||
}
|
||||
|
||||
await DownloadRepositoryActionAsync(executionContext, downloadInfo);
|
||||
Exception downloadFailure = null;
|
||||
try
|
||||
{
|
||||
await DownloadRepositoryActionAsync(executionContext, downloadInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// record the exception for telemetry, and rethrow the original exception to fail the step.
|
||||
downloadFailure = ex;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
|
||||
{
|
||||
Type = JobTelemetryType.General,
|
||||
Message = $"resolve_download_actions_telemetry:{StringUtil.ConvertToJson(new ActionTelemetryPayload
|
||||
{
|
||||
Operation = "download_action",
|
||||
Result = downloadFailure == null ? "succeeded" : downloadFailure.GetType().Name
|
||||
}, Newtonsoft.Json.Formatting.None)}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// More preparation based on content in the repository (action.yml)
|
||||
@@ -449,7 +581,17 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
else if (setupInfo != null && setupInfo.Steps != null && setupInfo.Steps.Count > 0)
|
||||
{
|
||||
state = await PrepareActionsRecursiveLegacyAsync(executionContext, state, setupInfo.Steps, depth + 1, action.Id);
|
||||
// Propagate parent's repo context for nested self-repository resolution
|
||||
var parentRef = action.Reference as Pipelines.RepositoryPathReference;
|
||||
var childRepoName = selfRepoName;
|
||||
var childRepoRef = selfRepoRef;
|
||||
if (parentRef != null &&
|
||||
string.Equals(parentRef.RepositoryType, Pipelines.RepositoryTypes.GitHub, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
childRepoName = parentRef.Name;
|
||||
childRepoRef = parentRef.Ref;
|
||||
}
|
||||
state = await PrepareActionsRecursiveLegacyAsync(executionContext, state, setupInfo.Steps, depth + 1, action.Id, childRepoName, childRepoRef);
|
||||
}
|
||||
var repoAction = action.Reference as Pipelines.RepositoryPathReference;
|
||||
if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias)
|
||||
@@ -562,6 +704,12 @@ namespace GitHub.Runner.Worker
|
||||
actionDirectory = Path.Combine(actionDirectory, repoAction.Path);
|
||||
}
|
||||
}
|
||||
else if (string.Equals(repoAction.RepositoryType, Pipelines.PipelineConstants.SelfRepositoryAlias, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Unresolved self-repository reference at load time — this
|
||||
// shouldn't happen but guard against NRE if it does.
|
||||
throw new InvalidOperationException($"Self-repository reference '$/{repoAction.Path}' was not resolved before LoadAction. Ensure the '{Constants.Runner.Features.SelfRepository}' feature flag is enabled.");
|
||||
}
|
||||
else
|
||||
{
|
||||
actionDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), repoAction.Name.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), repoAction.Ref);
|
||||
@@ -697,6 +845,27 @@ namespace GitHub.Runner.Worker
|
||||
_cachedEmbeddedStepIds[action.Id].Add(guid);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve self-repository refs in composite steps at load time.
|
||||
// During setup, resolution happens on a separate copy of these
|
||||
// step objects. At runtime, action.yml is re-parsed, producing
|
||||
// fresh self-repository refs that need resolution here.
|
||||
// When the parent is a dot-slash (self local-workspace) action,
|
||||
// repoAction.Name/Ref are null — fall back to workflow context.
|
||||
if (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SelfRepository) == true)
|
||||
{
|
||||
var parentName = repoAction.Name ?? executionContext.JobContext?.WorkflowRepository;
|
||||
var parentRef = repoAction.Ref ?? executionContext.JobContext?.WorkflowSha;
|
||||
ResolveSelfRepositoryReferences(executionContext, compositeAction.Steps, parentName, parentRef);
|
||||
if (compositeAction.PreSteps != null)
|
||||
{
|
||||
ResolveSelfRepositoryReferences(executionContext, compositeAction.PreSteps, parentName, parentRef);
|
||||
}
|
||||
if (compositeAction.PostSteps != null)
|
||||
{
|
||||
ResolveSelfRepositoryReferences(executionContext, compositeAction.PostSteps, parentName, parentRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -980,10 +1149,33 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
if (actionsToResolve.Count > 0)
|
||||
{
|
||||
var downloadInfos = await GetDownloadInfoAsync(executionContext, actionsToResolve);
|
||||
foreach (var kvp in downloadInfos)
|
||||
IDictionary<string, WebApi.ActionDownloadInfo> downloadInfos = null;
|
||||
Exception resolveFailure = null;
|
||||
try
|
||||
{
|
||||
resolvedDownloadInfos[kvp.Key] = kvp.Value;
|
||||
downloadInfos = await GetDownloadInfoAsync(executionContext, actionsToResolve);
|
||||
foreach (var kvp in downloadInfos)
|
||||
{
|
||||
resolvedDownloadInfos[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// record the exception for telemetry, and rethrow the original exception to fail the step.
|
||||
resolveFailure = ex;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
|
||||
{
|
||||
Type = JobTelemetryType.General,
|
||||
Message = $"resolve_download_actions_telemetry:{StringUtil.ConvertToJson(new ActionTelemetryPayload
|
||||
{
|
||||
Operation = "resolve_actions",
|
||||
Result = resolveFailure == null ? "succeeded" : resolveFailure.GetType().Name
|
||||
}, Newtonsoft.Json.Formatting.None)}"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1108,12 +1300,6 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
|
||||
{
|
||||
Type = JobTelemetryType.General,
|
||||
Message = $"Action archive cache usage: {downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha} use cache {useActionArchiveCache} has cache {hasActionArchiveCache}"
|
||||
});
|
||||
|
||||
if (!useActionArchiveCache)
|
||||
{
|
||||
await DownloadRepositoryArchive(executionContext, link, downloadInfo.Authentication?.Token, archiveFile);
|
||||
@@ -1122,6 +1308,13 @@ namespace GitHub.Runner.Worker
|
||||
var stagingDirectory = Path.Combine(tempDirectory, "_staging");
|
||||
Directory.CreateDirectory(stagingDirectory);
|
||||
|
||||
var fileInfo = new FileInfo(archiveFile);
|
||||
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
|
||||
{
|
||||
Type = JobTelemetryType.General,
|
||||
Message = $"Action archive cache usage: {downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha} use cache {useActionArchiveCache} has cache {hasActionArchiveCache} size {fileInfo.Length} bytes"
|
||||
});
|
||||
|
||||
#if OS_WINDOWS
|
||||
try
|
||||
{
|
||||
@@ -1159,7 +1352,6 @@ namespace GitHub.Runner.Worker
|
||||
int exitCode = await processInvoker.ExecuteAsync(stagingDirectory, tar, $"-xzf \"{archiveFile}\"", null, executionContext.CancellationToken);
|
||||
if (exitCode != 0)
|
||||
{
|
||||
var fileInfo = new FileInfo(archiveFile);
|
||||
var sha256hash = await IOUtil.GetFileContentSha256HashAsync(archiveFile);
|
||||
throw new InvalidActionArchiveException($"Can't use 'tar -xzf' extract archive file: {archiveFile} (SHA256 '{sha256hash}', size '{fileInfo.Length}' bytes, tar outputs '{string.Join(' ', tarOutputs)}'). Action being checked out: {downloadInfo.NameWithOwner}@{downloadInfo.Ref}. return code: {exitCode}.");
|
||||
}
|
||||
@@ -1209,6 +1401,12 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
private string GetWatermarkFilePath(string directory) => directory + ".completed";
|
||||
|
||||
private sealed class ActionTelemetryPayload
|
||||
{
|
||||
public string Operation { get; set; }
|
||||
public string Result { get; set; }
|
||||
}
|
||||
|
||||
private ActionSetupInfo PrepareRepositoryActionAsync(IExecutionContext executionContext, Pipelines.ActionStep repositoryAction)
|
||||
{
|
||||
var repositoryReference = repositoryAction.Reference as Pipelines.RepositoryPathReference;
|
||||
@@ -1347,6 +1545,47 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves self-reference ($/) references by mutating them in-place
|
||||
/// to standard GitHub repository references with the containing repo's
|
||||
/// name and ref.
|
||||
/// </summary>
|
||||
private void ResolveSelfRepositoryReferences(IExecutionContext executionContext, IEnumerable<Pipelines.ActionStep> actions, string repoName, string repoRef)
|
||||
{
|
||||
if (string.IsNullOrEmpty(repoName) || string.IsNullOrEmpty(repoRef))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var action in actions)
|
||||
{
|
||||
if (action.Reference.Type != Pipelines.ActionSourceType.Repository)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var repoAction = action.Reference as Pipelines.RepositoryPathReference;
|
||||
if (!string.Equals(repoAction.RepositoryType, Pipelines.PipelineConstants.SelfRepositoryAlias, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Trace.Info($"Resolving self-repository reference reference '$/{repoAction.Path}' to '{repoName}/{repoAction.Path}@{repoRef}'");
|
||||
executionContext.Debug($"Resolving $/{repoAction.Path} → {repoName}/{repoAction.Path}@{repoRef}");
|
||||
|
||||
repoAction.RepositoryType = Pipelines.RepositoryTypes.GitHub;
|
||||
repoAction.Name = repoName;
|
||||
repoAction.Ref = repoRef;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this is a reusable workflow job, ensure the workflow repo tarball
|
||||
/// is downloaded so self.workspace resolves to a real path on disk.
|
||||
/// Always downloads for reusable workflows when the feature flag is on,
|
||||
/// since step expressions are already expanded by the server and can't
|
||||
/// be scanned for self.* usage.
|
||||
/// </summary>
|
||||
private static string GetDownloadInfoLookupKey(Pipelines.ActionStep action)
|
||||
{
|
||||
if (action.Reference.Type != Pipelines.ActionSourceType.Repository)
|
||||
@@ -1362,6 +1601,11 @@ namespace GitHub.Runner.Worker
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.Equals(repositoryReference.RepositoryType, Pipelines.PipelineConstants.SelfRepositoryAlias, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to resolve self-reference '$/'. This can occur when the server does not support this syntax, the feature flag is disabled, or the workflow context (repository/SHA) is unavailable.");
|
||||
}
|
||||
|
||||
if (!string.Equals(repositoryReference.RepositoryType, Pipelines.RepositoryTypes.GitHub, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new NotSupportedException(repositoryReference.RepositoryType);
|
||||
|
||||
@@ -55,7 +55,18 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
break;
|
||||
case ActionSourceType.Repository:
|
||||
var repositoryReference = step.Reference as RepositoryPathReference;
|
||||
name = !String.IsNullOrEmpty(repositoryReference.Name) ? repositoryReference.Name : PipelineConstants.SelfAlias;
|
||||
if (!String.IsNullOrEmpty(repositoryReference.Name))
|
||||
{
|
||||
name = repositoryReference.Name;
|
||||
}
|
||||
else if (String.Equals(repositoryReference.RepositoryType, PipelineConstants.SelfRepositoryAlias, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
name = PipelineConstants.SelfRepositoryAlias;
|
||||
}
|
||||
else
|
||||
{
|
||||
name = PipelineConstants.SelfAlias;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -600,6 +611,14 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
Path = uses.Value
|
||||
};
|
||||
}
|
||||
else if (PipelineConstants.TryParseSelfRepository(uses.Value, out var selfPath))
|
||||
{
|
||||
result.Reference = new RepositoryPathReference
|
||||
{
|
||||
RepositoryType = PipelineConstants.SelfRepositoryAlias,
|
||||
Path = selfPath
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var usesSegments = uses.Value.Split('@');
|
||||
|
||||
@@ -38,10 +38,43 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
public static readonly Int32 MaxNodeNameLength = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Alias for the self repository.
|
||||
/// Alias for the self local-workspace repository type (./ syntax).
|
||||
/// Resolves to the local checkout on the runner.
|
||||
/// </summary>
|
||||
public static readonly String SelfAlias = "self";
|
||||
|
||||
/// <summary>
|
||||
/// RepositoryType for self-repository references ($/ syntax).
|
||||
/// Resolves to "this repo, at this SHA" based on the containing YAML file.
|
||||
/// </summary>
|
||||
public static readonly String SelfRepositoryAlias = "selfRepository";
|
||||
|
||||
/// <summary>
|
||||
/// The prefix for self-repository references in uses: values.
|
||||
/// </summary>
|
||||
public const String SelfRepositoryPrefix = "$/";
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the uses value is a self-repository reference (starts with $/),
|
||||
/// and outputs the subpath after the prefix.
|
||||
/// </summary>
|
||||
public static bool TryParseSelfRepository(string usesValue, out string path)
|
||||
{
|
||||
if (usesValue != null && usesValue.StartsWith(SelfRepositoryPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
path = usesValue.Substring(SelfRepositoryPrefix.Length).TrimStart('/');
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = null;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
path = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error code during graph validation.
|
||||
/// </summary>
|
||||
|
||||
@@ -1605,6 +1605,10 @@ namespace GitHub.Actions.WorkflowParser.Conversion
|
||||
{
|
||||
id = WorkflowConstants.SelfAlias;
|
||||
}
|
||||
else if (GitHub.DistributedTask.Pipelines.PipelineConstants.TryParseSelfRepository(action.Uses!.Value, out _))
|
||||
{
|
||||
id = WorkflowConstants.SelfRepositoryAlias;
|
||||
}
|
||||
else
|
||||
{
|
||||
var usesSegments = action.Uses!.Value.Split('@');
|
||||
|
||||
@@ -26,10 +26,15 @@ namespace GitHub.Actions.WorkflowParser
|
||||
internal const Int32 MaxNodeNameLength = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Alias for the self repository.
|
||||
/// Alias for the self local-workspace repository type (./ syntax).
|
||||
/// </summary>
|
||||
internal const String SelfAlias = "self";
|
||||
|
||||
/// <summary>
|
||||
/// RepositoryType for self-repository references ($/ syntax).
|
||||
/// </summary>
|
||||
internal const String SelfRepositoryAlias = "selfRepository";
|
||||
|
||||
public static class PermissionsPolicy
|
||||
{
|
||||
public const string LimitedRead = "LimitedRead";
|
||||
|
||||
@@ -90,6 +90,11 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
|
||||
var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "main", "action.yml");
|
||||
Assert.True(File.Exists(actionYamlFile));
|
||||
|
||||
var telemetryMessages = GetTelemetryMessages();
|
||||
Assert.True(ContainsTelemetry(telemetryMessages, "resolve_actions"));
|
||||
Assert.True(ContainsTelemetry(telemetryMessages, "succeeded"));
|
||||
Assert.True(ContainsTelemetry(telemetryMessages, "download_action"));
|
||||
_hc.GetTrace().Info(File.ReadAllText(actionYamlFile));
|
||||
}
|
||||
finally
|
||||
@@ -148,6 +153,11 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
|
||||
// Act + Assert
|
||||
await Assert.ThrowsAsync<InvalidActionArchiveException>(async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions));
|
||||
|
||||
var telemetryMessages = GetTelemetryMessages();
|
||||
Assert.True(ContainsTelemetry(telemetryMessages, "resolve_actions"));
|
||||
Assert.True(ContainsTelemetry(telemetryMessages, "download_action"));
|
||||
Assert.True(ContainsTelemetry(telemetryMessages, "InvalidActionArchiveException"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -215,6 +225,51 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task PrepareActions_ResolveActionDownloadInfo_RecordsTelemetry_OnFailure()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
_ec.Object.Global.Variables.Set(Constants.Variables.System.JobRequestType, "RunnerJobRequest");
|
||||
|
||||
_launchServer
|
||||
.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>(), It.IsAny<bool>()))
|
||||
.ThrowsAsync(new Exception("resolve failed"));
|
||||
|
||||
var actions = new List<Pipelines.JobStep>
|
||||
{
|
||||
new Pipelines.ActionStep()
|
||||
{
|
||||
Name = "action",
|
||||
Id = Guid.NewGuid(),
|
||||
Reference = new Pipelines.RepositoryPathReference()
|
||||
{
|
||||
Name = "actions/checkout",
|
||||
Ref = "v4",
|
||||
RepositoryType = "GitHub"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act + Assert
|
||||
await Assert.ThrowsAsync<FailedToResolveActionDownloadInfoException>(async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions));
|
||||
|
||||
var telemetryMessages = GetTelemetryMessages();
|
||||
Assert.Equal(1, telemetryMessages.Count(message =>
|
||||
message.Contains("resolve_actions", StringComparison.OrdinalIgnoreCase)
|
||||
&& !message.Contains("\"result\":\"succeeded\"", StringComparison.OrdinalIgnoreCase)));
|
||||
Assert.False(ContainsTelemetry(telemetryMessages, "resolve_actions\",\"result\":\"succeeded"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
#if OS_LINUX
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
@@ -530,9 +585,9 @@ runs:
|
||||
|
||||
//Assert
|
||||
string destDirectory = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "actions", "checkout", "master");
|
||||
Assert.True(Directory.Exists(destDirectory), "Destination directory does not exist");
|
||||
var di = new DirectoryInfo(destDirectory);
|
||||
Assert.NotNull(di.LinkTarget);
|
||||
Assert.True(Directory.Exists(destDirectory), "Destination directory does not exist");
|
||||
var di = new DirectoryInfo(destDirectory);
|
||||
Assert.NotNull(di.LinkTarget);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -2386,7 +2441,7 @@ runs:
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void LoadsNode24ActionDefinition()
|
||||
@@ -2454,7 +2509,7 @@ runs:
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
@@ -3333,6 +3388,16 @@ runs:
|
||||
}
|
||||
}
|
||||
|
||||
private IList<string> GetTelemetryMessages()
|
||||
{
|
||||
return _ec.Object.Global.JobTelemetry.Select(x => x.Message).ToList();
|
||||
}
|
||||
|
||||
private static bool ContainsTelemetry(IList<string> telemetryMessages, string expectedFragment)
|
||||
{
|
||||
return telemetryMessages.Any(message => message.Contains(expectedFragment, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
@@ -3468,5 +3533,604 @@ runs:
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async void PrepareActions_SelfRepository_ResolvesAtDepthZero()
|
||||
{
|
||||
// Self-references are only supported via run service (batch resolution path)
|
||||
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true");
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
const string RepoName = "my-org/my-repo";
|
||||
const string RepoSha = "abc123def456";
|
||||
_ec.Setup(x => x.GetGitHubContext("repository")).Returns(RepoName);
|
||||
_ec.Setup(x => x.GetGitHubContext("sha")).Returns(RepoSha);
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
|
||||
var jobContext = new JobContext();
|
||||
jobContext.WorkflowRepository = RepoName;
|
||||
jobContext.WorkflowSha = RepoSha;
|
||||
_ec.Setup(x => x.JobContext).Returns(jobContext);
|
||||
|
||||
var actionId = Guid.NewGuid();
|
||||
var actions = new List<Pipelines.ActionStep>
|
||||
{
|
||||
new Pipelines.ActionStep()
|
||||
{
|
||||
Name = "action",
|
||||
Id = actionId,
|
||||
Reference = new Pipelines.RepositoryPathReference()
|
||||
{
|
||||
RepositoryType = Pipelines.PipelineConstants.SelfRepositoryAlias,
|
||||
Path = "actions/my-action"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
string archiveFile = await CreateRepoArchive();
|
||||
using var stream = File.OpenRead(archiveFile);
|
||||
string archiveLink = GetLinkToActionArchive("https://api.github.com", RepoName, RepoSha);
|
||||
var mockClientHandler = new Mock<HttpClientHandler>();
|
||||
mockClientHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(m => m.RequestUri == new Uri(archiveLink)), ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(stream) });
|
||||
var mockHandlerFactory = new Mock<IHttpClientHandlerFactory>();
|
||||
mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny<RunnerWebProxy>())).Returns(mockClientHandler.Object);
|
||||
_hc.SetSingleton(mockHandlerFactory.Object);
|
||||
|
||||
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns("https://api.github.com");
|
||||
|
||||
// Act — resolution mutates the reference in-place before download/prepare.
|
||||
// The archive doesn't contain the subpath, so prepare will fail, but the
|
||||
// reference is already resolved by that point.
|
||||
try
|
||||
{
|
||||
await _actionManager.PrepareActionsAsync(_ec.Object, actions);
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.Message.Contains("Can't find"))
|
||||
{
|
||||
// Expected: test archive lacks the action.yml at the resolved subpath
|
||||
}
|
||||
|
||||
// Assert — the reference should be resolved to a GitHub repo reference
|
||||
var repoRef = actions[0].Reference as Pipelines.RepositoryPathReference;
|
||||
Assert.Equal(Pipelines.RepositoryTypes.GitHub, repoRef.RepositoryType);
|
||||
Assert.Equal(RepoName, repoRef.Name);
|
||||
Assert.Equal(RepoSha, repoRef.Ref);
|
||||
Assert.Equal("actions/my-action", repoRef.Path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null);
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async void PrepareActions_SelfRepository_NotResolvedWhenFeatureFlagDisabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
_ec.Setup(x => x.GetGitHubContext("repository")).Returns("my-org/my-repo");
|
||||
_ec.Setup(x => x.GetGitHubContext("sha")).Returns("abc123");
|
||||
// Feature flag NOT set
|
||||
|
||||
var actionId = Guid.NewGuid();
|
||||
var actions = new List<Pipelines.ActionStep>
|
||||
{
|
||||
new Pipelines.ActionStep()
|
||||
{
|
||||
Name = "action",
|
||||
Id = actionId,
|
||||
Reference = new Pipelines.RepositoryPathReference()
|
||||
{
|
||||
RepositoryType = Pipelines.PipelineConstants.SelfRepositoryAlias,
|
||||
Path = "actions/my-action"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act & Assert — should throw because unresolved self-reference hits GetDownloadInfoLookupKey
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await _actionManager.PrepareActionsAsync(_ec.Object, actions));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async void PrepareActions_SelfRepository_ResolvesNestedInComposite()
|
||||
{
|
||||
// Composite action at $/actions/parent uses $/actions/child (same repo).
|
||||
// This tests the batch path fix: $/ refs in nextLevel must be resolved
|
||||
// BEFORE ResolveNewActionsAsync, otherwise GetDownloadInfoLookupKey throws.
|
||||
// We pre-stage only the parent action.yml on disk so the composite steps
|
||||
// are discovered, but we DON'T stage the child — a download failure for
|
||||
// the child is fine; the important thing is that $/ was resolved
|
||||
// (no InvalidOperationException from GetDownloadInfoLookupKey).
|
||||
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true");
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
const string RepoName = "my-org/my-repo";
|
||||
const string RepoSha = "abc123def456";
|
||||
_ec.Setup(x => x.GetGitHubContext("repository")).Returns(RepoName);
|
||||
_ec.Setup(x => x.GetGitHubContext("sha")).Returns(RepoSha);
|
||||
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns("https://api.github.com");
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
|
||||
var jobContext = new JobContext();
|
||||
jobContext.WorkflowRepository = RepoName;
|
||||
jobContext.WorkflowSha = RepoSha;
|
||||
_ec.Setup(x => x.JobContext).Returns(jobContext);
|
||||
|
||||
// Stage parent action on disk as a composite that uses $/actions/child.
|
||||
// We use rootStepId != default to avoid directory deletion,
|
||||
// and create the watermark + action.yml in the expected location.
|
||||
string actionsDir = Path.Combine(_workFolder, Constants.Path.ActionsDirectory);
|
||||
string destDir = Path.Combine(actionsDir, RepoName.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), RepoSha);
|
||||
Directory.CreateDirectory(Path.Combine(destDir, "actions", "parent"));
|
||||
File.WriteAllText(Path.Combine(destDir, "actions", "parent", Constants.Path.ActionManifestYmlFile), @"
|
||||
name: 'Parent'
|
||||
description: 'Composite parent'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: $/actions/child
|
||||
");
|
||||
// Stage child action too (as a leaf node action)
|
||||
Directory.CreateDirectory(Path.Combine(destDir, "actions", "child"));
|
||||
File.WriteAllText(Path.Combine(destDir, "actions", "child", Constants.Path.ActionManifestYmlFile), @"
|
||||
name: 'Child'
|
||||
description: 'Node child'
|
||||
runs:
|
||||
using: 'node20'
|
||||
main: 'index.js'
|
||||
");
|
||||
// Write watermark
|
||||
File.WriteAllText($"{destDir}.completed", string.Empty);
|
||||
|
||||
var rootStepId = Guid.NewGuid();
|
||||
var actions = new List<Pipelines.ActionStep>
|
||||
{
|
||||
new Pipelines.ActionStep()
|
||||
{
|
||||
Name = "action",
|
||||
Id = Guid.NewGuid(),
|
||||
Reference = new Pipelines.RepositoryPathReference()
|
||||
{
|
||||
RepositoryType = Pipelines.PipelineConstants.SelfRepositoryAlias,
|
||||
Path = "actions/parent"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act — should resolve $/ and not throw InvalidOperationException
|
||||
await _actionManager.PrepareActionsAsync(_ec.Object, actions, rootStepId);
|
||||
|
||||
// Assert — top-level $/ resolved
|
||||
var topRef = actions[0].Reference as Pipelines.RepositoryPathReference;
|
||||
Assert.Equal(Pipelines.RepositoryTypes.GitHub, topRef.RepositoryType);
|
||||
Assert.Equal(RepoName, topRef.Name);
|
||||
Assert.Equal(RepoSha, topRef.Ref);
|
||||
Assert.Equal("actions/parent", topRef.Path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null);
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async void PrepareActions_SelfRepository_CrossRepoCompositeResolvesToParentRepo()
|
||||
{
|
||||
// External composite (external/foo@v1) uses $/lib/bar.
|
||||
// $/lib/bar should resolve to external/foo@v1 (the parent's repo),
|
||||
// NOT to the workflow's root repo.
|
||||
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true");
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
const string RootRepoName = "my-org/my-repo";
|
||||
const string RootRepoSha = "root-sha-111";
|
||||
const string ExtRepoName = "external/foo";
|
||||
const string ExtRepoRef = "v1";
|
||||
_ec.Setup(x => x.GetGitHubContext("repository")).Returns(RootRepoName);
|
||||
_ec.Setup(x => x.GetGitHubContext("sha")).Returns(RootRepoSha);
|
||||
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns("https://api.github.com");
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
|
||||
var jobContext = new JobContext();
|
||||
jobContext.WorkflowRepository = RootRepoName;
|
||||
jobContext.WorkflowSha = RootRepoSha;
|
||||
_ec.Setup(x => x.JobContext).Returns(jobContext);
|
||||
|
||||
string actionsDir = Path.Combine(_workFolder, Constants.Path.ActionsDirectory);
|
||||
string destDir = Path.Combine(actionsDir, ExtRepoName.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), ExtRepoRef);
|
||||
Directory.CreateDirectory(destDir);
|
||||
File.WriteAllText(Path.Combine(destDir, Constants.Path.ActionManifestYmlFile), @"
|
||||
name: 'External Foo'
|
||||
description: 'External composite'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: $/lib/bar
|
||||
");
|
||||
Directory.CreateDirectory(Path.Combine(destDir, "lib", "bar"));
|
||||
File.WriteAllText(Path.Combine(destDir, "lib", "bar", Constants.Path.ActionManifestYmlFile), @"
|
||||
name: 'Bar'
|
||||
description: 'Node action in external repo'
|
||||
runs:
|
||||
using: 'node20'
|
||||
main: 'index.js'
|
||||
");
|
||||
File.WriteAllText($"{destDir}.completed", string.Empty);
|
||||
|
||||
var rootStepId = Guid.NewGuid();
|
||||
var actions = new List<Pipelines.ActionStep>
|
||||
{
|
||||
new Pipelines.ActionStep()
|
||||
{
|
||||
Name = "action",
|
||||
Id = Guid.NewGuid(),
|
||||
Reference = new Pipelines.RepositoryPathReference()
|
||||
{
|
||||
Name = ExtRepoName,
|
||||
Ref = ExtRepoRef,
|
||||
RepositoryType = Pipelines.RepositoryTypes.GitHub
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act — should resolve $/lib/bar to external/foo@v1/lib/bar
|
||||
await _actionManager.PrepareActionsAsync(_ec.Object, actions, rootStepId);
|
||||
|
||||
// Assert — the top-level ref is unchanged (it was already concrete)
|
||||
var topRef = actions[0].Reference as Pipelines.RepositoryPathReference;
|
||||
Assert.Equal(ExtRepoName, topRef.Name);
|
||||
Assert.Equal(ExtRepoRef, topRef.Ref);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null);
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async void PrepareActions_SelfRepository_MultiLevelChain()
|
||||
{
|
||||
// $/a → composite → $/b → composite → $/c (three levels, same repo)
|
||||
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true");
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
const string RepoName = "my-org/my-repo";
|
||||
const string RepoSha = "chain-sha-222";
|
||||
_ec.Setup(x => x.GetGitHubContext("repository")).Returns(RepoName);
|
||||
_ec.Setup(x => x.GetGitHubContext("sha")).Returns(RepoSha);
|
||||
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns("https://api.github.com");
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
|
||||
var jobContext = new JobContext();
|
||||
jobContext.WorkflowRepository = RepoName;
|
||||
jobContext.WorkflowSha = RepoSha;
|
||||
_ec.Setup(x => x.JobContext).Returns(jobContext);
|
||||
|
||||
string actionsDir = Path.Combine(_workFolder, Constants.Path.ActionsDirectory);
|
||||
string destDir = Path.Combine(actionsDir, RepoName.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), RepoSha);
|
||||
Directory.CreateDirectory(Path.Combine(destDir, "a"));
|
||||
File.WriteAllText(Path.Combine(destDir, "a", Constants.Path.ActionManifestYmlFile), @"
|
||||
name: 'A'
|
||||
description: 'Level 0 composite'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: $/b
|
||||
");
|
||||
Directory.CreateDirectory(Path.Combine(destDir, "b"));
|
||||
File.WriteAllText(Path.Combine(destDir, "b", Constants.Path.ActionManifestYmlFile), @"
|
||||
name: 'B'
|
||||
description: 'Level 1 composite'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: $/c
|
||||
");
|
||||
Directory.CreateDirectory(Path.Combine(destDir, "c"));
|
||||
File.WriteAllText(Path.Combine(destDir, "c", Constants.Path.ActionManifestYmlFile), @"
|
||||
name: 'C'
|
||||
description: 'Level 2 node leaf'
|
||||
runs:
|
||||
using: 'node20'
|
||||
main: 'index.js'
|
||||
");
|
||||
File.WriteAllText($"{destDir}.completed", string.Empty);
|
||||
|
||||
var rootStepId = Guid.NewGuid();
|
||||
var actions = new List<Pipelines.ActionStep>
|
||||
{
|
||||
new Pipelines.ActionStep()
|
||||
{
|
||||
Name = "action",
|
||||
Id = Guid.NewGuid(),
|
||||
Reference = new Pipelines.RepositoryPathReference()
|
||||
{
|
||||
RepositoryType = Pipelines.PipelineConstants.SelfRepositoryAlias,
|
||||
Path = "a"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act — three-level $/ chain should resolve without error
|
||||
await _actionManager.PrepareActionsAsync(_ec.Object, actions, rootStepId);
|
||||
|
||||
// Assert — top-level ref resolved
|
||||
var topRef = actions[0].Reference as Pipelines.RepositoryPathReference;
|
||||
Assert.Equal(Pipelines.RepositoryTypes.GitHub, topRef.RepositoryType);
|
||||
Assert.Equal(RepoName, topRef.Name);
|
||||
Assert.Equal(RepoSha, topRef.Ref);
|
||||
Assert.Equal("a", topRef.Path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null);
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async void PrepareActions_SelfRepository_ResolvesAtDepthZero_LegacyPath()
|
||||
{
|
||||
// Same as ResolvesAtDepthZero but on the legacy (non-batch) path
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
const string RepoName = "my-org/my-repo";
|
||||
const string RepoSha = "abc123def456";
|
||||
_ec.Setup(x => x.GetGitHubContext("repository")).Returns(RepoName);
|
||||
_ec.Setup(x => x.GetGitHubContext("sha")).Returns(RepoSha);
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
|
||||
var jobContext = new JobContext();
|
||||
jobContext.WorkflowRepository = RepoName;
|
||||
jobContext.WorkflowSha = RepoSha;
|
||||
_ec.Setup(x => x.JobContext).Returns(jobContext);
|
||||
|
||||
var actionId = Guid.NewGuid();
|
||||
var actions = new List<Pipelines.ActionStep>
|
||||
{
|
||||
new Pipelines.ActionStep()
|
||||
{
|
||||
Name = "action",
|
||||
Id = actionId,
|
||||
Reference = new Pipelines.RepositoryPathReference()
|
||||
{
|
||||
RepositoryType = Pipelines.PipelineConstants.SelfRepositoryAlias,
|
||||
Path = "actions/my-action"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
string archiveFile = await CreateRepoArchive();
|
||||
using var stream = File.OpenRead(archiveFile);
|
||||
string archiveLink = GetLinkToActionArchive("https://api.github.com", RepoName, RepoSha);
|
||||
var mockClientHandler = new Mock<HttpClientHandler>();
|
||||
mockClientHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(m => m.RequestUri == new Uri(archiveLink)), ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(stream) });
|
||||
var mockHandlerFactory = new Mock<IHttpClientHandlerFactory>();
|
||||
mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny<RunnerWebProxy>())).Returns(mockClientHandler.Object);
|
||||
_hc.SetSingleton(mockHandlerFactory.Object);
|
||||
|
||||
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns("https://api.github.com");
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await _actionManager.PrepareActionsAsync(_ec.Object, actions);
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.Message.Contains("Can't find"))
|
||||
{
|
||||
// Expected: test archive lacks the action.yml at the resolved subpath
|
||||
}
|
||||
|
||||
// Assert — the reference should be resolved to a GitHub repo reference
|
||||
var repoRef = actions[0].Reference as Pipelines.RepositoryPathReference;
|
||||
Assert.Equal(Pipelines.RepositoryTypes.GitHub, repoRef.RepositoryType);
|
||||
Assert.Equal(RepoName, repoRef.Name);
|
||||
Assert.Equal(RepoSha, repoRef.Ref);
|
||||
Assert.Equal("actions/my-action", repoRef.Path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async void PrepareActions_SelfRepository_ResolvesNestedInComposite_LegacyPath()
|
||||
{
|
||||
// Same as ResolvesNestedInComposite but on the legacy (non-batch) path.
|
||||
// Verifies that $/ resolution works when batch action resolution is disabled.
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
const string RepoName = "my-org/my-repo";
|
||||
const string RepoSha = "abc123def456";
|
||||
_ec.Setup(x => x.GetGitHubContext("repository")).Returns(RepoName);
|
||||
_ec.Setup(x => x.GetGitHubContext("sha")).Returns(RepoSha);
|
||||
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns("https://api.github.com");
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
|
||||
var jobContext = new JobContext();
|
||||
jobContext.WorkflowRepository = RepoName;
|
||||
jobContext.WorkflowSha = RepoSha;
|
||||
_ec.Setup(x => x.JobContext).Returns(jobContext);
|
||||
|
||||
// Stage parent action on disk as a composite that uses $/actions/child.
|
||||
string actionsDir = Path.Combine(_workFolder, Constants.Path.ActionsDirectory);
|
||||
string destDir = Path.Combine(actionsDir, RepoName.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), RepoSha);
|
||||
Directory.CreateDirectory(Path.Combine(destDir, "actions", "parent"));
|
||||
File.WriteAllText(Path.Combine(destDir, "actions", "parent", Constants.Path.ActionManifestYmlFile), @"
|
||||
name: 'Parent'
|
||||
description: 'Composite parent'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: $/actions/child
|
||||
");
|
||||
// Stage child action too (as a leaf node action)
|
||||
Directory.CreateDirectory(Path.Combine(destDir, "actions", "child"));
|
||||
File.WriteAllText(Path.Combine(destDir, "actions", "child", Constants.Path.ActionManifestYmlFile), @"
|
||||
name: 'Child'
|
||||
description: 'Node child'
|
||||
runs:
|
||||
using: 'node20'
|
||||
main: 'index.js'
|
||||
");
|
||||
// Write watermark
|
||||
File.WriteAllText($"{destDir}.completed", string.Empty);
|
||||
|
||||
var rootStepId = Guid.NewGuid();
|
||||
var actions = new List<Pipelines.ActionStep>
|
||||
{
|
||||
new Pipelines.ActionStep()
|
||||
{
|
||||
Name = "action",
|
||||
Id = Guid.NewGuid(),
|
||||
Reference = new Pipelines.RepositoryPathReference()
|
||||
{
|
||||
RepositoryType = Pipelines.PipelineConstants.SelfRepositoryAlias,
|
||||
Path = "actions/parent"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Act — should resolve $/ and not throw InvalidOperationException
|
||||
await _actionManager.PrepareActionsAsync(_ec.Object, actions, rootStepId);
|
||||
|
||||
// Assert — top-level $/ resolved
|
||||
var topRef = actions[0].Reference as Pipelines.RepositoryPathReference;
|
||||
Assert.Equal(Pipelines.RepositoryTypes.GitHub, topRef.RepositoryType);
|
||||
Assert.Equal(RepoName, topRef.Name);
|
||||
Assert.Equal(RepoSha, topRef.Ref);
|
||||
Assert.Equal("actions/parent", topRef.Path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void LoadAction_DotSlashCompositeWithNestedSelfRepository_ResolvesViaWorkflowContext()
|
||||
{
|
||||
// Regression test: when a dot-slash (./) composite action contains a
|
||||
// nested $/actions/child step, LoadAction re-parses the action.yml at
|
||||
// runtime and must resolve the $/ ref. The parent is repositoryType "self"
|
||||
// so its Name and Ref are null — resolution must fall back to
|
||||
// WorkflowRepository/WorkflowSha from the job context. Before the fix,
|
||||
// this path hit a NullReferenceException at repoAction.Name.Replace().
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
Setup();
|
||||
const string WorkflowRepo = "my-org/my-repo";
|
||||
const string WorkflowSha = "abc123def456";
|
||||
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
|
||||
var jobContext = new JobContext();
|
||||
jobContext.WorkflowRepository = WorkflowRepo;
|
||||
jobContext.WorkflowSha = WorkflowSha;
|
||||
_ec.Setup(x => x.JobContext).Returns(jobContext);
|
||||
|
||||
// Stage the dot-slash composite in the workspace directory.
|
||||
// It contains a nested $/actions/child step.
|
||||
string workspaceDir = Path.Combine(_workFolder, "actions", "actions");
|
||||
string compositeDir = Path.Combine(workspaceDir, "my-composite");
|
||||
Directory.CreateDirectory(compositeDir);
|
||||
File.WriteAllText(Path.Combine(compositeDir, Constants.Path.ActionManifestYmlFile), @"
|
||||
name: 'DotSlash Parent'
|
||||
description: 'Composite loaded via ./ that nests a $/ ref'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- run: echo 'hello'
|
||||
shell: bash
|
||||
- uses: $/actions/child
|
||||
");
|
||||
|
||||
// Stage the child action in the actions cache under the workflow repo.
|
||||
string actionsDir = Path.Combine(_workFolder, Constants.Path.ActionsDirectory);
|
||||
string childDir = Path.Combine(actionsDir, WorkflowRepo.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), WorkflowSha, "actions", "child");
|
||||
Directory.CreateDirectory(childDir);
|
||||
File.WriteAllText(Path.Combine(childDir, Constants.Path.ActionManifestYmlFile), @"
|
||||
name: 'Child'
|
||||
description: 'Leaf action'
|
||||
runs:
|
||||
using: 'node20'
|
||||
main: 'index.js'
|
||||
");
|
||||
|
||||
// Create dot-slash step with Name = null (the real scenario).
|
||||
var instance = new Pipelines.ActionStep()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Reference = new Pipelines.RepositoryPathReference()
|
||||
{
|
||||
Name = null,
|
||||
Ref = null,
|
||||
RepositoryType = Pipelines.PipelineConstants.SelfAlias,
|
||||
Path = "my-composite"
|
||||
}
|
||||
};
|
||||
|
||||
// Act — should NOT throw NullReferenceException
|
||||
Definition definition = _actionManager.LoadAction(_ec.Object, instance);
|
||||
|
||||
// Assert — loaded the composite successfully
|
||||
Assert.NotNull(definition);
|
||||
Assert.NotNull(definition.Data);
|
||||
Assert.Equal(ActionExecutionType.Composite, definition.Data.Execution.ExecutionType);
|
||||
|
||||
// Assert — the nested $/ step was resolved to the workflow repo
|
||||
var compositeData = definition.Data.Execution as CompositeActionExecutionData;
|
||||
Assert.NotNull(compositeData);
|
||||
var childStep = compositeData.Steps
|
||||
.OfType<Pipelines.ActionStep>()
|
||||
.FirstOrDefault(s => s.Reference is Pipelines.RepositoryPathReference r
|
||||
&& r.Path == "actions/child");
|
||||
Assert.NotNull(childStep);
|
||||
var childRef = childStep.Reference as Pipelines.RepositoryPathReference;
|
||||
Assert.Equal(Pipelines.RepositoryTypes.GitHub, childRef.RepositoryType);
|
||||
Assert.Equal(WorkflowRepo, childRef.Name);
|
||||
Assert.Equal(WorkflowSha, childRef.Ref);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user