mirror of
https://github.com/actions/runner.git
synced 2026-07-05 20:38:40 +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 }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
# Build runner layout
|
# Build runner layout
|
||||||
- name: Build & Layout Release
|
- name: Build & Layout Release
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
docker_platform: linux/arm64
|
docker_platform: linux/arm64
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Get latest runner version
|
- name: Get latest runner version
|
||||||
id: latest_runner
|
id: latest_runner
|
||||||
|
|||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- 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 }}
|
npm-vulnerabilities: ${{ steps.check-versions.outputs.npm-vulnerabilities }}
|
||||||
open-dependency-prs: ${{ steps.check-prs.outputs.open-dependency-prs }}
|
open-dependency-prs: ${{ steps.check-prs.outputs.open-dependency-prs }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
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 }}
|
BUILDX_CURRENT_VERSION: ${{ steps.check_buildx_version.outputs.CURRENT_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Check Docker version
|
- name: Check Docker version
|
||||||
id: check_docker_version
|
id: check_docker_version
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Update Docker version
|
- name: Update Docker version
|
||||||
shell: bash
|
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
|
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.releaseBranch }}
|
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 }}
|
DOTNET_CURRENT_MAJOR_MINOR_VERSION: ${{ steps.fetch_current_version.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
- name: Get current major minor version
|
- name: Get current major minor version
|
||||||
id: fetch_current_version
|
id: fetch_current_version
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
if: ${{ needs.dotnet-update.outputs.SHOULD_UPDATE == 1 && needs.dotnet-update.outputs.BRANCH_EXISTS == 0 }}
|
if: ${{ needs.dotnet-update.outputs.SHOULD_UPDATE == 1 && needs.dotnet-update.outputs.BRANCH_EXISTS == 0 }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
ref: feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
|
ref: feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
|
||||||
- name: Create Pull Request
|
- 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:
|
update-node:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Get latest Node versions
|
- name: Get latest Node versions
|
||||||
id: node-versions
|
id: node-versions
|
||||||
run: |
|
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:
|
npm-audit-with-ts-fix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/npm-audit.yml
vendored
2
.github/workflows/npm-audit.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
npm-audit:
|
npm-audit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
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'
|
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
# Make sure ./releaseVersion match ./src/runnerversion
|
# Make sure ./releaseVersion match ./src/runnerversion
|
||||||
# Query GitHub release ensure version is not used
|
# Query GitHub release ensure version is not used
|
||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
# Build runner layout
|
# Build runner layout
|
||||||
- name: Build & Layout Release
|
- name: Build & Layout Release
|
||||||
@@ -129,7 +129,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
# Download runner package tar.gz/zip produced by 'build' job
|
# Download runner package tar.gz/zip produced by 'build' job
|
||||||
- name: Download Artifact (win-x64)
|
- name: Download Artifact (win-x64)
|
||||||
@@ -296,7 +296,7 @@ jobs:
|
|||||||
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
|
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Compute image version
|
- name: Compute image version
|
||||||
id: image
|
id: image
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ ARG TARGETOS
|
|||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG RUNNER_VERSION
|
ARG RUNNER_VERSION
|
||||||
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
||||||
ARG DOCKER_VERSION=29.5.3
|
ARG DOCKER_VERSION=29.6.1
|
||||||
ARG BUILDX_VERSION=0.34.1
|
ARG BUILDX_VERSION=0.35.0
|
||||||
|
|
||||||
RUN apt update -y && apt install curl unzip -y
|
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.
|
# 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
|
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
|
||||||
NODE20_VERSION="20.20.2"
|
NODE20_VERSION="20.20.2"
|
||||||
NODE24_VERSION="24.16.0"
|
NODE24_VERSION="24.18.0"
|
||||||
|
|
||||||
get_abs_path() {
|
get_abs_path() {
|
||||||
# exploits the fact that pwd will print abs path when no args
|
# 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 BatchActionResolution = "actions_batch_action_resolution";
|
||||||
public static readonly string UseBearerTokenForCodeload = "actions_use_bearer_token_for_codeload";
|
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 OverrideDebuggerWelcomeMessage = "actions_runner_override_debugger_welcome_message";
|
||||||
|
public static readonly string SelfRepository = "actions_self_repository";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node version migration related constants
|
// Node version migration related constants
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ namespace GitHub.Runner.Worker
|
|||||||
return new PrepareResult(containerSetupSteps, result.PreStepTracker);
|
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));
|
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||||
if (depth > Constants.CompositeActionsMaxDepth)
|
if (depth > Constants.CompositeActionsMaxDepth)
|
||||||
@@ -186,6 +186,21 @@ namespace GitHub.Runner.Worker
|
|||||||
throw new Exception($"Composite action depth exceeded max 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))
|
||||||
|
{
|
||||||
|
// 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>();
|
var repositoryActions = new List<Pipelines.ActionStep>();
|
||||||
|
|
||||||
foreach (var action in actions)
|
foreach (var action in actions)
|
||||||
@@ -228,7 +243,30 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
throw new Exception($"Missing download info for {lookupKey}");
|
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
|
// 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).
|
// then recurse per parent (which hits the cache, not the API).
|
||||||
if (nextLevel.Count > 0)
|
if (nextLevel.Count > 0)
|
||||||
{
|
{
|
||||||
var nextLevelRepoActions = nextLevel
|
if (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SelfRepository) == true)
|
||||||
.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))
|
|
||||||
{
|
{
|
||||||
var groupActions = group.Select(x => x.action).ToList();
|
// Self-repository path: group by parent so each group's
|
||||||
state = await PrepareActionsRecursiveAsync(executionContext, state, groupActions, resolvedDownloadInfos, depth + 1, group.Key);
|
// $/ 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.
|
/// sub-actions individually, with no cross-depth deduplication.
|
||||||
/// Used when the BatchActionResolution feature flag is disabled.
|
/// Used when the BatchActionResolution feature flag is disabled.
|
||||||
/// </summary>
|
/// </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));
|
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||||
if (depth > Constants.CompositeActionsMaxDepth)
|
if (depth > Constants.CompositeActionsMaxDepth)
|
||||||
{
|
{
|
||||||
throw new Exception($"Composite action depth exceeded max 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>();
|
var repositoryActions = new List<Pipelines.ActionStep>();
|
||||||
|
|
||||||
foreach (var action in actions)
|
foreach (var action in actions)
|
||||||
@@ -398,7 +485,30 @@ namespace GitHub.Runner.Worker
|
|||||||
if (repositoryActions.Count > 0)
|
if (repositoryActions.Count > 0)
|
||||||
{
|
{
|
||||||
// Get the download info
|
// 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
|
// Download each action
|
||||||
foreach (var action in repositoryActions)
|
foreach (var action in repositoryActions)
|
||||||
@@ -414,7 +524,29 @@ namespace GitHub.Runner.Worker
|
|||||||
throw new Exception($"Missing download info for {lookupKey}");
|
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)
|
// 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)
|
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;
|
var repoAction = action.Reference as Pipelines.RepositoryPathReference;
|
||||||
if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias)
|
if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias)
|
||||||
@@ -562,6 +704,12 @@ namespace GitHub.Runner.Worker
|
|||||||
actionDirectory = Path.Combine(actionDirectory, repoAction.Path);
|
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
|
else
|
||||||
{
|
{
|
||||||
actionDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), repoAction.Name.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), repoAction.Ref);
|
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);
|
_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
|
else
|
||||||
{
|
{
|
||||||
@@ -980,10 +1149,33 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
if (actionsToResolve.Count > 0)
|
if (actionsToResolve.Count > 0)
|
||||||
{
|
{
|
||||||
var downloadInfos = await GetDownloadInfoAsync(executionContext, actionsToResolve);
|
IDictionary<string, WebApi.ActionDownloadInfo> downloadInfos = null;
|
||||||
foreach (var kvp in downloadInfos)
|
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)
|
if (!useActionArchiveCache)
|
||||||
{
|
{
|
||||||
await DownloadRepositoryArchive(executionContext, link, downloadInfo.Authentication?.Token, archiveFile);
|
await DownloadRepositoryArchive(executionContext, link, downloadInfo.Authentication?.Token, archiveFile);
|
||||||
@@ -1122,6 +1308,13 @@ namespace GitHub.Runner.Worker
|
|||||||
var stagingDirectory = Path.Combine(tempDirectory, "_staging");
|
var stagingDirectory = Path.Combine(tempDirectory, "_staging");
|
||||||
Directory.CreateDirectory(stagingDirectory);
|
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
|
#if OS_WINDOWS
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -1159,7 +1352,6 @@ namespace GitHub.Runner.Worker
|
|||||||
int exitCode = await processInvoker.ExecuteAsync(stagingDirectory, tar, $"-xzf \"{archiveFile}\"", null, executionContext.CancellationToken);
|
int exitCode = await processInvoker.ExecuteAsync(stagingDirectory, tar, $"-xzf \"{archiveFile}\"", null, executionContext.CancellationToken);
|
||||||
if (exitCode != 0)
|
if (exitCode != 0)
|
||||||
{
|
{
|
||||||
var fileInfo = new FileInfo(archiveFile);
|
|
||||||
var sha256hash = await IOUtil.GetFileContentSha256HashAsync(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}.");
|
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 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)
|
private ActionSetupInfo PrepareRepositoryActionAsync(IExecutionContext executionContext, Pipelines.ActionStep repositoryAction)
|
||||||
{
|
{
|
||||||
var repositoryReference = repositoryAction.Reference as Pipelines.RepositoryPathReference;
|
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)
|
private static string GetDownloadInfoLookupKey(Pipelines.ActionStep action)
|
||||||
{
|
{
|
||||||
if (action.Reference.Type != Pipelines.ActionSourceType.Repository)
|
if (action.Reference.Type != Pipelines.ActionSourceType.Repository)
|
||||||
@@ -1362,6 +1601,11 @@ namespace GitHub.Runner.Worker
|
|||||||
return null;
|
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))
|
if (!string.Equals(repositoryReference.RepositoryType, Pipelines.RepositoryTypes.GitHub, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
throw new NotSupportedException(repositoryReference.RepositoryType);
|
throw new NotSupportedException(repositoryReference.RepositoryType);
|
||||||
|
|||||||
@@ -55,7 +55,18 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
|||||||
break;
|
break;
|
||||||
case ActionSourceType.Repository:
|
case ActionSourceType.Repository:
|
||||||
var repositoryReference = step.Reference as RepositoryPathReference;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,6 +611,14 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
|||||||
Path = uses.Value
|
Path = uses.Value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else if (PipelineConstants.TryParseSelfRepository(uses.Value, out var selfPath))
|
||||||
|
{
|
||||||
|
result.Reference = new RepositoryPathReference
|
||||||
|
{
|
||||||
|
RepositoryType = PipelineConstants.SelfRepositoryAlias,
|
||||||
|
Path = selfPath
|
||||||
|
};
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var usesSegments = uses.Value.Split('@');
|
var usesSegments = uses.Value.Split('@');
|
||||||
|
|||||||
@@ -38,10 +38,43 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
public static readonly Int32 MaxNodeNameLength = 100;
|
public static readonly Int32 MaxNodeNameLength = 100;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Alias for the self repository.
|
/// Alias for the self local-workspace repository type (./ syntax).
|
||||||
|
/// Resolves to the local checkout on the runner.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly String SelfAlias = "self";
|
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>
|
/// <summary>
|
||||||
/// Error code during graph validation.
|
/// Error code during graph validation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1605,6 +1605,10 @@ namespace GitHub.Actions.WorkflowParser.Conversion
|
|||||||
{
|
{
|
||||||
id = WorkflowConstants.SelfAlias;
|
id = WorkflowConstants.SelfAlias;
|
||||||
}
|
}
|
||||||
|
else if (GitHub.DistributedTask.Pipelines.PipelineConstants.TryParseSelfRepository(action.Uses!.Value, out _))
|
||||||
|
{
|
||||||
|
id = WorkflowConstants.SelfRepositoryAlias;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var usesSegments = action.Uses!.Value.Split('@');
|
var usesSegments = action.Uses!.Value.Split('@');
|
||||||
|
|||||||
@@ -26,10 +26,15 @@ namespace GitHub.Actions.WorkflowParser
|
|||||||
internal const Int32 MaxNodeNameLength = 100;
|
internal const Int32 MaxNodeNameLength = 100;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Alias for the self repository.
|
/// Alias for the self local-workspace repository type (./ syntax).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal const String SelfAlias = "self";
|
internal const String SelfAlias = "self";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RepositoryType for self-repository references ($/ syntax).
|
||||||
|
/// </summary>
|
||||||
|
internal const String SelfRepositoryAlias = "selfRepository";
|
||||||
|
|
||||||
public static class PermissionsPolicy
|
public static class PermissionsPolicy
|
||||||
{
|
{
|
||||||
public const string LimitedRead = "LimitedRead";
|
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");
|
var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "main", "action.yml");
|
||||||
Assert.True(File.Exists(actionYamlFile));
|
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));
|
_hc.GetTrace().Info(File.ReadAllText(actionYamlFile));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -148,6 +153,11 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
|
|
||||||
// Act + Assert
|
// Act + Assert
|
||||||
await Assert.ThrowsAsync<InvalidActionArchiveException>(async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions));
|
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
|
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
|
#if OS_LINUX
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
@@ -530,9 +585,9 @@ runs:
|
|||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
string destDirectory = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "actions", "checkout", "master");
|
string destDirectory = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "actions", "checkout", "master");
|
||||||
Assert.True(Directory.Exists(destDirectory), "Destination directory does not exist");
|
Assert.True(Directory.Exists(destDirectory), "Destination directory does not exist");
|
||||||
var di = new DirectoryInfo(destDirectory);
|
var di = new DirectoryInfo(destDirectory);
|
||||||
Assert.NotNull(di.LinkTarget);
|
Assert.NotNull(di.LinkTarget);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -2386,7 +2441,7 @@ runs:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
public void LoadsNode24ActionDefinition()
|
public void LoadsNode24ActionDefinition()
|
||||||
@@ -2454,7 +2509,7 @@ runs:
|
|||||||
Teardown();
|
Teardown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[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]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
@@ -3468,5 +3533,604 @@ runs:
|
|||||||
Teardown();
|
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