From 383d447b72c07c81ef0615f200d3d777c3b6b905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Wed, 15 Apr 2026 07:38:26 +0200 Subject: [PATCH 01/10] Add support for fileSystem based br/ module aliases --- .../OciArtifactModuleReferenceTests.cs | 4 +- .../OciArtifactEmulatedReferenceTests.cs | 242 ++++++++++++++++++ .../Utils/OciRegistryHelper.cs | 2 +- .../ModuleAliasesConfiguration.cs | 17 +- .../Diagnostics/DiagnosticBuilder.cs | 12 +- .../Modules/ModuleReferenceSchemes.cs | 2 + .../DefaultArtifactRegistryProvider.cs | 4 +- .../Registry/FileSystemModuleRegistry.cs | 47 ++++ .../Oci/OciArtifactEmulatedReference.cs | 125 +++++++++ .../Registry/Oci/OciArtifactReferenceFacts.cs | 3 + .../Registry/OciArtifactRegistry.cs | 35 ++- 11 files changed, 482 insertions(+), 11 deletions(-) create mode 100644 src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs create mode 100644 src/Bicep.Core/Registry/FileSystemModuleRegistry.cs create mode 100644 src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs diff --git a/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs b/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs index e61f25f32b9..8bc76199d80 100644 --- a/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs +++ b/src/Bicep.Core.UnitTests/Modules/OciArtifactModuleReferenceTests.cs @@ -277,7 +277,7 @@ private static IEnumerable GetInvalidAliasData() ["moduleAliases.br.myModulePath.modulePath"] = "path", }), "BCP216", - "The OCI artifact module alias \"myModulePath\" in the built-in Bicep configuration is invalid. The \"registry\" property cannot be null or undefined.", + "The OCI artifact module alias \"myModulePath\" in the built-in Bicep configuration is invalid. Either the \"registry\" or \"fileSystem\" property must be specified.", }; yield return new object[] @@ -291,7 +291,7 @@ private static IEnumerable GetInvalidAliasData() }, "/bicepconfig.json"), "BCP216", - "The OCI artifact module alias \"myModulePath2\" in the Bicep configuration \"/bicepconfig.json\" is invalid. The \"registry\" property cannot be null or undefined.", + "The OCI artifact module alias \"myModulePath2\" in the Bicep configuration \"/bicepconfig.json\" is invalid. Either the \"registry\" or \"fileSystem\" property must be specified.", }; } diff --git a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs new file mode 100644 index 00000000000..6f4f4a6e2fb --- /dev/null +++ b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO.Abstractions.TestingHelpers; +using Bicep.Core.Configuration; +using Bicep.Core.Diagnostics; +using Bicep.Core.Registry; +using Bicep.Core.Registry.Oci; +using Bicep.Core.SourceGraph; +using Bicep.Core.UnitTests.Assertions; +using Bicep.IO.Abstraction; +using Bicep.IO.FileSystem; +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.Core.UnitTests.Registry.Oci +{ + [TestClass] + public class OciArtifactEmulatedReferenceTests + { + [TestMethod] + [DataRow("keyvault:1.0.0", "keyvault")] + [DataRow("storage/queue:v2.0", "storage/queue")] + [DataRow("keyvault@sha256:e207a69d02b3de40d48ede9fd208d80441a9e590a83a0bc915d46244c03310d4", "keyvault")] + [DataRow("keyvault", "keyvault")] + [DataRow("a/b/c:latest", "a/b/c")] + [DataRow("mymodule:v1", "mymodule")] + [DataRow("", "")] + [DataRow(":", "")] + [DataRow("@sha256:abc", "")] + public void ExtractModulePath_ShouldExtractCorrectPath(string input, string expected) + { + OciArtifactEmulatedReference.ExtractModulePath(input).Should().Be(expected); + } + + [TestMethod] + public void TryParse_EmptyModulePath_ShouldFail() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "./modules", + configFileUri, + ":1.0.0", + fileExplorer); + + result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); + var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); + diagnostic.Code.Should().Be("BCP090"); + } + + [TestMethod] + public void TryParse_ValidModulePath_ShouldSucceed() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "../bicepModules", + configFileUri, + "keyvault:1.0.0", + fileExplorer); + + result.IsSuccess(out var reference, out _).Should().BeTrue(); + reference!.UnqualifiedReference.Should().Be("keyvault"); + reference!.FullyQualifiedReference.Should().Be("br:keyvault"); + reference!.IsExternal.Should().BeFalse(); + reference.Scheme.Should().Be("br-fs"); + } + + [TestMethod] + public void TryParse_MultiSegmentModulePath_ShouldSucceed() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "./modules", + configFileUri, + "storage/queue:v2.0", + fileExplorer); + + result.IsSuccess(out var reference, out _).Should().BeTrue(); + reference!.UnqualifiedReference.Should().Be("storage/queue"); + reference!.FullyQualifiedReference.Should().Be("br:storage/queue"); + } + + [TestMethod] + public void TryParse_DigestReference_ShouldIgnoreDigestAndSucceed() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "./modules", + configFileUri, + "keyvault@sha256:e207a69d02b3de40d48ede9fd208d80441a9e590a83a0bc915d46244c03310d4", + fileExplorer); + + result.IsSuccess(out var reference, out _).Should().BeTrue(); + reference!.UnqualifiedReference.Should().Be("keyvault"); + reference!.FullyQualifiedReference.Should().Be("br:keyvault"); + } + + [TestMethod] + public void TryGetEntryPointFileHandle_ShouldReturnFileHandle() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var parseResult = OciArtifactEmulatedReference.TryParse( + referencingFile, + "../bicepModules", + configFileUri, + "keyvault:1.0.0", + fileExplorer); + + parseResult.IsSuccess(out var reference, out _).Should().BeTrue(); + + var entryPointResult = reference!.TryGetEntryPointFileHandle(); + + entryPointResult.IsSuccess(out var fileHandle, out _).Should().BeTrue(); + fileHandle.Should().NotBeNull(); + fileHandle!.Uri.Path.Should().Contain("keyvault.bicep"); + } + + [TestMethod] + public void Equals_SameReferences_ShouldBeEqual() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result1 = OciArtifactEmulatedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "keyvault:1.0.0", fileExplorer); + var result2 = OciArtifactEmulatedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "keyvault:2.0.0", fileExplorer); + + result1.IsSuccess(out var ref1, out _).Should().BeTrue(); + result2.IsSuccess(out var ref2, out _).Should().BeTrue(); + + ref1!.Equals(ref2).Should().BeTrue(); + ref1.GetHashCode().Should().Be(ref2!.GetHashCode()); + } + + [TestMethod] + public void Equals_DifferentModulePaths_ShouldNotBeEqual() + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result1 = OciArtifactEmulatedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "keyvault:1.0.0", fileExplorer); + var result2 = OciArtifactEmulatedReference.TryParse( + referencingFile, "../bicepModules", configFileUri, "storage:1.0.0", fileExplorer); + + result1.IsSuccess(out var ref1, out _).Should().BeTrue(); + result2.IsSuccess(out var ref2, out _).Should().BeTrue(); + + ref1!.Equals(ref2).Should().BeFalse(); + } + + [TestMethod] + public void TryGetOciArtifactModuleAlias_BothRegistryAndFileSystemSet_ShouldFail() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliases.br.myAlias.registry"] = "example.azurecr.io", + ["moduleAliases.br.myAlias.fileSystem"] = "../bicepModules", + }); + + var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + + result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); + var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); + diagnostic.Code.Should().Be("BCP446"); + diagnostic.Message.Should().Contain("mutually exclusive"); + } + + [TestMethod] + public void TryGetOciArtifactModuleAlias_NeitherRegistryNorFileSystemSet_ShouldFail() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliases.br.myAlias.modulePath"] = "path", + }); + + var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + + result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); + var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); + diagnostic.Code.Should().Be("BCP216"); + diagnostic.Message.Should().Contain("fileSystem"); + } + + [TestMethod] + public void TryGetOciArtifactModuleAlias_OnlyFileSystemSet_ShouldSucceed() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliases.br.myAlias.fileSystem"] = "../bicepModules", + }); + + var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + + result.IsSuccess(out var alias, out _).Should().BeTrue(); + alias!.FileSystem.Should().Be("../bicepModules"); + alias.Registry.Should().BeNull(); + } + + [TestMethod] + public void TryGetOciArtifactModuleAlias_OnlyRegistrySet_ShouldSucceed() + { + var configuration = BicepTestConstants.CreateMockConfiguration( + new() + { + ["moduleAliases.br.myAlias.registry"] = "example.azurecr.io", + }); + + var result = configuration.ModuleAliases.TryGetOciArtifactModuleAlias("myAlias"); + + result.IsSuccess(out var alias, out _).Should().BeTrue(); + alias!.Registry.Should().Be("example.azurecr.io"); + alias.FileSystem.Should().BeNull(); + } + } +} diff --git a/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs b/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs index 82c024b84dd..4460e30fd00 100644 --- a/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs +++ b/src/Bicep.Core.UnitTests/Utils/OciRegistryHelper.cs @@ -77,7 +77,7 @@ public static (OciArtifactRegistry, FakeRegistryBlobClient) CreateModuleRegistry .Setup(m => m.CreateAuthenticatedBlobClient(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(blobClient); - var registry = new OciArtifactRegistry(clientFactory.Object, StrictMock.Of().Object); + var registry = new OciArtifactRegistry(clientFactory.Object, StrictMock.Of().Object, new FileSystemFileExplorer(new MockFileSystem())); return (registry, blobClient); } diff --git a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs index 1e3f30ba77a..53c35c7c37b 100644 --- a/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs +++ b/src/Bicep.Core/Configuration/ModuleAliasesConfiguration.cs @@ -37,9 +37,13 @@ public record OciArtifactModuleAlias public string? ModulePath { get; init; } - public override string ToString() => this.ModulePath is not null - ? $"{Registry}/{ModulePath}" - : $"{Registry}"; + public string? FileSystem { get; init; } + + public override string ToString() => this.FileSystem is not null + ? $"{FileSystem}" + : this.ModulePath is not null + ? $"{Registry}/{ModulePath}" + : $"{Registry}"; } public partial class ModuleAliasesConfiguration : ConfigurationSection @@ -101,7 +105,12 @@ public ResultWithDiagnosticBuilder TryGetOciArtifactModu return new(x => x.OciArtifactModuleAliasNameDoesNotExistInConfiguration(aliasName, configFileUri)); } - if (alias.Registry is null) + if (alias.Registry is not null && alias.FileSystem is not null) + { + return new(x => x.InvalidOciArtifactModuleAliasRegistryAndFileSystemSetTogether(aliasName, configFileUri)); + } + + if (alias.Registry is null && alias.FileSystem is null) { return new(x => x.InvalidOciArtifactModuleAliasRegistryNullOrUndefined(aliasName, configFileUri)); } diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index 999aba6eb7b..b64adee4be6 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -991,7 +991,7 @@ public Diagnostic ReferencedArmTemplateHasErrors() => CoreError( public Diagnostic UnknownModuleReferenceScheme(string badScheme, ImmutableArray allowedSchemes) { - string FormatSchemes() => ToQuotedString(allowedSchemes.Where(scheme => !string.Equals(scheme, ArtifactReferenceSchemes.Local))); + string FormatSchemes() => ToQuotedString(allowedSchemes.Where(scheme => !string.Equals(scheme, ArtifactReferenceSchemes.Local) && scheme != ArtifactReferenceSchemes.OciEmulated)); return CoreError( "BCP189", @@ -1111,7 +1111,7 @@ public Diagnostic InvalidTemplateSpecAliasResourceGroupNullOrUndefined(string al public Diagnostic InvalidOciArtifactModuleAliasRegistryNullOrUndefined(string aliasName, IOUri? configFileUri) => CoreError( "BCP216", - $"The OCI artifact module alias \"{aliasName}\" in the {BuildBicepConfigurationClause(configFileUri)} is invalid. The \"registry\" property cannot be null or undefined."); + $"The OCI artifact module alias \"{aliasName}\" in the {BuildBicepConfigurationClause(configFileUri)} is invalid. Either the \"registry\" or \"fileSystem\" property must be specified."); public Diagnostic InvalidTemplateSpecReferenceInvalidSubscriptionId(string? aliasName, string subscriptionId, string referenceValue) => CoreError( "BCP217", @@ -2026,6 +2026,14 @@ public Diagnostic RuntimeValueNotAllowedInExtensionDeclarationWithClause(string? public Diagnostic NullIfNotFoundOnlyValidOnExistingResources() => CoreError( "BCP445", $@"The ""@{LanguageConstants.NullIfNotFoundDecoratorName}()"" decorator can only be used on existing resources."); + + public Diagnostic InvalidOciArtifactModuleAliasRegistryAndFileSystemSetTogether(string aliasName, IOUri? configFileUri) => CoreError( + "BCP446", + $"The OCI artifact module alias \"{aliasName}\" in the {BuildBicepConfigurationClause(configFileUri)} is invalid. The \"registry\" and \"fileSystem\" properties are mutually exclusive."); + + public Diagnostic OciArtifactModuleAliasFileSystemOnlySupportsModules(string aliasName) => CoreError( + "BCP447", + $"The OCI artifact module alias \"{aliasName}\" has a \"fileSystem\" property which is only supported for modules, not extensions."); } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs b/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs index 75d3f813f1a..b399a229042 100644 --- a/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs +++ b/src/Bicep.Core/Modules/ModuleReferenceSchemes.cs @@ -11,6 +11,8 @@ public static class ArtifactReferenceSchemes public const string Oci = OciArtifactReferenceFacts.Scheme; + public const string OciEmulated = OciArtifactReferenceFacts.EmulatedScheme; + public const string TemplateSpecs = "ts"; } } diff --git a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs index a1bcf5268a6..70d0f76d274 100644 --- a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs +++ b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Bicep.Core.Registry.Catalog; +using Bicep.IO.Abstraction; using Microsoft.Extensions.DependencyInjection; namespace Bicep.Core.Registry @@ -12,7 +13,8 @@ public DefaultArtifactRegistryProvider(IServiceProvider serviceProvider, IContai : base(new IArtifactRegistry[] { new LocalModuleRegistry(), - new OciArtifactRegistry(clientFactory, serviceProvider.GetRequiredService()), + new OciArtifactRegistry(clientFactory, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService()), + new FileSystemModuleRegistry(), new TemplateSpecModuleRegistry(templateSpecRepositoryFactory), }) { diff --git a/src/Bicep.Core/Registry/FileSystemModuleRegistry.cs b/src/Bicep.Core/Registry/FileSystemModuleRegistry.cs new file mode 100644 index 00000000000..b456468b745 --- /dev/null +++ b/src/Bicep.Core/Registry/FileSystemModuleRegistry.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.Diagnostics; +using Bicep.Core.Modules; +using Bicep.Core.Registry.Oci; +using Bicep.Core.Semantics; +using Bicep.Core.SourceGraph; +using Bicep.Core.SourceLink; + +namespace Bicep.Core.Registry +{ + public class FileSystemModuleRegistry : ArtifactRegistry + { + public override string Scheme => ArtifactReferenceSchemes.OciEmulated; + + public override RegistryCapabilities GetCapabilities(ArtifactType artifactType, OciArtifactEmulatedReference reference) + => RegistryCapabilities.Default; + + public override ResultWithDiagnosticBuilder TryParseArtifactReference(BicepSourceFile referencingFile, ArtifactType artifactType, string? aliasName, string reference) + => throw new NotSupportedException("Parsing is handled by OciArtifactRegistry."); + + public override bool IsArtifactRestoreRequired(OciArtifactEmulatedReference reference) => false; + + public override Task CheckArtifactExists(ArtifactType artifactType, OciArtifactEmulatedReference reference) + => Task.FromResult(reference.TryGetEntryPointFileHandle().IsSuccess(out var fileHandle, out _) && fileHandle.Exists()); + + public override Task> RestoreArtifacts(IEnumerable references) + => Task.FromResult>( + new Dictionary()); + + public override Task> InvalidateArtifactsCache(IEnumerable references) + => Task.FromResult>( + new Dictionary()); + + public override Task PublishModule(OciArtifactEmulatedReference reference, BinaryData compiled, BinaryData? bicepSources, string? documentationUri, string? description) + => throw new NotSupportedException("Publishing is not supported for filesystem-based module aliases."); + + public override Task PublishExtension(OciArtifactEmulatedReference reference, ExtensionPackage package) + => throw new NotSupportedException("Publishing is not supported for filesystem-based module aliases."); + + public override string? TryGetDocumentationUri(OciArtifactEmulatedReference reference) => null; + + public override Task TryGetModuleDescription(ModuleSymbol module, OciArtifactEmulatedReference reference) + => Task.FromResult(null); + } +} diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs new file mode 100644 index 00000000000..3b95a813ad8 --- /dev/null +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.Diagnostics; +using Bicep.Core.Modules; +using Bicep.Core.SourceGraph; +using Bicep.IO.Abstraction; + +namespace Bicep.Core.Registry.Oci +{ + /// + /// Represents an OCI module reference that is emulated via a local filesystem path. + /// When a module alias specifies a "fileSystem" property instead of "registry", + /// module references are resolved to local .bicep files instead of pulling from a container registry. + /// + public class OciArtifactEmulatedReference : ArtifactReference + { + private readonly IFileHandle fileHandle; + + public OciArtifactEmulatedReference(BicepSourceFile referencingFile, string modulePath, IFileHandle fileHandle) : + base(referencingFile, OciArtifactReferenceFacts.EmulatedScheme) + { + this.modulePath = modulePath; + this.fileHandle = fileHandle; + } + + private readonly string modulePath; + + // Override FullyQualifiedReference so user-facing diagnostics shows "br:..." + public override string FullyQualifiedReference => $"{OciArtifactReferenceFacts.Scheme}:{UnqualifiedReference}"; + + public override string UnqualifiedReference => modulePath; + + public override bool IsExternal => false; + + public override ResultWithDiagnosticBuilder TryGetEntryPointFileHandle() + { + return new(fileHandle); + } + + // Extracts the module path from an unqualified reference string by removing any tag or digest suffix + public static string ExtractModulePath(string unqualifiedReference) + { + // Check for digest separator (@) + var digestIndex = unqualifiedReference.IndexOf('@'); + if (digestIndex >= 0) + { + return unqualifiedReference[..digestIndex]; + } + + // Check for tag separator (:) + var tagIndex = unqualifiedReference.LastIndexOf(':'); + if (tagIndex >= 0) + { + return unqualifiedReference[..tagIndex]; + } + + // No tag or digest — use the whole reference as the module path + return unqualifiedReference; + } + + // referencingFile is the Bicep source file containing the module reference + // fileSystemPath is the filesystem path from the alias configuration + // configFileUri is the URI of the bicepconfig.json file, used to resolve relative paths + // unqualifiedReference is the unqualified reference string (e.g., "keyvault:1.0.0") + // fileExplorer is the file explorer used to create file handles + public static ResultWithDiagnosticBuilder TryParse( + BicepSourceFile referencingFile, + string fileSystemPath, + IOUri? configFileUri, + string unqualifiedReference, + IFileExplorer fileExplorer) + { + var modulePath = ExtractModulePath(unqualifiedReference); + + if (string.IsNullOrEmpty(modulePath)) + { + return new(x => x.ModulePathHasNotBeenSpecified()); + } + + // Resolve the filesystem base directory relative to bicepconfig.json + IOUri baseUri; + if (configFileUri is not null) + { + // Ensure the fileSystem path ends with '/' so it's treated as a directory + var directoryPath = fileSystemPath.EndsWith('/') || fileSystemPath.EndsWith('\\') + ? fileSystemPath + : fileSystemPath + "/"; + baseUri = configFileUri.Resolve(directoryPath); + } + else + { + baseUri = IOUri.FromFilePath(fileSystemPath); + } + + // Construct the file URI by appending the module path with a .bicep extension. + var moduleFileName = modulePath + ".bicep"; + var moduleFileUri = baseUri.Resolve(moduleFileName); + + var fileHandle = fileExplorer.GetFile(moduleFileUri); + + return new(new OciArtifactEmulatedReference(referencingFile, modulePath, fileHandle)); + } + + public override bool Equals(object? obj) + { + if (obj is not OciArtifactEmulatedReference other) + { + return false; + } + + return StringComparer.Ordinal.Equals(modulePath, other.modulePath) && + fileHandle.Equals(other.fileHandle); + } + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(modulePath, StringComparer.Ordinal); + hash.Add(fileHandle); + + return hash.ToHashCode(); + } + } +} diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs b/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs index 7e9faecd9a7..8a52a0ee355 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactReferenceFacts.cs @@ -8,6 +8,9 @@ namespace Bicep.Core.Registry.Oci public static partial class OciArtifactReferenceFacts { public const string Scheme = "br"; + + public const string EmulatedScheme = "br-fs"; + public const string SchemeWithColon = Scheme + ":"; public const int MaxRegistryLength = 255; diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index 984fb8d727e..8de8475b3a0 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -30,12 +30,16 @@ public sealed class OciArtifactRegistry : ExternalArtifactRegistry ArtifactReferenceSchemes.Oci; @@ -48,6 +52,35 @@ public override RegistryCapabilities GetCapabilities(ArtifactType artifactType, public override ResultWithDiagnosticBuilder TryParseArtifactReference(BicepSourceFile referencingFile, ArtifactType artifactType, string? aliasName, string reference) { + // Check if the alias resolves to a filesystem-based alias. + if (aliasName is not null) + { + if (!referencingFile.Configuration.ModuleAliases.TryGetOciArtifactModuleAlias(aliasName).IsSuccess(out var alias, out var aliasFailureBuilder)) + { + return new(aliasFailureBuilder); + } + + if (alias.FileSystem is not null) + { + if (artifactType != ArtifactType.Module) + { + return new(x => x.OciArtifactModuleAliasFileSystemOnlySupportsModules(aliasName)); + } + + if (!OciArtifactEmulatedReference.TryParse( + referencingFile, + alias.FileSystem, + referencingFile.Configuration.ConfigFileUri, + reference, + this.fileExplorer).IsSuccess(out var emulatedRef, out var emulatedFailureBuilder)) + { + return new(emulatedFailureBuilder); + } + + return new(emulatedRef); + } + } + if (!OciArtifactReference.TryParse(referencingFile, artifactType, aliasName, reference).IsSuccess(out var @ref, out var failureBuilder)) { return new(failureBuilder); From 07a8148a5462fcb73d722feab9854d7fa8927f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Thu, 16 Apr 2026 21:32:50 +0200 Subject: [PATCH 02/10] Add baseline for emulated br alias --- .../baselines/Registry_LF/bicepconfig.json | 3 + .../Files/baselines/Registry_LF/main.bicep | 13 +- .../Registry_LF/main.diagnostics.bicep | 12 ++ .../Registry_LF/main.formatted.bicep | 13 ++ .../Files/baselines/Registry_LF/main.ir.bicep | 50 +++++++- .../Files/baselines/Registry_LF/main.json | 103 ++++++++++++++- .../Registry_LF/main.sourcemap.bicep | 113 +++++++++++++++++ .../Registry_LF/main.symbolicnames.json | 104 ++++++++++++++- .../baselines/Registry_LF/main.symbols.bicep | 14 ++ .../baselines/Registry_LF/main.syntax.bicep | 120 +++++++++++++++++- .../baselines/Registry_LF/main.tokens.bicep | 72 ++++++++++- 11 files changed, 610 insertions(+), 7 deletions(-) diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json index 047ab63222d..161bd860fd4 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/bicepconfig.json @@ -13,6 +13,9 @@ "demo-two": { "registry": "mock-registry-two.invalid", "modulePath": "demo" + }, + "mock-registry-emulated": { + "fileSystem": "Publish" } } } diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep index 6480bc5dcba..8a35621edd3 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep @@ -54,6 +54,17 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { + name: '${site.name}siteDeploy3' + scope: rg + params: { + appPlanId: appPlanDeploy.outputs.planId + namePrefix: site.name + dockerImage: 'nginxdemos/hello' + dockerImageTag: site.tag + } +}] + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { name: 'storageDeploy' scope: rg @@ -130,4 +141,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { params: { ipv6port: 'test' } -} \ No newline at end of file +} diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep index b104fe240fe..13575506321 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep @@ -54,6 +54,17 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { + name: '${site.name}siteDeploy3' + scope: rg + params: { + appPlanId: appPlanDeploy.outputs.planId + namePrefix: site.name + dockerImage: 'nginxdemos/hello' + dockerImageTag: site.tag + } +}] + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { name: 'storageDeploy' scope: rg @@ -132,3 +143,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { ipv6port: 'test' } } + diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep index 5f20b35edaf..95d8588d6c1 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep @@ -58,6 +58,19 @@ module siteDeploy2 'br/demo-two:site:v3' = [ } ] +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [ + for site in websites: { + name: '${site.name}siteDeploy3' + scope: rg + params: { + appPlanId: appPlanDeploy.outputs.planId + namePrefix: site.name + dockerImage: 'nginxdemos/hello' + dockerImageTag: site.tag + } + } +] + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { name: 'storageDeploy' scope: rg diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep index 8d0b57400a4..0e0568069e1 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep @@ -1,5 +1,5 @@ targetScope = 'subscription' -//@[000:2463) ProgramExpression +//@[000:2747) ProgramExpression //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] @@ -12,6 +12,10 @@ targetScope = 'subscription' //@[000:0000) | | └─ModuleReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] +//@[000:0000) | ├─ResourceDependencyExpression [UNPARENTED] +//@[000:0000) | | └─ModuleReferenceExpression [UNPARENTED] +//@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] +//@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] @@ -187,6 +191,49 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { +//@[000:0281) ├─DeclaredModuleExpression +//@[057:0281) | ├─ForLoopExpression +//@[070:0078) | | ├─VariableReferenceExpression { Variable = websites } +//@[080:0280) | | └─ObjectExpression +//@[070:0078) | | └─VariableReferenceExpression { Variable = websites } +//@[070:0078) | | | └─VariableReferenceExpression { Variable = websites } +//@[070:0078) | | └─VariableReferenceExpression { Variable = websites } + name: '${site.name}siteDeploy3' +//@[002:0033) | | └─ObjectPropertyExpression +//@[002:0006) | | ├─StringLiteralExpression { Value = name } +//@[008:0033) | | └─InterpolatedStringExpression +//@[011:0020) | | └─PropertyAccessExpression { PropertyName = name } +//@[011:0015) | | └─ArrayAccessExpression +//@[011:0015) | | ├─CopyIndexExpression + scope: rg + params: { +//@[010:0150) | ├─ObjectExpression + appPlanId: appPlanDeploy.outputs.planId +//@[004:0043) | | ├─ObjectPropertyExpression +//@[004:0013) | | | ├─StringLiteralExpression { Value = appPlanId } +//@[015:0043) | | | └─ModuleOutputPropertyAccessExpression { PropertyName = planId } +//@[015:0036) | | | └─PropertyAccessExpression { PropertyName = outputs } +//@[015:0028) | | | └─ModuleReferenceExpression + namePrefix: site.name +//@[004:0025) | | ├─ObjectPropertyExpression +//@[004:0014) | | | ├─StringLiteralExpression { Value = namePrefix } +//@[016:0025) | | | └─PropertyAccessExpression { PropertyName = name } +//@[016:0020) | | | └─ArrayAccessExpression +//@[016:0020) | | | ├─CopyIndexExpression + dockerImage: 'nginxdemos/hello' +//@[004:0035) | | ├─ObjectPropertyExpression +//@[004:0015) | | | ├─StringLiteralExpression { Value = dockerImage } +//@[017:0035) | | | └─StringLiteralExpression { Value = nginxdemos/hello } + dockerImageTag: site.tag +//@[004:0028) | | └─ObjectPropertyExpression +//@[004:0018) | | ├─StringLiteralExpression { Value = dockerImageTag } +//@[020:0028) | | └─PropertyAccessExpression { PropertyName = tag } +//@[020:0024) | | └─ArrayAccessExpression +//@[020:0024) | | ├─CopyIndexExpression + } +}] + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[000:0168) ├─DeclaredModuleExpression //@[090:0168) | ├─ObjectExpression @@ -374,3 +421,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { //@[014:0020) | | └─StringLiteralExpression { Value = test } } } + diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json index 703ac380df8..1c474d2bc7e 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "16097344762357511977" + "templateHash": "16849026672229767237" } }, "variables": { @@ -361,6 +361,107 @@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" ] }, + { + "copy": { + "name": "siteDeploy3", + "count": "[length(variables('websites'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}siteDeploy3', variables('websites')[copyIndex()].name)]", + "resourceGroup": "adotfrank-rg", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "appPlanId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy'), '2025-04-01').outputs.planId.value]" + }, + "namePrefix": { + "value": "[variables('websites')[copyIndex()].name]" + }, + "dockerImage": { + "value": "nginxdemos/hello" + }, + "dockerImageTag": { + "value": "[variables('websites')[copyIndex()].tag]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "dev", + "templateHash": "15188988612540889945" + } + }, + "parameters": { + "namePrefix": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "dockerImage": { + "type": "string" + }, + "dockerImageTag": { + "type": "string" + }, + "appPlanId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Web/sites", + "apiVersion": "2020-06-01", + "name": "[format('{0}site', parameters('namePrefix'))]", + "location": "[parameters('location')]", + "properties": { + "siteConfig": { + "appSettings": [ + { + "name": "DOCKER_REGISTRY_SERVER_URL", + "value": "https://index.docker.io" + }, + { + "name": "DOCKER_REGISTRY_SERVER_USERNAME", + "value": "" + }, + { + "name": "DOCKER_REGISTRY_SERVER_PASSWORD", + "value": "" + }, + { + "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", + "value": "false" + } + ], + "linuxFxVersion": "[format('DOCKER|{0}:{1}', parameters('dockerImage'), parameters('dockerImageTag'))]" + }, + "serverFarmId": "[parameters('appPlanId')]" + } + } + ], + "outputs": { + "siteUrl": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Web/sites', format('{0}site', parameters('namePrefix'))), '2020-06-01').hostNames[0]]" + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" + ] + }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep index f286a23b89d..b9d0030cc38 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep @@ -394,6 +394,118 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { +//@ { +//@ "copy": { +//@ "name": "siteDeploy3", +//@ "count": "[length(variables('websites'))]" +//@ }, +//@ "type": "Microsoft.Resources/deployments", +//@ "apiVersion": "2025-04-01", +//@ "resourceGroup": "adotfrank-rg", +//@ "properties": { +//@ "expressionEvaluationOptions": { +//@ "scope": "inner" +//@ }, +//@ "mode": "Incremental", +//@ "template": { +//@ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", +//@ "contentVersion": "1.0.0.0", +//@ "metadata": { +//@ "_generator": { +//@ "name": "bicep", +//@ "version": "dev", +//@ "templateHash": "15188988612540889945" +//@ } +//@ }, +//@ "parameters": { +//@ "namePrefix": { +//@ "type": "string" +//@ }, +//@ "location": { +//@ "type": "string", +//@ "defaultValue": "[resourceGroup().location]" +//@ }, +//@ "dockerImage": { +//@ "type": "string" +//@ }, +//@ "dockerImageTag": { +//@ "type": "string" +//@ }, +//@ "appPlanId": { +//@ "type": "string" +//@ } +//@ }, +//@ "resources": [ +//@ { +//@ "type": "Microsoft.Web/sites", +//@ "apiVersion": "2020-06-01", +//@ "name": "[format('{0}site', parameters('namePrefix'))]", +//@ "location": "[parameters('location')]", +//@ "properties": { +//@ "siteConfig": { +//@ "appSettings": [ +//@ { +//@ "name": "DOCKER_REGISTRY_SERVER_URL", +//@ "value": "https://index.docker.io" +//@ }, +//@ { +//@ "name": "DOCKER_REGISTRY_SERVER_USERNAME", +//@ "value": "" +//@ }, +//@ { +//@ "name": "DOCKER_REGISTRY_SERVER_PASSWORD", +//@ "value": "" +//@ }, +//@ { +//@ "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", +//@ "value": "false" +//@ } +//@ ], +//@ "linuxFxVersion": "[format('DOCKER|{0}:{1}', parameters('dockerImage'), parameters('dockerImageTag'))]" +//@ }, +//@ "serverFarmId": "[parameters('appPlanId')]" +//@ } +//@ } +//@ ], +//@ "outputs": { +//@ "siteUrl": { +//@ "type": "string", +//@ "value": "[reference(resourceId('Microsoft.Web/sites', format('{0}site', parameters('namePrefix'))), '2020-06-01').hostNames[0]]" +//@ } +//@ } +//@ } +//@ }, +//@ "dependsOn": [ +//@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy')]", +//@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" +//@ ] +//@ }, + name: '${site.name}siteDeploy3' +//@ "name": "[format('{0}siteDeploy3', variables('websites')[copyIndex()].name)]", + scope: rg + params: { +//@ "parameters": { +//@ }, + appPlanId: appPlanDeploy.outputs.planId +//@ "appPlanId": { +//@ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy'), '2025-04-01').outputs.planId.value]" +//@ }, + namePrefix: site.name +//@ "namePrefix": { +//@ "value": "[variables('websites')[copyIndex()].name]" +//@ }, + dockerImage: 'nginxdemos/hello' +//@ "dockerImage": { +//@ "value": "nginxdemos/hello" +//@ }, + dockerImageTag: site.tag +//@ "dockerImageTag": { +//@ "value": "[variables('websites')[copyIndex()].tag]" +//@ } + } +}] + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@ { //@ "type": "Microsoft.Resources/deployments", @@ -780,3 +892,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { //@ } } } + diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json index f90b2d774a0..e9b482322da 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "3717084932040700305" + "templateHash": "9031371693506242739" } }, "variables": { @@ -366,6 +366,108 @@ "rg" ] }, + "siteDeploy3": { + "copy": { + "name": "siteDeploy3", + "count": "[length(variables('websites'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}siteDeploy3', variables('websites')[copyIndex()].name)]", + "resourceGroup": "adotfrank-rg", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "appPlanId": { + "value": "[reference('appPlanDeploy').outputs.planId.value]" + }, + "namePrefix": { + "value": "[variables('websites')[copyIndex()].name]" + }, + "dockerImage": { + "value": "nginxdemos/hello" + }, + "dockerImageTag": { + "value": "[variables('websites')[copyIndex()].tag]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "dev", + "templateHash": "1727609956407115618" + } + }, + "parameters": { + "namePrefix": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "dockerImage": { + "type": "string" + }, + "dockerImageTag": { + "type": "string" + }, + "appPlanId": { + "type": "string" + } + }, + "resources": { + "namePrefix_site": { + "type": "Microsoft.Web/sites", + "apiVersion": "2020-06-01", + "name": "[format('{0}site', parameters('namePrefix'))]", + "location": "[parameters('location')]", + "properties": { + "siteConfig": { + "appSettings": [ + { + "name": "DOCKER_REGISTRY_SERVER_URL", + "value": "https://index.docker.io" + }, + { + "name": "DOCKER_REGISTRY_SERVER_USERNAME", + "value": "" + }, + { + "name": "DOCKER_REGISTRY_SERVER_PASSWORD", + "value": "" + }, + { + "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", + "value": "false" + } + ], + "linuxFxVersion": "[format('DOCKER|{0}:{1}', parameters('dockerImage'), parameters('dockerImageTag'))]" + }, + "serverFarmId": "[parameters('appPlanId')]" + } + } + }, + "outputs": { + "siteUrl": { + "type": "string", + "value": "[reference('namePrefix_site').hostNames[0]]" + } + } + } + }, + "dependsOn": [ + "appPlanDeploy", + "rg" + ] + }, "storageDeploy": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep index 1bfca75ea18..d6d89234270 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep @@ -62,6 +62,19 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { +//@[62:66) Local site. Type: object | object. Declaration start char: 62, length: 4 +//@[07:18) Module siteDeploy3. Type: module[]. Declaration start char: 0, length: 281 + name: '${site.name}siteDeploy3' + scope: rg + params: { + appPlanId: appPlanDeploy.outputs.planId + namePrefix: site.name + dockerImage: 'nginxdemos/hello' + dockerImageTag: site.tag + } +}] + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[07:20) Module storageDeploy. Type: module. Declaration start char: 0, length: 168 name: 'storageDeploy' @@ -152,3 +165,4 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { ipv6port: 'test' } } + diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep index 1f5a364ea01..136c3e2f4d5 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep @@ -1,5 +1,5 @@ targetScope = 'subscription' -//@[000:2463) ProgramSyntax +//@[000:2747) ProgramSyntax //@[000:0028) ├─TargetScopeSyntax //@[000:0011) | ├─Token(Identifier) |targetScope| //@[012:0013) | ├─Token(Assignment) |=| @@ -436,6 +436,120 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { //@[001:0002) | └─Token(RightSquare) |]| //@[002:0004) ├─Token(NewLine) |\n\n| +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { +//@[000:0281) ├─ModuleDeclarationSyntax +//@[000:0006) | ├─Token(Identifier) |module| +//@[007:0018) | ├─IdentifierSyntax +//@[007:0018) | | └─Token(Identifier) |siteDeploy3| +//@[019:0054) | ├─StringSyntax +//@[019:0054) | | └─Token(StringComplete) |'br/mock-registry-emulated:site:v3'| +//@[055:0056) | ├─Token(Assignment) |=| +//@[057:0281) | └─ForSyntax +//@[057:0058) | ├─Token(LeftSquare) |[| +//@[058:0061) | ├─Token(Identifier) |for| +//@[062:0066) | ├─LocalVariableSyntax +//@[062:0066) | | └─IdentifierSyntax +//@[062:0066) | | └─Token(Identifier) |site| +//@[067:0069) | ├─Token(Identifier) |in| +//@[070:0078) | ├─VariableAccessSyntax +//@[070:0078) | | └─IdentifierSyntax +//@[070:0078) | | └─Token(Identifier) |websites| +//@[078:0079) | ├─Token(Colon) |:| +//@[080:0280) | ├─ObjectSyntax +//@[080:0081) | | ├─Token(LeftBrace) |{| +//@[081:0082) | | ├─Token(NewLine) |\n| + name: '${site.name}siteDeploy3' +//@[002:0033) | | ├─ObjectPropertySyntax +//@[002:0006) | | | ├─IdentifierSyntax +//@[002:0006) | | | | └─Token(Identifier) |name| +//@[006:0007) | | | ├─Token(Colon) |:| +//@[008:0033) | | | └─StringSyntax +//@[008:0011) | | | ├─Token(StringLeftPiece) |'${| +//@[011:0020) | | | ├─PropertyAccessSyntax +//@[011:0015) | | | | ├─VariableAccessSyntax +//@[011:0015) | | | | | └─IdentifierSyntax +//@[011:0015) | | | | | └─Token(Identifier) |site| +//@[015:0016) | | | | ├─Token(Dot) |.| +//@[016:0020) | | | | └─IdentifierSyntax +//@[016:0020) | | | | └─Token(Identifier) |name| +//@[020:0033) | | | └─Token(StringRightPiece) |}siteDeploy3'| +//@[033:0034) | | ├─Token(NewLine) |\n| + scope: rg +//@[002:0011) | | ├─ObjectPropertySyntax +//@[002:0007) | | | ├─IdentifierSyntax +//@[002:0007) | | | | └─Token(Identifier) |scope| +//@[007:0008) | | | ├─Token(Colon) |:| +//@[009:0011) | | | └─VariableAccessSyntax +//@[009:0011) | | | └─IdentifierSyntax +//@[009:0011) | | | └─Token(Identifier) |rg| +//@[011:0012) | | ├─Token(NewLine) |\n| + params: { +//@[002:0150) | | ├─ObjectPropertySyntax +//@[002:0008) | | | ├─IdentifierSyntax +//@[002:0008) | | | | └─Token(Identifier) |params| +//@[008:0009) | | | ├─Token(Colon) |:| +//@[010:0150) | | | └─ObjectSyntax +//@[010:0011) | | | ├─Token(LeftBrace) |{| +//@[011:0012) | | | ├─Token(NewLine) |\n| + appPlanId: appPlanDeploy.outputs.planId +//@[004:0043) | | | ├─ObjectPropertySyntax +//@[004:0013) | | | | ├─IdentifierSyntax +//@[004:0013) | | | | | └─Token(Identifier) |appPlanId| +//@[013:0014) | | | | ├─Token(Colon) |:| +//@[015:0043) | | | | └─PropertyAccessSyntax +//@[015:0036) | | | | ├─PropertyAccessSyntax +//@[015:0028) | | | | | ├─VariableAccessSyntax +//@[015:0028) | | | | | | └─IdentifierSyntax +//@[015:0028) | | | | | | └─Token(Identifier) |appPlanDeploy| +//@[028:0029) | | | | | ├─Token(Dot) |.| +//@[029:0036) | | | | | └─IdentifierSyntax +//@[029:0036) | | | | | └─Token(Identifier) |outputs| +//@[036:0037) | | | | ├─Token(Dot) |.| +//@[037:0043) | | | | └─IdentifierSyntax +//@[037:0043) | | | | └─Token(Identifier) |planId| +//@[043:0044) | | | ├─Token(NewLine) |\n| + namePrefix: site.name +//@[004:0025) | | | ├─ObjectPropertySyntax +//@[004:0014) | | | | ├─IdentifierSyntax +//@[004:0014) | | | | | └─Token(Identifier) |namePrefix| +//@[014:0015) | | | | ├─Token(Colon) |:| +//@[016:0025) | | | | └─PropertyAccessSyntax +//@[016:0020) | | | | ├─VariableAccessSyntax +//@[016:0020) | | | | | └─IdentifierSyntax +//@[016:0020) | | | | | └─Token(Identifier) |site| +//@[020:0021) | | | | ├─Token(Dot) |.| +//@[021:0025) | | | | └─IdentifierSyntax +//@[021:0025) | | | | └─Token(Identifier) |name| +//@[025:0026) | | | ├─Token(NewLine) |\n| + dockerImage: 'nginxdemos/hello' +//@[004:0035) | | | ├─ObjectPropertySyntax +//@[004:0015) | | | | ├─IdentifierSyntax +//@[004:0015) | | | | | └─Token(Identifier) |dockerImage| +//@[015:0016) | | | | ├─Token(Colon) |:| +//@[017:0035) | | | | └─StringSyntax +//@[017:0035) | | | | └─Token(StringComplete) |'nginxdemos/hello'| +//@[035:0036) | | | ├─Token(NewLine) |\n| + dockerImageTag: site.tag +//@[004:0028) | | | ├─ObjectPropertySyntax +//@[004:0018) | | | | ├─IdentifierSyntax +//@[004:0018) | | | | | └─Token(Identifier) |dockerImageTag| +//@[018:0019) | | | | ├─Token(Colon) |:| +//@[020:0028) | | | | └─PropertyAccessSyntax +//@[020:0024) | | | | ├─VariableAccessSyntax +//@[020:0024) | | | | | └─IdentifierSyntax +//@[020:0024) | | | | | └─Token(Identifier) |site| +//@[024:0025) | | | | ├─Token(Dot) |.| +//@[025:0028) | | | | └─IdentifierSyntax +//@[025:0028) | | | | └─Token(Identifier) |tag| +//@[028:0029) | | | ├─Token(NewLine) |\n| + } +//@[002:0003) | | | └─Token(RightBrace) |}| +//@[003:0004) | | ├─Token(NewLine) |\n| +}] +//@[000:0001) | | └─Token(RightBrace) |}| +//@[001:0002) | └─Token(RightSquare) |]| +//@[002:0004) ├─Token(NewLine) |\n\n| + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[000:0168) ├─ModuleDeclarationSyntax //@[000:0006) | ├─Token(Identifier) |module| @@ -988,4 +1102,6 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { //@[003:0004) | ├─Token(NewLine) |\n| } //@[000:0001) | └─Token(RightBrace) |}| -//@[001:0001) └─Token(EndOfFile) || +//@[001:0002) ├─Token(NewLine) |\n| + +//@[000:0000) └─Token(EndOfFile) || diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep index 43d953768e3..ebd99f647de 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep @@ -275,6 +275,74 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { //@[001:002) RightSquare |]| //@[002:004) NewLine |\n\n| +module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { +//@[000:006) Identifier |module| +//@[007:018) Identifier |siteDeploy3| +//@[019:054) StringComplete |'br/mock-registry-emulated:site:v3'| +//@[055:056) Assignment |=| +//@[057:058) LeftSquare |[| +//@[058:061) Identifier |for| +//@[062:066) Identifier |site| +//@[067:069) Identifier |in| +//@[070:078) Identifier |websites| +//@[078:079) Colon |:| +//@[080:081) LeftBrace |{| +//@[081:082) NewLine |\n| + name: '${site.name}siteDeploy3' +//@[002:006) Identifier |name| +//@[006:007) Colon |:| +//@[008:011) StringLeftPiece |'${| +//@[011:015) Identifier |site| +//@[015:016) Dot |.| +//@[016:020) Identifier |name| +//@[020:033) StringRightPiece |}siteDeploy3'| +//@[033:034) NewLine |\n| + scope: rg +//@[002:007) Identifier |scope| +//@[007:008) Colon |:| +//@[009:011) Identifier |rg| +//@[011:012) NewLine |\n| + params: { +//@[002:008) Identifier |params| +//@[008:009) Colon |:| +//@[010:011) LeftBrace |{| +//@[011:012) NewLine |\n| + appPlanId: appPlanDeploy.outputs.planId +//@[004:013) Identifier |appPlanId| +//@[013:014) Colon |:| +//@[015:028) Identifier |appPlanDeploy| +//@[028:029) Dot |.| +//@[029:036) Identifier |outputs| +//@[036:037) Dot |.| +//@[037:043) Identifier |planId| +//@[043:044) NewLine |\n| + namePrefix: site.name +//@[004:014) Identifier |namePrefix| +//@[014:015) Colon |:| +//@[016:020) Identifier |site| +//@[020:021) Dot |.| +//@[021:025) Identifier |name| +//@[025:026) NewLine |\n| + dockerImage: 'nginxdemos/hello' +//@[004:015) Identifier |dockerImage| +//@[015:016) Colon |:| +//@[017:035) StringComplete |'nginxdemos/hello'| +//@[035:036) NewLine |\n| + dockerImageTag: site.tag +//@[004:018) Identifier |dockerImageTag| +//@[018:019) Colon |:| +//@[020:024) Identifier |site| +//@[024:025) Dot |.| +//@[025:028) Identifier |tag| +//@[028:029) NewLine |\n| + } +//@[002:003) RightBrace |}| +//@[003:004) NewLine |\n| +}] +//@[000:001) RightBrace |}| +//@[001:002) RightSquare |]| +//@[002:004) NewLine |\n\n| + module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[000:006) Identifier |module| //@[007:020) Identifier |storageDeploy| @@ -633,4 +701,6 @@ module ipv6port 'br:[::1]:5000/passthrough/ipv6port:v1' = { //@[003:004) NewLine |\n| } //@[000:001) RightBrace |}| -//@[001:001) EndOfFile || +//@[001:002) NewLine |\n| + +//@[000:000) EndOfFile || From 9838910a186dcc3f7303ec44b3682593275e845d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sun, 19 Apr 2026 22:15:58 +0200 Subject: [PATCH 03/10] Update integration tests --- .../RegistryTests.cs | 4 +- .../Files/baselines/Registry_LF/main.bicep | 19 +- .../Registry_LF/main.diagnostics.bicep | 19 +- .../Registry_LF/main.formatted.bicep | 21 +-- .../Files/baselines/Registry_LF/main.ir.bicep | 64 ++----- .../Files/baselines/Registry_LF/main.json | 80 +++------ .../Registry_LF/main.sourcemap.bicep | 139 +++++---------- .../Registry_LF/main.symbolicnames.json | 88 +++------ .../baselines/Registry_LF/main.symbols.bicep | 22 +-- .../baselines/Registry_LF/main.syntax.bicep | 167 ++++++------------ .../baselines/Registry_LF/main.tokens.bicep | 102 ++++------- 11 files changed, 231 insertions(+), 494 deletions(-) diff --git a/src/Bicep.Core.IntegrationTests/RegistryTests.cs b/src/Bicep.Core.IntegrationTests/RegistryTests.cs index 466897dec6c..3bc5a7719a5 100644 --- a/src/Bicep.Core.IntegrationTests/RegistryTests.cs +++ b/src/Bicep.Core.IntegrationTests/RegistryTests.cs @@ -64,9 +64,9 @@ public async Task InvalidRootCachePathShouldProduceReasonableErrors() var compilation = await compiler.CreateCompilation(fileUri.ToIOUri()); var diagnostics = compilation.GetAllDiagnosticsByBicepFile(); - diagnostics.Should().HaveCount(1); + diagnostics.Should().HaveCount(2); var expectedErrorMessage = "Unable to restore the artifact with reference \"{0}\": Unable to create the local artifact directory \""; - diagnostics.Single().Value.ExcludingLinterDiagnostics().Should().SatisfyRespectively( + diagnostics[compilation.SourceFileGrouping.EntryPoint].ExcludingLinterDiagnostics().Should().SatisfyRespectively( x => { x.Level.Should().Be(DiagnosticLevel.Error); diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep index 8a35621edd3..db4d3503e0f 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.bicep @@ -21,6 +21,14 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ { name: 'fancy' @@ -54,17 +62,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { - name: '${site.name}siteDeploy3' - scope: rg - params: { - appPlanId: appPlanDeploy.outputs.planId - namePrefix: site.name - dockerImage: 'nginxdemos/hello' - dockerImageTag: site.tag - } -}] - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { name: 'storageDeploy' scope: rg diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep index 13575506321..7d4f17d9542 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.diagnostics.bicep @@ -21,6 +21,14 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ { name: 'fancy' @@ -54,17 +62,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { - name: '${site.name}siteDeploy3' - scope: rg - params: { - appPlanId: appPlanDeploy.outputs.planId - namePrefix: site.name - dockerImage: 'nginxdemos/hello' - dockerImageTag: site.tag - } -}] - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { name: 'storageDeploy' scope: rg diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep index 95d8588d6c1..c7a28bb80ad 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.formatted.bicep @@ -21,6 +21,14 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ { name: 'fancy' @@ -58,19 +66,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [ } ] -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [ - for site in websites: { - name: '${site.name}siteDeploy3' - scope: rg - params: { - appPlanId: appPlanDeploy.outputs.planId - namePrefix: site.name - dockerImage: 'nginxdemos/hello' - dockerImageTag: site.tag - } - } -] - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { name: 'storageDeploy' scope: rg diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep index 0e0568069e1..fcd302c7f41 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.ir.bicep @@ -1,11 +1,9 @@ targetScope = 'subscription' -//@[000:2747) ProgramExpression +//@[000:2603) ProgramExpression //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] -//@[000:0000) | ├─ResourceDependencyExpression [UNPARENTED] -//@[000:0000) | | └─ModuleReferenceExpression [UNPARENTED] //@[000:0000) | └─ResourceDependencyExpression [UNPARENTED] //@[000:0000) | └─ResourceReferenceExpression [UNPARENTED] //@[000:0000) | ├─ResourceDependencyExpression [UNPARENTED] @@ -78,6 +76,23 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@[000:0137) ├─DeclaredModuleExpression +//@[060:0137) | ├─ObjectExpression + name: 'planDeploy3' +//@[002:0021) | | └─ObjectPropertyExpression +//@[002:0006) | | ├─StringLiteralExpression { Value = name } +//@[008:0021) | | └─StringLiteralExpression { Value = planDeploy3 } + scope: rg + params: { +//@[010:0039) | ├─ObjectExpression + namePrefix: 'hello' +//@[004:0023) | | └─ObjectPropertyExpression +//@[004:0014) | | ├─StringLiteralExpression { Value = namePrefix } +//@[016:0023) | | └─StringLiteralExpression { Value = hello } + } +} + var websites = [ //@[000:0110) ├─DeclaredVariableExpression { Name = websites } //@[015:0110) | └─ArrayExpression @@ -191,49 +206,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { -//@[000:0281) ├─DeclaredModuleExpression -//@[057:0281) | ├─ForLoopExpression -//@[070:0078) | | ├─VariableReferenceExpression { Variable = websites } -//@[080:0280) | | └─ObjectExpression -//@[070:0078) | | └─VariableReferenceExpression { Variable = websites } -//@[070:0078) | | | └─VariableReferenceExpression { Variable = websites } -//@[070:0078) | | └─VariableReferenceExpression { Variable = websites } - name: '${site.name}siteDeploy3' -//@[002:0033) | | └─ObjectPropertyExpression -//@[002:0006) | | ├─StringLiteralExpression { Value = name } -//@[008:0033) | | └─InterpolatedStringExpression -//@[011:0020) | | └─PropertyAccessExpression { PropertyName = name } -//@[011:0015) | | └─ArrayAccessExpression -//@[011:0015) | | ├─CopyIndexExpression - scope: rg - params: { -//@[010:0150) | ├─ObjectExpression - appPlanId: appPlanDeploy.outputs.planId -//@[004:0043) | | ├─ObjectPropertyExpression -//@[004:0013) | | | ├─StringLiteralExpression { Value = appPlanId } -//@[015:0043) | | | └─ModuleOutputPropertyAccessExpression { PropertyName = planId } -//@[015:0036) | | | └─PropertyAccessExpression { PropertyName = outputs } -//@[015:0028) | | | └─ModuleReferenceExpression - namePrefix: site.name -//@[004:0025) | | ├─ObjectPropertyExpression -//@[004:0014) | | | ├─StringLiteralExpression { Value = namePrefix } -//@[016:0025) | | | └─PropertyAccessExpression { PropertyName = name } -//@[016:0020) | | | └─ArrayAccessExpression -//@[016:0020) | | | ├─CopyIndexExpression - dockerImage: 'nginxdemos/hello' -//@[004:0035) | | ├─ObjectPropertyExpression -//@[004:0015) | | | ├─StringLiteralExpression { Value = dockerImage } -//@[017:0035) | | | └─StringLiteralExpression { Value = nginxdemos/hello } - dockerImageTag: site.tag -//@[004:0028) | | └─ObjectPropertyExpression -//@[004:0018) | | ├─StringLiteralExpression { Value = dockerImageTag } -//@[020:0028) | | └─PropertyAccessExpression { PropertyName = tag } -//@[020:0024) | | └─ArrayAccessExpression -//@[020:0024) | | ├─CopyIndexExpression - } -}] - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[000:0168) ├─DeclaredModuleExpression //@[090:0168) | ├─ObjectExpression diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json index 1c474d2bc7e..710dcd5ed2b 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "16849026672229767237" + "templateHash": "11340138247578714789" } }, "variables": { @@ -160,13 +160,9 @@ ] }, { - "copy": { - "name": "siteDeploy", - "count": "[length(variables('websites'))]" - }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}siteDeploy', variables('websites')[copyIndex()].name)]", + "name": "planDeploy3", "resourceGroup": "adotfrank-rg", "properties": { "expressionEvaluationOptions": { @@ -174,17 +170,8 @@ }, "mode": "Incremental", "parameters": { - "appPlanId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy'), '2025-04-01').outputs.planId.value]" - }, "namePrefix": { - "value": "[variables('websites')[copyIndex()].name]" - }, - "dockerImage": { - "value": "nginxdemos/hello" - }, - "dockerImageTag": { - "value": "[variables('websites')[copyIndex()].tag]" + "value": "hello" } }, "template": { @@ -194,80 +181,53 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "15188988612540889945" + "templateHash": "15019246960605065046" } }, "parameters": { "namePrefix": { "type": "string" }, - "location": { + "sku": { "type": "string", - "defaultValue": "[resourceGroup().location]" - }, - "dockerImage": { - "type": "string" - }, - "dockerImageTag": { - "type": "string" - }, - "appPlanId": { - "type": "string" + "defaultValue": "B1" } }, "resources": [ { - "type": "Microsoft.Web/sites", + "type": "Microsoft.Web/serverfarms", "apiVersion": "2020-06-01", - "name": "[format('{0}site', parameters('namePrefix'))]", - "location": "[parameters('location')]", + "name": "[format('{0}appPlan', parameters('namePrefix'))]", + "location": "[resourceGroup().location]", + "kind": "linux", + "sku": { + "name": "[parameters('sku')]" + }, "properties": { - "siteConfig": { - "appSettings": [ - { - "name": "DOCKER_REGISTRY_SERVER_URL", - "value": "https://index.docker.io" - }, - { - "name": "DOCKER_REGISTRY_SERVER_USERNAME", - "value": "" - }, - { - "name": "DOCKER_REGISTRY_SERVER_PASSWORD", - "value": "" - }, - { - "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", - "value": "false" - } - ], - "linuxFxVersion": "[format('DOCKER|{0}:{1}', parameters('dockerImage'), parameters('dockerImageTag'))]" - }, - "serverFarmId": "[parameters('appPlanId')]" + "reserved": true } } ], "outputs": { - "siteUrl": { + "planId": { "type": "string", - "value": "[reference(resourceId('Microsoft.Web/sites', format('{0}site', parameters('namePrefix'))), '2020-06-01').hostNames[0]]" + "value": "[resourceId('Microsoft.Web/serverfarms', format('{0}appPlan', parameters('namePrefix')))]" } } } }, "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" ] }, { "copy": { - "name": "siteDeploy2", + "name": "siteDeploy", "count": "[length(variables('websites'))]" }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}siteDeploy2', variables('websites')[copyIndex()].name)]", + "name": "[format('{0}siteDeploy', variables('websites')[copyIndex()].name)]", "resourceGroup": "adotfrank-rg", "properties": { "expressionEvaluationOptions": { @@ -363,12 +323,12 @@ }, { "copy": { - "name": "siteDeploy3", + "name": "siteDeploy2", "count": "[length(variables('websites'))]" }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}siteDeploy3', variables('websites')[copyIndex()].name)]", + "name": "[format('{0}siteDeploy2', variables('websites')[copyIndex()].name)]", "resourceGroup": "adotfrank-rg", "properties": { "expressionEvaluationOptions": { diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep index b9d0030cc38..529e8a6ea67 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.sourcemap.bicep @@ -149,33 +149,8 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } -var websites = [ -//@ "websites": [ -//@ ], - { -//@ { -//@ }, - name: 'fancy' -//@ "name": "fancy", - tag: 'latest' -//@ "tag": "latest" - } - { -//@ { -//@ } - name: 'plain' -//@ "name": "plain", - tag: 'plain-text' -//@ "tag": "plain-text" - } -] - -module siteDeploy 'br:mock-registry-two.invalid/demo/site:v3' = [for site in websites: { +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { //@ { -//@ "copy": { -//@ "name": "siteDeploy", -//@ "count": "[length(variables('websites'))]" -//@ }, //@ "type": "Microsoft.Resources/deployments", //@ "apiVersion": "2025-04-01", //@ "resourceGroup": "adotfrank-rg", @@ -191,101 +166,83 @@ module siteDeploy 'br:mock-registry-two.invalid/demo/site:v3' = [for site in web //@ "_generator": { //@ "name": "bicep", //@ "version": "dev", -//@ "templateHash": "15188988612540889945" +//@ "templateHash": "15019246960605065046" //@ } //@ }, //@ "parameters": { //@ "namePrefix": { //@ "type": "string" //@ }, -//@ "location": { +//@ "sku": { //@ "type": "string", -//@ "defaultValue": "[resourceGroup().location]" -//@ }, -//@ "dockerImage": { -//@ "type": "string" -//@ }, -//@ "dockerImageTag": { -//@ "type": "string" -//@ }, -//@ "appPlanId": { -//@ "type": "string" +//@ "defaultValue": "B1" //@ } //@ }, //@ "resources": [ //@ { -//@ "type": "Microsoft.Web/sites", +//@ "type": "Microsoft.Web/serverfarms", //@ "apiVersion": "2020-06-01", -//@ "name": "[format('{0}site', parameters('namePrefix'))]", -//@ "location": "[parameters('location')]", +//@ "name": "[format('{0}appPlan', parameters('namePrefix'))]", +//@ "location": "[resourceGroup().location]", +//@ "kind": "linux", +//@ "sku": { +//@ "name": "[parameters('sku')]" +//@ }, //@ "properties": { -//@ "siteConfig": { -//@ "appSettings": [ -//@ { -//@ "name": "DOCKER_REGISTRY_SERVER_URL", -//@ "value": "https://index.docker.io" -//@ }, -//@ { -//@ "name": "DOCKER_REGISTRY_SERVER_USERNAME", -//@ "value": "" -//@ }, -//@ { -//@ "name": "DOCKER_REGISTRY_SERVER_PASSWORD", -//@ "value": "" -//@ }, -//@ { -//@ "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", -//@ "value": "false" -//@ } -//@ ], -//@ "linuxFxVersion": "[format('DOCKER|{0}:{1}', parameters('dockerImage'), parameters('dockerImageTag'))]" -//@ }, -//@ "serverFarmId": "[parameters('appPlanId')]" +//@ "reserved": true //@ } //@ } //@ ], //@ "outputs": { -//@ "siteUrl": { +//@ "planId": { //@ "type": "string", -//@ "value": "[reference(resourceId('Microsoft.Web/sites', format('{0}site', parameters('namePrefix'))), '2020-06-01').hostNames[0]]" +//@ "value": "[resourceId('Microsoft.Web/serverfarms', format('{0}appPlan', parameters('namePrefix')))]" //@ } //@ } //@ } //@ }, //@ "dependsOn": [ -//@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy')]", //@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" //@ ] //@ }, - name: '${site.name}siteDeploy' -//@ "name": "[format('{0}siteDeploy', variables('websites')[copyIndex()].name)]", + name: 'planDeploy3' +//@ "name": "planDeploy3", scope: rg params: { //@ "parameters": { //@ }, - appPlanId: appPlanDeploy.outputs.planId -//@ "appPlanId": { -//@ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'adotfrank-rg'), 'Microsoft.Resources/deployments', 'planDeploy'), '2025-04-01').outputs.planId.value]" -//@ }, - namePrefix: site.name + namePrefix: 'hello' //@ "namePrefix": { -//@ "value": "[variables('websites')[copyIndex()].name]" -//@ }, - dockerImage: 'nginxdemos/hello' -//@ "dockerImage": { -//@ "value": "nginxdemos/hello" -//@ }, - dockerImageTag: site.tag -//@ "dockerImageTag": { -//@ "value": "[variables('websites')[copyIndex()].tag]" +//@ "value": "hello" //@ } } -}] +} -module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { +var websites = [ +//@ "websites": [ +//@ ], + { +//@ { +//@ }, + name: 'fancy' +//@ "name": "fancy", + tag: 'latest' +//@ "tag": "latest" + } + { +//@ { +//@ } + name: 'plain' +//@ "name": "plain", + tag: 'plain-text' +//@ "tag": "plain-text" + } +] + +module siteDeploy 'br:mock-registry-two.invalid/demo/site:v3' = [for site in websites: { //@ { //@ "copy": { -//@ "name": "siteDeploy2", +//@ "name": "siteDeploy", //@ "count": "[length(variables('websites'))]" //@ }, //@ "type": "Microsoft.Resources/deployments", @@ -369,8 +326,8 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { //@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" //@ ] //@ }, - name: '${site.name}siteDeploy2' -//@ "name": "[format('{0}siteDeploy2', variables('websites')[copyIndex()].name)]", + name: '${site.name}siteDeploy' +//@ "name": "[format('{0}siteDeploy', variables('websites')[copyIndex()].name)]", scope: rg params: { //@ "parameters": { @@ -394,10 +351,10 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { +module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { //@ { //@ "copy": { -//@ "name": "siteDeploy3", +//@ "name": "siteDeploy2", //@ "count": "[length(variables('websites'))]" //@ }, //@ "type": "Microsoft.Resources/deployments", @@ -481,8 +438,8 @@ module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: //@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', 'adotfrank-rg')]" //@ ] //@ }, - name: '${site.name}siteDeploy3' -//@ "name": "[format('{0}siteDeploy3', variables('websites')[copyIndex()].name)]", + name: '${site.name}siteDeploy2' +//@ "name": "[format('{0}siteDeploy2', variables('websites')[copyIndex()].name)]", scope: rg params: { //@ "parameters": { diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json index e9b482322da..57e16934904 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbolicnames.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "9031371693506242739" + "templateHash": "17302964314594708830" } }, "variables": { @@ -162,14 +162,10 @@ "rg" ] }, - "siteDeploy": { - "copy": { - "name": "siteDeploy", - "count": "[length(variables('websites'))]" - }, + "appPlanDeploy3": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}siteDeploy', variables('websites')[copyIndex()].name)]", + "name": "planDeploy3", "resourceGroup": "adotfrank-rg", "properties": { "expressionEvaluationOptions": { @@ -177,17 +173,8 @@ }, "mode": "Incremental", "parameters": { - "appPlanId": { - "value": "[reference('appPlanDeploy').outputs.planId.value]" - }, "namePrefix": { - "value": "[variables('websites')[copyIndex()].name]" - }, - "dockerImage": { - "value": "nginxdemos/hello" - }, - "dockerImageTag": { - "value": "[variables('websites')[copyIndex()].tag]" + "value": "hello" } }, "template": { @@ -198,80 +185,53 @@ "_generator": { "name": "bicep", "version": "dev", - "templateHash": "1727609956407115618" + "templateHash": "13508561622047952911" } }, "parameters": { "namePrefix": { "type": "string" }, - "location": { + "sku": { "type": "string", - "defaultValue": "[resourceGroup().location]" - }, - "dockerImage": { - "type": "string" - }, - "dockerImageTag": { - "type": "string" - }, - "appPlanId": { - "type": "string" + "defaultValue": "B1" } }, "resources": { - "namePrefix_site": { - "type": "Microsoft.Web/sites", + "appPlan": { + "type": "Microsoft.Web/serverfarms", "apiVersion": "2020-06-01", - "name": "[format('{0}site', parameters('namePrefix'))]", - "location": "[parameters('location')]", + "name": "[format('{0}appPlan', parameters('namePrefix'))]", + "location": "[resourceGroup().location]", + "kind": "linux", + "sku": { + "name": "[parameters('sku')]" + }, "properties": { - "siteConfig": { - "appSettings": [ - { - "name": "DOCKER_REGISTRY_SERVER_URL", - "value": "https://index.docker.io" - }, - { - "name": "DOCKER_REGISTRY_SERVER_USERNAME", - "value": "" - }, - { - "name": "DOCKER_REGISTRY_SERVER_PASSWORD", - "value": "" - }, - { - "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", - "value": "false" - } - ], - "linuxFxVersion": "[format('DOCKER|{0}:{1}', parameters('dockerImage'), parameters('dockerImageTag'))]" - }, - "serverFarmId": "[parameters('appPlanId')]" + "reserved": true } } }, "outputs": { - "siteUrl": { + "planId": { "type": "string", - "value": "[reference('namePrefix_site').hostNames[0]]" + "value": "[resourceId('Microsoft.Web/serverfarms', format('{0}appPlan', parameters('namePrefix')))]" } } } }, "dependsOn": [ - "appPlanDeploy", "rg" ] }, - "siteDeploy2": { + "siteDeploy": { "copy": { - "name": "siteDeploy2", + "name": "siteDeploy", "count": "[length(variables('websites'))]" }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}siteDeploy2', variables('websites')[copyIndex()].name)]", + "name": "[format('{0}siteDeploy', variables('websites')[copyIndex()].name)]", "resourceGroup": "adotfrank-rg", "properties": { "expressionEvaluationOptions": { @@ -366,14 +326,14 @@ "rg" ] }, - "siteDeploy3": { + "siteDeploy2": { "copy": { - "name": "siteDeploy3", + "name": "siteDeploy2", "count": "[length(variables('websites'))]" }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('{0}siteDeploy3', variables('websites')[copyIndex()].name)]", + "name": "[format('{0}siteDeploy2', variables('websites')[copyIndex()].name)]", "resourceGroup": "adotfrank-rg", "properties": { "expressionEvaluationOptions": { diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep index d6d89234270..fea26bf29d8 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.symbols.bicep @@ -24,6 +24,15 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { } } +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@[07:21) Module appPlanDeploy3. Type: module. Declaration start char: 0, length: 137 + name: 'planDeploy3' + scope: rg + params: { + namePrefix: 'hello' + } +} + var websites = [ //@[04:12) Variable websites. Type: [object, object]. Declaration start char: 0, length: 110 { @@ -62,19 +71,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { } }] -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { -//@[62:66) Local site. Type: object | object. Declaration start char: 62, length: 4 -//@[07:18) Module siteDeploy3. Type: module[]. Declaration start char: 0, length: 281 - name: '${site.name}siteDeploy3' - scope: rg - params: { - appPlanId: appPlanDeploy.outputs.planId - namePrefix: site.name - dockerImage: 'nginxdemos/hello' - dockerImageTag: site.tag - } -}] - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[07:20) Module storageDeploy. Type: module. Declaration start char: 0, length: 168 name: 'storageDeploy' diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep index 136c3e2f4d5..55f64164bc5 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.syntax.bicep @@ -1,5 +1,5 @@ targetScope = 'subscription' -//@[000:2747) ProgramSyntax +//@[000:2603) ProgramSyntax //@[000:0028) ├─TargetScopeSyntax //@[000:0011) | ├─Token(Identifier) |targetScope| //@[012:0013) | ├─Token(Assignment) |=| @@ -147,6 +147,57 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { //@[000:0001) | └─Token(RightBrace) |}| //@[001:0003) ├─Token(NewLine) |\n\n| +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@[000:0137) ├─ModuleDeclarationSyntax +//@[000:0006) | ├─Token(Identifier) |module| +//@[007:0021) | ├─IdentifierSyntax +//@[007:0021) | | └─Token(Identifier) |appPlanDeploy3| +//@[022:0057) | ├─StringSyntax +//@[022:0057) | | └─Token(StringComplete) |'br/mock-registry-emulated:plan:v2'| +//@[058:0059) | ├─Token(Assignment) |=| +//@[060:0137) | └─ObjectSyntax +//@[060:0061) | ├─Token(LeftBrace) |{| +//@[061:0062) | ├─Token(NewLine) |\n| + name: 'planDeploy3' +//@[002:0021) | ├─ObjectPropertySyntax +//@[002:0006) | | ├─IdentifierSyntax +//@[002:0006) | | | └─Token(Identifier) |name| +//@[006:0007) | | ├─Token(Colon) |:| +//@[008:0021) | | └─StringSyntax +//@[008:0021) | | └─Token(StringComplete) |'planDeploy3'| +//@[021:0022) | ├─Token(NewLine) |\n| + scope: rg +//@[002:0011) | ├─ObjectPropertySyntax +//@[002:0007) | | ├─IdentifierSyntax +//@[002:0007) | | | └─Token(Identifier) |scope| +//@[007:0008) | | ├─Token(Colon) |:| +//@[009:0011) | | └─VariableAccessSyntax +//@[009:0011) | | └─IdentifierSyntax +//@[009:0011) | | └─Token(Identifier) |rg| +//@[011:0012) | ├─Token(NewLine) |\n| + params: { +//@[002:0039) | ├─ObjectPropertySyntax +//@[002:0008) | | ├─IdentifierSyntax +//@[002:0008) | | | └─Token(Identifier) |params| +//@[008:0009) | | ├─Token(Colon) |:| +//@[010:0039) | | └─ObjectSyntax +//@[010:0011) | | ├─Token(LeftBrace) |{| +//@[011:0012) | | ├─Token(NewLine) |\n| + namePrefix: 'hello' +//@[004:0023) | | ├─ObjectPropertySyntax +//@[004:0014) | | | ├─IdentifierSyntax +//@[004:0014) | | | | └─Token(Identifier) |namePrefix| +//@[014:0015) | | | ├─Token(Colon) |:| +//@[016:0023) | | | └─StringSyntax +//@[016:0023) | | | └─Token(StringComplete) |'hello'| +//@[023:0024) | | ├─Token(NewLine) |\n| + } +//@[002:0003) | | └─Token(RightBrace) |}| +//@[003:0004) | ├─Token(NewLine) |\n| +} +//@[000:0001) | └─Token(RightBrace) |}| +//@[001:0003) ├─Token(NewLine) |\n\n| + var websites = [ //@[000:0110) ├─VariableDeclarationSyntax //@[000:0003) | ├─Token(Identifier) |var| @@ -436,120 +487,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { //@[001:0002) | └─Token(RightSquare) |]| //@[002:0004) ├─Token(NewLine) |\n\n| -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { -//@[000:0281) ├─ModuleDeclarationSyntax -//@[000:0006) | ├─Token(Identifier) |module| -//@[007:0018) | ├─IdentifierSyntax -//@[007:0018) | | └─Token(Identifier) |siteDeploy3| -//@[019:0054) | ├─StringSyntax -//@[019:0054) | | └─Token(StringComplete) |'br/mock-registry-emulated:site:v3'| -//@[055:0056) | ├─Token(Assignment) |=| -//@[057:0281) | └─ForSyntax -//@[057:0058) | ├─Token(LeftSquare) |[| -//@[058:0061) | ├─Token(Identifier) |for| -//@[062:0066) | ├─LocalVariableSyntax -//@[062:0066) | | └─IdentifierSyntax -//@[062:0066) | | └─Token(Identifier) |site| -//@[067:0069) | ├─Token(Identifier) |in| -//@[070:0078) | ├─VariableAccessSyntax -//@[070:0078) | | └─IdentifierSyntax -//@[070:0078) | | └─Token(Identifier) |websites| -//@[078:0079) | ├─Token(Colon) |:| -//@[080:0280) | ├─ObjectSyntax -//@[080:0081) | | ├─Token(LeftBrace) |{| -//@[081:0082) | | ├─Token(NewLine) |\n| - name: '${site.name}siteDeploy3' -//@[002:0033) | | ├─ObjectPropertySyntax -//@[002:0006) | | | ├─IdentifierSyntax -//@[002:0006) | | | | └─Token(Identifier) |name| -//@[006:0007) | | | ├─Token(Colon) |:| -//@[008:0033) | | | └─StringSyntax -//@[008:0011) | | | ├─Token(StringLeftPiece) |'${| -//@[011:0020) | | | ├─PropertyAccessSyntax -//@[011:0015) | | | | ├─VariableAccessSyntax -//@[011:0015) | | | | | └─IdentifierSyntax -//@[011:0015) | | | | | └─Token(Identifier) |site| -//@[015:0016) | | | | ├─Token(Dot) |.| -//@[016:0020) | | | | └─IdentifierSyntax -//@[016:0020) | | | | └─Token(Identifier) |name| -//@[020:0033) | | | └─Token(StringRightPiece) |}siteDeploy3'| -//@[033:0034) | | ├─Token(NewLine) |\n| - scope: rg -//@[002:0011) | | ├─ObjectPropertySyntax -//@[002:0007) | | | ├─IdentifierSyntax -//@[002:0007) | | | | └─Token(Identifier) |scope| -//@[007:0008) | | | ├─Token(Colon) |:| -//@[009:0011) | | | └─VariableAccessSyntax -//@[009:0011) | | | └─IdentifierSyntax -//@[009:0011) | | | └─Token(Identifier) |rg| -//@[011:0012) | | ├─Token(NewLine) |\n| - params: { -//@[002:0150) | | ├─ObjectPropertySyntax -//@[002:0008) | | | ├─IdentifierSyntax -//@[002:0008) | | | | └─Token(Identifier) |params| -//@[008:0009) | | | ├─Token(Colon) |:| -//@[010:0150) | | | └─ObjectSyntax -//@[010:0011) | | | ├─Token(LeftBrace) |{| -//@[011:0012) | | | ├─Token(NewLine) |\n| - appPlanId: appPlanDeploy.outputs.planId -//@[004:0043) | | | ├─ObjectPropertySyntax -//@[004:0013) | | | | ├─IdentifierSyntax -//@[004:0013) | | | | | └─Token(Identifier) |appPlanId| -//@[013:0014) | | | | ├─Token(Colon) |:| -//@[015:0043) | | | | └─PropertyAccessSyntax -//@[015:0036) | | | | ├─PropertyAccessSyntax -//@[015:0028) | | | | | ├─VariableAccessSyntax -//@[015:0028) | | | | | | └─IdentifierSyntax -//@[015:0028) | | | | | | └─Token(Identifier) |appPlanDeploy| -//@[028:0029) | | | | | ├─Token(Dot) |.| -//@[029:0036) | | | | | └─IdentifierSyntax -//@[029:0036) | | | | | └─Token(Identifier) |outputs| -//@[036:0037) | | | | ├─Token(Dot) |.| -//@[037:0043) | | | | └─IdentifierSyntax -//@[037:0043) | | | | └─Token(Identifier) |planId| -//@[043:0044) | | | ├─Token(NewLine) |\n| - namePrefix: site.name -//@[004:0025) | | | ├─ObjectPropertySyntax -//@[004:0014) | | | | ├─IdentifierSyntax -//@[004:0014) | | | | | └─Token(Identifier) |namePrefix| -//@[014:0015) | | | | ├─Token(Colon) |:| -//@[016:0025) | | | | └─PropertyAccessSyntax -//@[016:0020) | | | | ├─VariableAccessSyntax -//@[016:0020) | | | | | └─IdentifierSyntax -//@[016:0020) | | | | | └─Token(Identifier) |site| -//@[020:0021) | | | | ├─Token(Dot) |.| -//@[021:0025) | | | | └─IdentifierSyntax -//@[021:0025) | | | | └─Token(Identifier) |name| -//@[025:0026) | | | ├─Token(NewLine) |\n| - dockerImage: 'nginxdemos/hello' -//@[004:0035) | | | ├─ObjectPropertySyntax -//@[004:0015) | | | | ├─IdentifierSyntax -//@[004:0015) | | | | | └─Token(Identifier) |dockerImage| -//@[015:0016) | | | | ├─Token(Colon) |:| -//@[017:0035) | | | | └─StringSyntax -//@[017:0035) | | | | └─Token(StringComplete) |'nginxdemos/hello'| -//@[035:0036) | | | ├─Token(NewLine) |\n| - dockerImageTag: site.tag -//@[004:0028) | | | ├─ObjectPropertySyntax -//@[004:0018) | | | | ├─IdentifierSyntax -//@[004:0018) | | | | | └─Token(Identifier) |dockerImageTag| -//@[018:0019) | | | | ├─Token(Colon) |:| -//@[020:0028) | | | | └─PropertyAccessSyntax -//@[020:0024) | | | | ├─VariableAccessSyntax -//@[020:0024) | | | | | └─IdentifierSyntax -//@[020:0024) | | | | | └─Token(Identifier) |site| -//@[024:0025) | | | | ├─Token(Dot) |.| -//@[025:0028) | | | | └─IdentifierSyntax -//@[025:0028) | | | | └─Token(Identifier) |tag| -//@[028:0029) | | | ├─Token(NewLine) |\n| - } -//@[002:0003) | | | └─Token(RightBrace) |}| -//@[003:0004) | | ├─Token(NewLine) |\n| -}] -//@[000:0001) | | └─Token(RightBrace) |}| -//@[001:0002) | └─Token(RightSquare) |]| -//@[002:0004) ├─Token(NewLine) |\n\n| - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[000:0168) ├─ModuleDeclarationSyntax //@[000:0006) | ├─Token(Identifier) |module| diff --git a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep index ebd99f647de..ff81a47d607 100644 --- a/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep +++ b/src/Bicep.Core.Samples/Files/baselines/Registry_LF/main.tokens.bicep @@ -97,6 +97,40 @@ module appPlanDeploy2 'br/mock-registry-one:demo/plan:v2' = { //@[000:001) RightBrace |}| //@[001:003) NewLine |\n\n| +module appPlanDeploy3 'br/mock-registry-emulated:plan:v2' = { +//@[000:006) Identifier |module| +//@[007:021) Identifier |appPlanDeploy3| +//@[022:057) StringComplete |'br/mock-registry-emulated:plan:v2'| +//@[058:059) Assignment |=| +//@[060:061) LeftBrace |{| +//@[061:062) NewLine |\n| + name: 'planDeploy3' +//@[002:006) Identifier |name| +//@[006:007) Colon |:| +//@[008:021) StringComplete |'planDeploy3'| +//@[021:022) NewLine |\n| + scope: rg +//@[002:007) Identifier |scope| +//@[007:008) Colon |:| +//@[009:011) Identifier |rg| +//@[011:012) NewLine |\n| + params: { +//@[002:008) Identifier |params| +//@[008:009) Colon |:| +//@[010:011) LeftBrace |{| +//@[011:012) NewLine |\n| + namePrefix: 'hello' +//@[004:014) Identifier |namePrefix| +//@[014:015) Colon |:| +//@[016:023) StringComplete |'hello'| +//@[023:024) NewLine |\n| + } +//@[002:003) RightBrace |}| +//@[003:004) NewLine |\n| +} +//@[000:001) RightBrace |}| +//@[001:003) NewLine |\n\n| + var websites = [ //@[000:003) Identifier |var| //@[004:012) Identifier |websites| @@ -275,74 +309,6 @@ module siteDeploy2 'br/demo-two:site:v3' = [for site in websites: { //@[001:002) RightSquare |]| //@[002:004) NewLine |\n\n| -module siteDeploy3 'br/mock-registry-emulated:site:v3' = [for site in websites: { -//@[000:006) Identifier |module| -//@[007:018) Identifier |siteDeploy3| -//@[019:054) StringComplete |'br/mock-registry-emulated:site:v3'| -//@[055:056) Assignment |=| -//@[057:058) LeftSquare |[| -//@[058:061) Identifier |for| -//@[062:066) Identifier |site| -//@[067:069) Identifier |in| -//@[070:078) Identifier |websites| -//@[078:079) Colon |:| -//@[080:081) LeftBrace |{| -//@[081:082) NewLine |\n| - name: '${site.name}siteDeploy3' -//@[002:006) Identifier |name| -//@[006:007) Colon |:| -//@[008:011) StringLeftPiece |'${| -//@[011:015) Identifier |site| -//@[015:016) Dot |.| -//@[016:020) Identifier |name| -//@[020:033) StringRightPiece |}siteDeploy3'| -//@[033:034) NewLine |\n| - scope: rg -//@[002:007) Identifier |scope| -//@[007:008) Colon |:| -//@[009:011) Identifier |rg| -//@[011:012) NewLine |\n| - params: { -//@[002:008) Identifier |params| -//@[008:009) Colon |:| -//@[010:011) LeftBrace |{| -//@[011:012) NewLine |\n| - appPlanId: appPlanDeploy.outputs.planId -//@[004:013) Identifier |appPlanId| -//@[013:014) Colon |:| -//@[015:028) Identifier |appPlanDeploy| -//@[028:029) Dot |.| -//@[029:036) Identifier |outputs| -//@[036:037) Dot |.| -//@[037:043) Identifier |planId| -//@[043:044) NewLine |\n| - namePrefix: site.name -//@[004:014) Identifier |namePrefix| -//@[014:015) Colon |:| -//@[016:020) Identifier |site| -//@[020:021) Dot |.| -//@[021:025) Identifier |name| -//@[025:026) NewLine |\n| - dockerImage: 'nginxdemos/hello' -//@[004:015) Identifier |dockerImage| -//@[015:016) Colon |:| -//@[017:035) StringComplete |'nginxdemos/hello'| -//@[035:036) NewLine |\n| - dockerImageTag: site.tag -//@[004:018) Identifier |dockerImageTag| -//@[018:019) Colon |:| -//@[020:024) Identifier |site| -//@[024:025) Dot |.| -//@[025:028) Identifier |tag| -//@[028:029) NewLine |\n| - } -//@[002:003) RightBrace |}| -//@[003:004) NewLine |\n| -}] -//@[000:001) RightBrace |}| -//@[001:002) RightSquare |]| -//@[002:004) NewLine |\n\n| - module storageDeploy 'ts:00000000-0000-0000-0000-000000000000/test-rg/storage-spec:1.0' = { //@[000:006) Identifier |module| //@[007:020) Identifier |storageDeploy| From ebdae41481b88ae3f53961f498f130c6b7ae02ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sun, 19 Apr 2026 22:35:00 +0200 Subject: [PATCH 04/10] Add check for OCI-compliant modulePath --- .../OciArtifactEmulatedReferenceTests.cs | 27 +++++++++++++++++++ .../Oci/OciArtifactEmulatedReference.cs | 13 ++++++++- .../Registry/OciArtifactRegistry.cs | 3 ++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs index 6f4f4a6e2fb..8a9ce33bd3f 100644 --- a/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs +++ b/src/Bicep.Core.UnitTests/Registry/OciArtifactEmulatedReferenceTests.cs @@ -238,5 +238,32 @@ public void TryGetOciArtifactModuleAlias_OnlyRegistrySet_ShouldSucceed() alias!.Registry.Should().Be("example.azurecr.io"); alias.FileSystem.Should().BeNull(); } + + [TestMethod] + [DataRow("../escape:1.0.0", "..")] + [DataRow("valid/../escape:1.0.0", "..")] + [DataRow(".:1.0.0", ".")] + [DataRow("UPPERCASE:1.0.0", "UPPERCASE")] + [DataRow("has spaces/module:1.0.0", "has spaces")] + [DataRow("valid/bad!segment:1.0.0", "bad!segment")] + public void TryParse_InvalidPathSegment_ShouldFail(string unqualifiedReference, string expectedBadSegment) + { + var fileExplorer = new FileSystemFileExplorer(new MockFileSystem()); + var referencingFile = BicepTestConstants.CreateDummyBicepFile(); + var configFileUri = new IOUri("file", "", "/repo/bicepconfig.json"); + + var result = OciArtifactEmulatedReference.TryParse( + referencingFile, + "./modules", + configFileUri, + unqualifiedReference, + fileExplorer, + "myAlias"); + + result.IsSuccess(out _, out var failureBuilder).Should().BeFalse(); + var diagnostic = failureBuilder!(DiagnosticBuilder.ForDocumentStart()); + diagnostic.Code.Should().Be("BCP195"); + diagnostic.Message.Should().Contain(expectedBadSegment); + } } } diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs index 3b95a813ad8..a3482100895 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -64,12 +64,14 @@ public static string ExtractModulePath(string unqualifiedReference) // configFileUri is the URI of the bicepconfig.json file, used to resolve relative paths // unqualifiedReference is the unqualified reference string (e.g., "keyvault:1.0.0") // fileExplorer is the file explorer used to create file handles + // aliasName is the name of the module alias, used in diagnostics public static ResultWithDiagnosticBuilder TryParse( BicepSourceFile referencingFile, string fileSystemPath, IOUri? configFileUri, string unqualifiedReference, - IFileExplorer fileExplorer) + IFileExplorer fileExplorer, + string? aliasName = null) { var modulePath = ExtractModulePath(unqualifiedReference); @@ -78,6 +80,15 @@ public static ResultWithDiagnosticBuilder TryParse return new(x => x.ModulePathHasNotBeenSpecified()); } + var segments = modulePath.Split('/'); + foreach (var segment in segments) + { + if (!OciArtifactReferenceFacts.IsOciNamespaceSegment(segment)) + { + return new(x => x.InvalidOciArtifactReferenceInvalidPathSegment(aliasName, unqualifiedReference, segment)); + } + } + // Resolve the filesystem base directory relative to bicepconfig.json IOUri baseUri; if (configFileUri is not null) diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index 8de8475b3a0..1c2ef5d8be4 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -72,7 +72,8 @@ public override ResultWithDiagnosticBuilder TryParseArtifactR alias.FileSystem, referencingFile.Configuration.ConfigFileUri, reference, - this.fileExplorer).IsSuccess(out var emulatedRef, out var emulatedFailureBuilder)) + this.fileExplorer, + aliasName).IsSuccess(out var emulatedRef, out var emulatedFailureBuilder)) { return new(emulatedFailureBuilder); } From 6eab145b0ff5f0d2ee6731c18bf884b93917cd0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sun, 19 Apr 2026 22:46:33 +0200 Subject: [PATCH 05/10] Rename FileSystemModuleRegistry to OciArtifactEmulatedRegistry and add diagnostics reserving br-fs scheme for internal use only --- src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs | 4 ++++ src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs | 2 +- ...SystemModuleRegistry.cs => OciArtifactEmulatedRegistry.cs} | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) rename src/Bicep.Core/Registry/{FileSystemModuleRegistry.cs => OciArtifactEmulatedRegistry.cs} (93%) diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index b64adee4be6..016ab2a7537 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -2034,6 +2034,10 @@ public Diagnostic InvalidOciArtifactModuleAliasRegistryAndFileSystemSetTogether( public Diagnostic OciArtifactModuleAliasFileSystemOnlySupportsModules(string aliasName) => CoreError( "BCP447", $"The OCI artifact module alias \"{aliasName}\" has a \"fileSystem\" property which is only supported for modules, not extensions."); + + public Diagnostic ModuleReferenceSchemeBrFsNotSupported() => CoreError( + "BCP448", + "The 'br-fs' module reference scheme is for internal use only. Use a 'br/:' reference with a configured 'fileSystem' alias instead."); } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs index 70d0f76d274..0fae64844cc 100644 --- a/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs +++ b/src/Bicep.Core/Registry/DefaultArtifactRegistryProvider.cs @@ -14,7 +14,7 @@ public DefaultArtifactRegistryProvider(IServiceProvider serviceProvider, IContai { new LocalModuleRegistry(), new OciArtifactRegistry(clientFactory, serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService()), - new FileSystemModuleRegistry(), + new OciArtifactEmulatedRegistry(), new TemplateSpecModuleRegistry(templateSpecRepositoryFactory), }) { diff --git a/src/Bicep.Core/Registry/FileSystemModuleRegistry.cs b/src/Bicep.Core/Registry/OciArtifactEmulatedRegistry.cs similarity index 93% rename from src/Bicep.Core/Registry/FileSystemModuleRegistry.cs rename to src/Bicep.Core/Registry/OciArtifactEmulatedRegistry.cs index b456468b745..8f0edf86763 100644 --- a/src/Bicep.Core/Registry/FileSystemModuleRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactEmulatedRegistry.cs @@ -10,7 +10,7 @@ namespace Bicep.Core.Registry { - public class FileSystemModuleRegistry : ArtifactRegistry + public class OciArtifactEmulatedRegistry : ArtifactRegistry { public override string Scheme => ArtifactReferenceSchemes.OciEmulated; @@ -18,7 +18,7 @@ public override RegistryCapabilities GetCapabilities(ArtifactType artifactType, => RegistryCapabilities.Default; public override ResultWithDiagnosticBuilder TryParseArtifactReference(BicepSourceFile referencingFile, ArtifactType artifactType, string? aliasName, string reference) - => throw new NotSupportedException("Parsing is handled by OciArtifactRegistry."); + => new(x => x.ModuleReferenceSchemeBrFsNotSupported()); public override bool IsArtifactRestoreRequired(OciArtifactEmulatedReference reference) => false; From 3b0268b3945ba0f79a1d499afdb925418399d3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Mon, 20 Apr 2026 20:59:21 +0200 Subject: [PATCH 06/10] Require bicepconfig for feature OciEmulatedModuleAlias and only accept alias paths relative to bicepconfig --- .../Diagnostics/DiagnosticBuilder.cs | 4 ++++ .../Oci/OciArtifactEmulatedReference.cs | 21 +++++++------------ .../Registry/OciArtifactRegistry.cs | 5 +++++ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index 016ab2a7537..1e074f44abd 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -2038,6 +2038,10 @@ public Diagnostic OciArtifactModuleAliasFileSystemOnlySupportsModules(string ali public Diagnostic ModuleReferenceSchemeBrFsNotSupported() => CoreError( "BCP448", "The 'br-fs' module reference scheme is for internal use only. Use a 'br/:' reference with a configured 'fileSystem' alias instead."); + + public Diagnostic ConfigurationFileNotFound(string featureName) => CoreError( + "BCP449", + $"Configuration file is not found. Feature \"{featureName}\" requires a configuration file."); } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs index a3482100895..5a08f166b70 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -68,7 +68,7 @@ public static string ExtractModulePath(string unqualifiedReference) public static ResultWithDiagnosticBuilder TryParse( BicepSourceFile referencingFile, string fileSystemPath, - IOUri? configFileUri, + IOUri configFileUri, string unqualifiedReference, IFileExplorer fileExplorer, string? aliasName = null) @@ -89,20 +89,13 @@ public static ResultWithDiagnosticBuilder TryParse } } - // Resolve the filesystem base directory relative to bicepconfig.json IOUri baseUri; - if (configFileUri is not null) - { - // Ensure the fileSystem path ends with '/' so it's treated as a directory - var directoryPath = fileSystemPath.EndsWith('/') || fileSystemPath.EndsWith('\\') - ? fileSystemPath - : fileSystemPath + "/"; - baseUri = configFileUri.Resolve(directoryPath); - } - else - { - baseUri = IOUri.FromFilePath(fileSystemPath); - } + // Resolve the filesystem base directory relative to bicepconfig.json + // Ensure the fileSystem path ends with '/' so it's treated as a directory + var directoryPath = fileSystemPath.EndsWith('/') || fileSystemPath.EndsWith('\\') + ? fileSystemPath + : fileSystemPath + "/"; + baseUri = configFileUri.Resolve(directoryPath); // Construct the file URI by appending the module path with a .bicep extension. var moduleFileName = modulePath + ".bicep"; diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index 1c2ef5d8be4..def160ce28e 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -67,6 +67,11 @@ public override ResultWithDiagnosticBuilder TryParseArtifactR return new(x => x.OciArtifactModuleAliasFileSystemOnlySupportsModules(aliasName)); } + if (referencingFile.Configuration.ConfigFileUri is null) + { + return new(x => x.ConfigurationFileNotFound("OciEmulatedModuleAliases")); + } + if (!OciArtifactEmulatedReference.TryParse( referencingFile, alias.FileSystem, From 7ec58bb03ff82a6a3dd4a03a384b99db18870235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Mon, 20 Apr 2026 21:17:07 +0200 Subject: [PATCH 07/10] Update bicepconfig schema with fileSystem property for ModuleAlias --- .../schemas/bicepconfig.schema.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/vscode-bicep/schemas/bicepconfig.schema.json b/src/vscode-bicep/schemas/bicepconfig.schema.json index 6c28ca07ba1..7b66acf5c8b 100644 --- a/src/vscode-bicep/schemas/bicepconfig.schema.json +++ b/src/vscode-bicep/schemas/bicepconfig.schema.json @@ -136,8 +136,17 @@ "bicepRegistryModuleAlias": { "type": "object", "additionalProperties": false, - "required": [ - "registry" + "oneOf": [ + { + "required": [ + "registry" + ] + }, + { + "required": [ + "fileSystem" + ] + } ], "properties": { "registry": { @@ -147,6 +156,12 @@ "minLength": 1, "maxLength": 255 }, + "fileSystem": { + "title": "File System", + "description": "The path relative to bicepconfig.json used to emulate a registry alias", + "type": "string", + "minLength": 1 + }, "modulePath": { "title": "Module Path", "description": "The module path of the alias", From 2a840e49fac5b0c2819a3aead3f33394b1690ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sun, 3 May 2026 20:57:10 +0200 Subject: [PATCH 08/10] Improve rewrite of original module alias --- .../Registry/Oci/OciArtifactEmulatedReference.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs index 5a08f166b70..ca96a816954 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -16,18 +16,19 @@ namespace Bicep.Core.Registry.Oci public class OciArtifactEmulatedReference : ArtifactReference { private readonly IFileHandle fileHandle; + private readonly string modulePath; + private readonly string? fullyQualifiedReference; - public OciArtifactEmulatedReference(BicepSourceFile referencingFile, string modulePath, IFileHandle fileHandle) : - base(referencingFile, OciArtifactReferenceFacts.EmulatedScheme) + public OciArtifactEmulatedReference(BicepSourceFile referencingFile, string modulePath, IFileHandle fileHandle, string? fullyQualifiedReference = null) + : base(referencingFile, OciArtifactReferenceFacts.EmulatedScheme) { this.modulePath = modulePath; this.fileHandle = fileHandle; + this.fullyQualifiedReference = fullyQualifiedReference; } - private readonly string modulePath; - // Override FullyQualifiedReference so user-facing diagnostics shows "br:..." - public override string FullyQualifiedReference => $"{OciArtifactReferenceFacts.Scheme}:{UnqualifiedReference}"; + public override string FullyQualifiedReference => fullyQualifiedReference ?? $"{OciArtifactReferenceFacts.Scheme}:{UnqualifiedReference}"; public override string UnqualifiedReference => modulePath; From d0cd2964687bd56e83b9ff0202ad6acb4654b64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Sun, 3 May 2026 21:05:33 +0200 Subject: [PATCH 09/10] Remove unused using --- src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs index ca96a816954..701ce82d7a5 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using Bicep.Core.Diagnostics; -using Bicep.Core.Modules; using Bicep.Core.SourceGraph; using Bicep.IO.Abstraction; From 89de8eeca777949f7aa49e7d603de7f37777309a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=A5hlin?= Date: Mon, 4 May 2026 06:53:09 +0200 Subject: [PATCH 10/10] Add support for absolute paths for module aliases Co-authored-by: Copilot --- .../Diagnostics/DiagnosticBuilder.cs | 6 +++++- .../Oci/OciArtifactEmulatedReference.cs | 20 ++++++++++++++++--- src/Bicep.IO/Abstraction/IOUri.cs | 3 +++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index 1e074f44abd..bac6660a221 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -2038,10 +2038,14 @@ public Diagnostic OciArtifactModuleAliasFileSystemOnlySupportsModules(string ali public Diagnostic ModuleReferenceSchemeBrFsNotSupported() => CoreError( "BCP448", "The 'br-fs' module reference scheme is for internal use only. Use a 'br/:' reference with a configured 'fileSystem' alias instead."); - public Diagnostic ConfigurationFileNotFound(string featureName) => CoreError( "BCP449", $"Configuration file is not found. Feature \"{featureName}\" requires a configuration file."); + + public Diagnostic InvalidOciArtifactModuleAliasFileSystemPath(string? aliasName, string path, string reason) => CoreError( + "BCP450", + $"The OCI artifact module alias{(aliasName is not null ? $" \"{aliasName}\"" : "")} has an invalid \"fileSystem\" path \"{path}\": {reason}"); + } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs index 701ce82d7a5..d1f3ad5776d 100644 --- a/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs +++ b/src/Bicep.Core/Registry/Oci/OciArtifactEmulatedReference.cs @@ -90,12 +90,26 @@ public static ResultWithDiagnosticBuilder TryParse } IOUri baseUri; - // Resolve the filesystem base directory relative to bicepconfig.json - // Ensure the fileSystem path ends with '/' so it's treated as a directory + // Ensure the fileSystem path ends with '/' so it's treated as a directory. var directoryPath = fileSystemPath.EndsWith('/') || fileSystemPath.EndsWith('\\') ? fileSystemPath : fileSystemPath + "/"; - baseUri = configFileUri.Resolve(directoryPath); + + if (IOUri.IsAbsoluteFilePath(fileSystemPath)) + { + try + { + baseUri = IOUri.FromFilePath(directoryPath); + } + catch (IOException ex) + { + return new(x => x.InvalidOciArtifactModuleAliasFileSystemPath(aliasName, fileSystemPath, ex.Message)); + } + } + else + { + baseUri = configFileUri.Resolve(directoryPath); + } // Construct the file URI by appending the module path with a .bicep extension. var moduleFileName = modulePath + ".bicep"; diff --git a/src/Bicep.IO/Abstraction/IOUri.cs b/src/Bicep.IO/Abstraction/IOUri.cs index 49e33f7f67b..e056da52e1b 100644 --- a/src/Bicep.IO/Abstraction/IOUri.cs +++ b/src/Bicep.IO/Abstraction/IOUri.cs @@ -71,6 +71,9 @@ public IOUri(IOUriScheme scheme, string? authority, string path, string query = public static implicit operator string(IOUri uri) => uri.ToString(); + public static bool IsAbsoluteFilePath(string path) => + FilePath.IsPathFullyQualified(path) || path.StartsWith('/') || path.StartsWith('\\'); + public static IOUri FromFilePath(string filePath) { if (!FilePath.IsPathFullyQualified(filePath) && !(filePath.StartsWith('/') || filePath.StartsWith('\\')))