From d6570abe36527c3e70b07706e572a17d4ee6c762 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 1/2] 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 From 4f97f9b5332f584951fe73f26b13064b95970079 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Thu, 3 Sep 2026 10:50:15 -0700 Subject: [PATCH 2/2] Tighten self-repository parser tests --- src/Test/L0/Worker/ActionManifestManagerL0.cs | 6 +++--- src/Test/L0/Worker/ActionManifestParserComparisonL0.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Test/L0/Worker/ActionManifestManagerL0.cs b/src/Test/L0/Worker/ActionManifestManagerL0.cs index e2c28285936..f42c928a624 100644 --- a/src/Test/L0/Worker/ActionManifestManagerL0.cs +++ b/src/Test/L0/Worker/ActionManifestManagerL0.cs @@ -810,9 +810,9 @@ public void Load_SelfRepositoryCompositeAction() 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); + Assert.Equal("$/.github/actions/inventory-client", Assert.IsType(composite.Steps[0]).Uses.Value); + Assert.Equal("$/actions/nested/composite", Assert.IsType(composite.Steps[1]).Uses.Value); + Assert.Equal("$/foo@v1", Assert.IsType(composite.Steps[2]).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); diff --git a/src/Test/L0/Worker/ActionManifestParserComparisonL0.cs b/src/Test/L0/Worker/ActionManifestParserComparisonL0.cs index 4454073ff1a..290944f6fa4 100644 --- a/src/Test/L0/Worker/ActionManifestParserComparisonL0.cs +++ b/src/Test/L0/Worker/ActionManifestParserComparisonL0.cs @@ -422,7 +422,7 @@ public void Load_SelfRepositoryReferences_BothParsersAgree() { // 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. + // green jobs emit spurious template errors. Setup(); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");