From 7fca99be604ee13ae9a91a0aee91982df5f298b7 Mon Sep 17 00:00:00 2001 From: Dylan <67774922+heavymachinery@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:34:30 -0500 Subject: [PATCH] Accept $/ self-repository in action manifest manager --- .../ActionManifestManagerWrapper.cs | 11 +++ .../Conversion/WorkflowTemplateConverter.cs | 3 +- src/Test/L0/Worker/ActionManifestManagerL0.cs | 37 ++++++++++ .../ActionManifestParserComparisonL0.cs | 67 +++++++++++++++++++ .../self_repository_composite_action.yml | 22 ++++++ 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/Test/TestData/self_repository_composite_action.yml diff --git a/src/Runner.Worker/ActionManifestManagerWrapper.cs b/src/Runner.Worker/ActionManifestManagerWrapper.cs index 6d893fd8252..531222f0ba8 100644 --- a/src/Runner.Worker/ActionManifestManagerWrapper.cs +++ b/src/Runner.Worker/ActionManifestManagerWrapper.cs @@ -274,6 +274,17 @@ private GitHub.DistributedTask.Pipelines.ActionStepDefinitionReference ParseActi }; } + // Self-repository reference: $/path/to/action + // Mirrors PipelineTemplateConverter.ConvertToStep, which folds any '@ref' into the path. + if (GitHub.DistributedTask.Pipelines.PipelineConstants.TryParseSelfRepository(uses, out var selfPath)) + { + return new GitHub.DistributedTask.Pipelines.RepositoryPathReference + { + RepositoryType = GitHub.DistributedTask.Pipelines.PipelineConstants.SelfRepositoryAlias, + Path = selfPath + }; + } + // Repository reference: owner/repo@ref or owner/repo/path@ref var atIndex = uses.LastIndexOf('@'); string refPart = null; diff --git a/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs b/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs index 7f344c4de63..28a829774b1 100644 --- a/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs +++ b/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs @@ -1759,7 +1759,8 @@ private static IStep ConvertToStep( if (!uses.Value.StartsWith(WorkflowTemplateConstants.DockerUriPrefix, StringComparison.Ordinal) && !uses.Value.StartsWith("./") && - !uses.Value.StartsWith(".\\")) + !uses.Value.StartsWith(".\\") && + !GitHub.DistributedTask.Pipelines.PipelineConstants.TryParseSelfRepository(uses.Value, out _)) { var usesSegments = uses.Value.Split('@'); var pathSegments = usesSegments[0].Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); diff --git a/src/Test/L0/Worker/ActionManifestManagerL0.cs b/src/Test/L0/Worker/ActionManifestManagerL0.cs index 6a3da0c7209..e2c28285936 100644 --- a/src/Test/L0/Worker/ActionManifestManagerL0.cs +++ b/src/Test/L0/Worker/ActionManifestManagerL0.cs @@ -786,6 +786,43 @@ public void Load_ConditionalCompositeAction() } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_SelfRepositoryCompositeAction() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManager(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "self_repository_composite_action.yml")); + + //Assert + Assert.Equal("Self Repository Composite", result.Name); + Assert.Equal(ActionExecutionType.Composite, result.Execution.ExecutionType); + + var composite = result.Execution as CompositeActionExecutionDataNew; + Assert.NotNull(composite); + Assert.Equal(6, composite.Steps.Count); + + Assert.Equal("$/.github/actions/inventory-client", (composite.Steps[0] as ActionStep).Uses.Value); + Assert.Equal("$/actions/nested/composite", (composite.Steps[1] as ActionStep).Uses.Value); + Assert.Equal("$/foo@v1", (composite.Steps[2] as ActionStep).Uses.Value); + + // No template errors should have been reported for the $/ steps + _ec.Verify(x => x.AddIssue(It.Is(s => s.Message.Contains("Expected format")), It.IsAny()), Times.Never); + } + finally + { + Teardown(); + } + } + [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] diff --git a/src/Test/L0/Worker/ActionManifestParserComparisonL0.cs b/src/Test/L0/Worker/ActionManifestParserComparisonL0.cs index 7551b0f57e6..4454073ff1a 100644 --- a/src/Test/L0/Worker/ActionManifestParserComparisonL0.cs +++ b/src/Test/L0/Worker/ActionManifestParserComparisonL0.cs @@ -413,6 +413,73 @@ public void Load_BothParsersRejectInvalidExpressionContext() } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_SelfRepositoryReferences_BothParsersAgree() + { + try + { + // Arrange — regression test: '$/' self-repository references must be + // accepted by the new parser exactly like the legacy parser, otherwise + // otherwise-green jobs emit spurious template errors. + Setup(); + _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); + + var legacyManager = new ActionManifestManagerLegacy(); + legacyManager.Initialize(_hc); + _hc.SetSingleton(legacyManager); + + var newManager = new ActionManifestManager(); + newManager.Initialize(_hc); + _hc.SetSingleton(newManager); + + var wrapper = new ActionManifestManagerWrapper(); + wrapper.Initialize(_hc); + + var manifestPath = Path.Combine(TestUtil.GetTestDataPath(), "self_repository_composite_action.yml"); + + // Act + var result = wrapper.Load(_ec.Object, manifestPath); + + // Assert - no mismatch recorded between the two parsers + Assert.False(_ec.Object.Global.HasActionManifestMismatch); + + Assert.NotNull(result); + Assert.Equal(ActionExecutionType.Composite, result.Execution.ExecutionType); + + var compositeExecution = result.Execution as CompositeActionExecutionData; + Assert.NotNull(compositeExecution); + Assert.Equal(6, compositeExecution.Steps.Count); + + // $/path resolves to the self-repository alias with the subpath preserved + var selfRepoRef = Assert.IsType(compositeExecution.Steps[0].Reference); + Assert.Equal(GitHub.DistributedTask.Pipelines.PipelineConstants.SelfRepositoryAlias, selfRepoRef.RepositoryType); + Assert.Equal(".github/actions/inventory-client", selfRepoRef.Path); + Assert.Null(selfRepoRef.Name); + Assert.Null(selfRepoRef.Ref); + + var nestedRef = Assert.IsType(compositeExecution.Steps[1].Reference); + Assert.Equal(GitHub.DistributedTask.Pipelines.PipelineConstants.SelfRepositoryAlias, nestedRef.RepositoryType); + Assert.Equal("actions/nested/composite", nestedRef.Path); + + // '$/foo@v1' — legacy folds the '@ref' into the path; the new parser must match + var withRefRef = Assert.IsType(compositeExecution.Steps[2].Reference); + Assert.Equal(GitHub.DistributedTask.Pipelines.PipelineConstants.SelfRepositoryAlias, withRefRef.RepositoryType); + Assert.Equal("foo@v1", withRefRef.Path); + + // Sanity: ordinary references are unaffected + var externalRef = Assert.IsType(compositeExecution.Steps[3].Reference); + Assert.Equal("GitHub", externalRef.RepositoryType); + Assert.Equal("actions/checkout", externalRef.Name); + Assert.Equal("v4", externalRef.Ref); + } + finally + { + Teardown(); + } + } + private string GetFullExceptionMessage(Exception ex) { var messages = new List(); diff --git a/src/Test/TestData/self_repository_composite_action.yml b/src/Test/TestData/self_repository_composite_action.yml new file mode 100644 index 00000000000..bdb48a554b7 --- /dev/null +++ b/src/Test/TestData/self_repository_composite_action.yml @@ -0,0 +1,22 @@ +name: 'Self Repository Composite' +description: 'Test composite action referencing actions in the same repository via $/' +runs: + using: "composite" + steps: + - uses: $/.github/actions/inventory-client + id: inventory + + - uses: $/actions/nested/composite + id: nested + + - uses: $/foo@v1 + id: with-ref + + - uses: actions/checkout@v4 + id: external + + - uses: ./local-action + id: local + + - run: echo done + shell: bash