diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 698425d66bd..7292312eaaf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -710,6 +710,9 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + submodules: true - name: Setup Node.js uses: actions/setup-node@v4 @@ -786,6 +789,6 @@ jobs: - name: Run Bicep Live E2E Tests (${{ matrix.environment }}) run: npm ci && npm run test:live:${{ matrix.environment }} env: - BICEP_CLI_DOTNET_RID: ${{ matrix.runtime.rid }} + BICEP_CLI_DOTNET_RID: linux-musl-x64 BICEP_CLI_EXECUTABLE: ../../../Bicep.Cli.E2eTests/src/temp/bicep-cli/bicep working-directory: ./src/Bicep.Cli.E2eTests \ No newline at end of file diff --git a/src/Bicep.Cli.E2eTests/src/examples/local-deploy/bicepconfig.ff.json b/src/Bicep.Cli.E2eTests/src/examples/local-deploy/bicepconfig.ff.json new file mode 100644 index 00000000000..b47259999d0 --- /dev/null +++ b/src/Bicep.Cli.E2eTests/src/examples/local-deploy/bicepconfig.ff.json @@ -0,0 +1,18 @@ +{ + "experimentalFeaturesEnabled": { + "extensibility": true, + "extensionRegistry": true, + "localDeploy": true + }, + "extensions": { + "mock": "$TARGET_REFERENCE" + }, + "cloud": { + "currentProfile": "AzureUSGovernment", + "credentialPrecedence": [ + // used by CI + "Environment", + "AzureCLI" + ] + } +} diff --git a/src/Bicep.Cli.E2eTests/src/examples/local-deploy/bicepconfig.json b/src/Bicep.Cli.E2eTests/src/examples/local-deploy/bicepconfig.json index 18453119d53..3739be51ed7 100644 --- a/src/Bicep.Cli.E2eTests/src/examples/local-deploy/bicepconfig.json +++ b/src/Bicep.Cli.E2eTests/src/examples/local-deploy/bicepconfig.json @@ -3,5 +3,8 @@ "extensibility": true, "extensionRegistry": true, "localDeploy": true + }, + "extensions": { + "mock": "$TARGET_REFERENCE" } } diff --git a/src/Bicep.Cli.E2eTests/src/examples/local-deploy/bicepconfig.prod.json b/src/Bicep.Cli.E2eTests/src/examples/local-deploy/bicepconfig.prod.json new file mode 100644 index 00000000000..5c45857f3ed --- /dev/null +++ b/src/Bicep.Cli.E2eTests/src/examples/local-deploy/bicepconfig.prod.json @@ -0,0 +1,14 @@ +{ + "experimentalFeaturesEnabled": { + "extensibility": true, + "extensionRegistry": true, + "localDeploy": true + }, + "extensions": { + "mock": "$TARGET_REFERENCE" + }, + "cloud": { + "currentProfile": "AzureCloud", + "credentialPrecedence": ["AzureCLI"] + } +} diff --git a/src/Bicep.Cli.E2eTests/src/examples/local-deploy/main.bicep b/src/Bicep.Cli.E2eTests/src/examples/local-deploy/main.bicep index 1ee5894acc2..bfce500094f 100644 --- a/src/Bicep.Cli.E2eTests/src/examples/local-deploy/main.bicep +++ b/src/Bicep.Cli.E2eTests/src/examples/local-deploy/main.bicep @@ -1,4 +1,4 @@ -extension '../../temp/local-deploy/provider.tgz' +extension mock param payload string diff --git a/src/Bicep.Cli.E2eTests/src/localDeploy.test.live.ts b/src/Bicep.Cli.E2eTests/src/localDeploy.test.live.ts new file mode 100644 index 00000000000..18ecbde8dcf --- /dev/null +++ b/src/Bicep.Cli.E2eTests/src/localDeploy.test.live.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Live tests for "bicep local-deploy". + * + * @group live + */ + +import os from "os"; +import { invokingBicepCommand } from "./utils/command"; +import { copyToTempFile, pathToExampleFile, pathToTempFile } from "./utils/fs"; +import { + platformSupportsLocalDeploy, + publishExtension, +} from "./utils/localdeploy"; +import { getEnvironment } from "./utils/liveTestEnvironments"; +import { BicepRegistryReferenceBuilder } from "./utils/br"; +import { itif } from "./utils/testHelpers"; + +describe("bicep local-deploy", () => { + const environment = getEnvironment(); + const testArea = `local-deploy-live${environment.suffix}`; + const builder = new BicepRegistryReferenceBuilder( + environment.registryUri, + testArea, + ); + + itif(platformSupportsLocalDeploy())( + "should publish and run an extension published to a registry", + () => { + const baseFolder = pathToExampleFile("local-deploy"); + const target = builder.getBicepReference("mock", "0.0.1"); + + const files = { + bicep: copyToTempFile(baseFolder, "main.bicep", testArea), + bicepparam: copyToTempFile(baseFolder, "main.bicepparam", testArea), + bicepconfig: copyToTempFile( + baseFolder, + `bicepconfig${environment.suffix}.json`, + testArea, + { + relativePath: "bicepconfig.json", + values: { + $TARGET_REFERENCE: target, + }, + }, + ), + }; + + const typesIndexPath = pathToTempFile(testArea, "types", "index.json"); + publishExtension(typesIndexPath, target) + .shouldSucceed() + .withEmptyStdout(); + + invokingBicepCommand("local-deploy", files.bicepparam) + .shouldSucceed() + .withStdout( + [ + 'Output sayHiResult: "Hello, World!"', + "Resource sayHi (Create): Succeeded", + "Result: Succeeded", + "", + ].join(os.EOL), + ); + }, + ); +}); diff --git a/src/Bicep.Cli.E2eTests/src/localDeploy.test.ts b/src/Bicep.Cli.E2eTests/src/localDeploy.test.ts index ac46952daf3..924bc31adf1 100644 --- a/src/Bicep.Cli.E2eTests/src/localDeploy.test.ts +++ b/src/Bicep.Cli.E2eTests/src/localDeploy.test.ts @@ -7,85 +7,50 @@ * @group CI */ -import spawn from "cross-spawn"; import os from "os"; -import path from "path"; import { invokingBicepCommand } from "./utils/command"; +import { copyToTempFile, pathToExampleFile, pathToTempFile } from "./utils/fs"; import { - ensureParentDirExists, - expectFileExists, - pathToExampleFile, - pathToTempFile, -} from "./utils/fs"; - -const itif = (condition: boolean) => condition ? it : it.skip; -const cliDotnetRid = process.env.BICEP_CLI_DOTNET_RID; -// We don't have an easy way of running this test for linux-musl-x64 RID, so skip for now. -const canRunLocalDeploy = () => !cliDotnetRid || architectures.map(x => x.dotnetRid).includes(cliDotnetRid); - -const mockExtensionExeName = 'bicep-ext-mock'; -const mockExtensionProjPath = path.resolve( - __dirname, - "../../Bicep.Local.Extension.Mock" -); - -const architectures = [ - { dotnetRid: 'osx-arm64', bicepArgs: ['--bin-osx-arm64', `${mockExtensionProjPath}/bin/release/net8.0/osx-arm64/publish/${mockExtensionExeName}`] }, - { dotnetRid: 'osx-x64', bicepArgs: ['--bin-osx-x64', `${mockExtensionProjPath}/bin/release/net8.0/osx-x64/publish/${mockExtensionExeName}`] }, - { dotnetRid: 'linux-x64', bicepArgs: ['--bin-linux-x64', `${mockExtensionProjPath}/bin/release/net8.0/linux-x64/publish/${mockExtensionExeName}`] }, - { dotnetRid: 'win-x64', bicepArgs: ['--bin-win-x64', `${mockExtensionProjPath}/bin/release/net8.0/win-x64/publish/${mockExtensionExeName}.exe`] }, -]; + platformSupportsLocalDeploy, + publishExtension, +} from "./utils/localdeploy"; +import { itif } from "./utils/testHelpers"; describe("bicep local-deploy", () => { - itif(canRunLocalDeploy())("should build and deploy a provider published to the local file system", () => { - - for (const arch of architectures) { - execDotnet(['publish', '--verbosity', 'quiet', '--configuration', 'release', '--self-contained', 'true', '-r', arch.dotnetRid, mockExtensionProjPath]); - } - - const typesIndexPath = pathToTempFile("local-deploy", "types", "index.json"); - const typesDir = path.dirname(typesIndexPath); - ensureParentDirExists(typesIndexPath); - - execDotnet(['run', '--verbosity', 'quiet', '--project', mockExtensionProjPath], { - MOCK_TYPES_OUTPUT_PATH: typesDir, - }); - - const targetPath = pathToTempFile("local-deploy", "provider.tgz"); - - invokingBicepCommand( - "publish-provider", - typesIndexPath, - "--target", - targetPath, - ...(architectures.flatMap(arch => arch.bicepArgs)) - ) - .shouldSucceed() - .withEmptyStdout(); - - expectFileExists(targetPath); - - const bicepparamFilePath = pathToExampleFile("local-deploy", "main.bicepparam"); - - invokingBicepCommand("local-deploy", bicepparamFilePath) - .shouldSucceed() - .withStdout([ - 'Output sayHiResult: "Hello, World!"', - 'Resource sayHi (Create): Succeeded', - 'Result: Succeeded', - '' - ].join(os.EOL)); - }); + const testArea = "local-deploy"; + + itif(platformSupportsLocalDeploy())( + "should publish and run an extension published to the local file system", + () => { + const baseFolder = pathToExampleFile("local-deploy"); + const target = pathToTempFile(testArea, "mock.tgz"); + + const files = { + bicep: copyToTempFile(baseFolder, "main.bicep", testArea), + bicepparam: copyToTempFile(baseFolder, "main.bicepparam", testArea), + bicepconfig: copyToTempFile(baseFolder, `bicepconfig.json`, testArea, { + relativePath: "bicepconfig.json", + values: { + $TARGET_REFERENCE: "./mock.tgz", + }, + }), + }; + + const typesIndexPath = pathToTempFile(testArea, "types", "index.json"); + publishExtension(typesIndexPath, target) + .shouldSucceed() + .withEmptyStdout(); + + invokingBicepCommand("local-deploy", files.bicepparam) + .shouldSucceed() + .withStdout( + [ + 'Output sayHiResult: "Hello, World!"', + "Resource sayHi (Create): Succeeded", + "Result: Succeeded", + "", + ].join(os.EOL), + ); + }, + ); }); - -function execDotnet(args: string[], envOverrides?: NodeJS.ProcessEnv) { - const result = spawn.sync('dotnet', args, { - encoding: 'utf-8', - stdio: 'inherit', - env: { - ...process.env, - ...(envOverrides ?? {}) - } - }); - expect(result.status).toBe(0); -} \ No newline at end of file diff --git a/src/Bicep.Cli.E2eTests/src/utils/fs.ts b/src/Bicep.Cli.E2eTests/src/utils/fs.ts index 64aef8337c9..7a24bb49374 100644 --- a/src/Bicep.Cli.E2eTests/src/utils/fs.ts +++ b/src/Bicep.Cli.E2eTests/src/utils/fs.ts @@ -83,4 +83,23 @@ export function writeTempFile( export function ensureParentDirExists(filePath: string): void { fs.mkdirSync(path.dirname(filePath), { recursive: true }); -} \ No newline at end of file +} + +export function copyToTempFile( + baseFolder: string, + relativePath: string, + testArea: string, + replace?: { + values: Record; + relativePath: string + } +) { + const fileContents = readFileSync(path.join(baseFolder, relativePath)); + + const replacedContents = Object.entries(replace?.values ?? {}).reduce( + (contents, [from, to]) => contents.replace(from, to), + fileContents, + ); + + return writeTempFile(testArea, replace?.relativePath ?? relativePath, replacedContents); +} diff --git a/src/Bicep.Cli.E2eTests/src/utils/localdeploy.ts b/src/Bicep.Cli.E2eTests/src/utils/localdeploy.ts new file mode 100644 index 00000000000..8bfe31270f7 --- /dev/null +++ b/src/Bicep.Cli.E2eTests/src/utils/localdeploy.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Live tests for "bicep local-deploy". + * + * @group CI + */ + +import spawn from "cross-spawn"; +import path from "path"; +import { ensureParentDirExists } from "./fs"; +import { invokingBicepCommand } from "./command"; + +const mockExtensionExeName = "bicep-ext-mock"; +const mockExtensionProjPath = path.resolve( + __dirname, + "../../../Bicep.Local.Extension.Mock", +); + +function execDotnet(args: string[], envOverrides?: NodeJS.ProcessEnv) { + const result = spawn.sync("dotnet", args, { + encoding: "utf-8", + stdio: "inherit", + env: { + ...process.env, + ...(envOverrides ?? {}), + }, + }); + expect(result.status).toBe(0); +} + +type ExtensionConfiguration = { + dotnetRid: string; + dotnetPublishPath: string; + bicepCliPublishArg: string; +}; + +const supportedConfigurations: ExtensionConfiguration[] = [ + { + dotnetRid: "osx-arm64", + bicepCliPublishArg: "--bin-osx-arm64", + dotnetPublishPath: `${mockExtensionProjPath}/bin/release/net8.0/osx-arm64/publish/${mockExtensionExeName}`, + }, + { + dotnetRid: "osx-x64", + bicepCliPublishArg: "--bin-osx-x64", + dotnetPublishPath: `${mockExtensionProjPath}/bin/release/net8.0/osx-x64/publish/${mockExtensionExeName}`, + }, + { + dotnetRid: "linux-x64", + bicepCliPublishArg: "--bin-linux-x64", + dotnetPublishPath: `${mockExtensionProjPath}/bin/release/net8.0/linux-x64/publish/${mockExtensionExeName}`, + }, + { + dotnetRid: "win-x64", + bicepCliPublishArg: "--bin-win-x64", + dotnetPublishPath: `${mockExtensionProjPath}/bin/release/net8.0/win-x64/publish/${mockExtensionExeName}.exe`, + }, +]; + +export function platformSupportsLocalDeploy() { + const cliDotnetRid = process.env.BICEP_CLI_DOTNET_RID; + + // We don't have an easy way of running this test for linux-musl-x64 RID, so skip for now. + return ( + !cliDotnetRid || + supportedConfigurations.map((x) => x.dotnetRid).includes(cliDotnetRid) + ); +} + +export function publishExtension(typesIndexPath: string, target: string) { + // build the binary in different flavors + for (const config of supportedConfigurations) { + execDotnet([ + "publish", + "--verbosity", + "quiet", + "--configuration", + "release", + "--self-contained", + "true", + "-r", + config.dotnetRid, + mockExtensionProjPath, + ]); + } + + const typesDir = path.dirname(typesIndexPath); + ensureParentDirExists(typesIndexPath); + + // generate types on disk + execDotnet( + ["run", "--verbosity", "quiet", "--project", mockExtensionProjPath], + { + MOCK_TYPES_OUTPUT_PATH: typesDir, + }, + ); + + // run the bicep command to publish it + return invokingBicepCommand( + "publish-extension", + typesIndexPath, + "--target", + target, + ...supportedConfigurations.flatMap((c) => [ + c.bicepCliPublishArg, + c.dotnetPublishPath, + ]), + ); +} diff --git a/src/Bicep.Cli.E2eTests/src/utils/testHelpers.ts b/src/Bicep.Cli.E2eTests/src/utils/testHelpers.ts new file mode 100644 index 00000000000..b74c581d329 --- /dev/null +++ b/src/Bicep.Cli.E2eTests/src/utils/testHelpers.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export const itif = (condition: boolean) => (condition ? it : it.skip); diff --git a/src/Bicep.Core/Registry/OciArtifactRegistry.cs b/src/Bicep.Core/Registry/OciArtifactRegistry.cs index 49510eef5bf..c7b711a1df9 100644 --- a/src/Bicep.Core/Registry/OciArtifactRegistry.cs +++ b/src/Bicep.Core/Registry/OciArtifactRegistry.cs @@ -428,7 +428,7 @@ protected override void WriteArtifactContentToCache(OciArtifactReference referen throw new InvalidOperationException($"Failed to determine the system OS or architecture to execute provider extension \"{reference}\"."); } - if (binaryArchitectures.Contains(architecture.Name) || + if (!binaryArchitectures.Contains(architecture.Name) || result.TryGetSingleLayerByMediaType(BicepMediaTypes.GetProviderArtifactLayerV1Binary(architecture)) is not { } sourceData) { throw new InvalidOperationException($"The provider extension \"{reference}\" does not support architecture {architecture.Name}.");