-
-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
348 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import { spawnProcess } from '../../utils/system'; | ||
import { NoneArtifactProvider } from '../../artifact_providers/none'; | ||
import { ConfigurationError } from '../../utils/errors'; | ||
import { PowerShellTarget } from '../powershell'; | ||
|
||
jest.mock('fs'); | ||
jest.mock('../../utils/system'); | ||
|
||
/** Returns a new PowerShellTarget test instance. */ | ||
function getPwshTarget(): PowerShellTarget { | ||
return new PowerShellTarget( | ||
{ | ||
name: 'powershell', | ||
module: 'moduleName', | ||
repository: 'repositoryName', | ||
}, | ||
new NoneArtifactProvider() | ||
); | ||
} | ||
|
||
function setPwshEnvironmentVariables() { | ||
process.env.POWERSHELL_API_KEY = 'test access key'; | ||
} | ||
|
||
describe('pwsh environment variables', () => { | ||
const oldEnvVariables = process.env; | ||
|
||
beforeEach(() => { | ||
jest.resetModules(); // Clear the cache. | ||
process.env = { ...oldEnvVariables }; // Restore environment | ||
}); | ||
|
||
afterAll(() => { | ||
process.env = { ...oldEnvVariables }; // Restore environment | ||
}); | ||
|
||
function deleteTargetOptionsFromEnvironment() { | ||
if ('POWERSHELL_API_KEY' in process.env) { | ||
delete process.env.POWERSHELL_API_KEY; | ||
} | ||
} | ||
|
||
test('errors on missing environment variables', () => { | ||
deleteTargetOptionsFromEnvironment(); | ||
try { | ||
getPwshTarget(); | ||
} catch (e) { | ||
expect(e instanceof ConfigurationError).toBe(true); | ||
} | ||
}); | ||
|
||
test('success on environment variables', () => { | ||
deleteTargetOptionsFromEnvironment(); | ||
setPwshEnvironmentVariables(); | ||
getPwshTarget(); | ||
}); | ||
}); | ||
|
||
describe('config', () => { | ||
function clearConfig(target: PowerShellTarget): void { | ||
target.psConfig.apiKey = ''; | ||
target.psConfig.repository = ''; | ||
target.psConfig.module = ''; | ||
} | ||
|
||
test('fails with missing config parameters', async () => { | ||
const target = getPwshTarget(); | ||
clearConfig(target); | ||
try { | ||
await target.publish('', ''); | ||
} catch (error) { | ||
expect(error).toBeInstanceOf(ConfigurationError); | ||
expect(error.message).toBe( | ||
'Missing project configuration parameter(s): apiKey,repository,module'); | ||
} | ||
}); | ||
}); | ||
|
||
describe('publish', () => { | ||
const mockedSpawnProcess = spawnProcess as jest.Mock; | ||
const spawnOptions = { enableInDryRunMode: true, showStdout: true } | ||
|
||
beforeEach(() => { | ||
setPwshEnvironmentVariables(); | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
|
||
test('error on missing artifact', async () => { | ||
const target = getPwshTarget(); | ||
target.getArtifactsForRevision = jest.fn() | ||
.mockImplementation(() => []).bind(PowerShellTarget); | ||
|
||
// `publish` should report an error. When it's not dry run, the error is | ||
// thrown; when it's on dry run, the error is logged and `undefined` is | ||
// returned. Thus, both alternatives have been considered. | ||
try { | ||
const noPackageFound = await target.publish('version', 'revision'); | ||
expect(noPackageFound).toBe(undefined); | ||
} catch (error) { | ||
expect(error).toBeInstanceOf(Error); | ||
expect(error.message).toMatch(/there are no matching artifacts/); | ||
} | ||
}); | ||
|
||
test('error on having too many artifacts', async () => { | ||
const target = getPwshTarget(); | ||
target.getArtifactsForRevision = jest.fn() | ||
.mockImplementation(() => ['file1', 'file2']).bind(PowerShellTarget); | ||
|
||
// `publish` should report an error. When it's not dry run, the error is | ||
// thrown; when it's on dry run, the error is logged and `undefined` is | ||
// returned. Thus, both alternatives have been considered. | ||
try { | ||
await target.publish('1.0', 'sha'); | ||
} catch (error) { | ||
expect(error).toBeInstanceOf(Error); | ||
expect(error.message).toMatch(/found multiple matching artifacts/); | ||
} | ||
}); | ||
|
||
test('prints pwsh info', async () => { | ||
const target = getPwshTarget(); | ||
try { | ||
await target.publish('1.0', 'sha'); | ||
} catch (error) { | ||
expect(error).toBeInstanceOf(Error); | ||
expect(error.message).toMatch(/there are no matching artifact/); | ||
} | ||
expect(mockedSpawnProcess).toBeCalledWith('pwsh', ['--version'], {}, spawnOptions); | ||
expect(mockedSpawnProcess).toBeCalledWith('pwsh', | ||
[ | ||
'-Command', | ||
`$ErrorActionPreference = 'Stop' | ||
$info = Get-Command -Name Publish-Module | ||
"Module name: $($info.ModuleName)" | ||
"Module version: $($info.Module.Version)" | ||
"Module path: $($info.Module.Path)" | ||
` | ||
], {}, spawnOptions); | ||
}); | ||
|
||
test('publish-module runs with expected args', async () => { | ||
const target = getPwshTarget(); | ||
await target.publishModule('/path/to/module'); | ||
expect(mockedSpawnProcess).toBeCalledWith('pwsh', | ||
[ | ||
'-Command', | ||
`$ErrorActionPreference = 'Stop' | ||
Publish-Module -Path '/path/to/module' \` | ||
-Repository 'repositoryName' \` | ||
-NuGetApiKey 'test access key' \` | ||
-WhatIf:$false | ||
` | ||
], {}, spawnOptions); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import { join } from 'path'; | ||
import { BaseArtifactProvider } from '../artifact_providers/base'; | ||
import { TargetConfig } from '../schemas/project_config'; | ||
import { ConfigurationError, reportError } from '../utils/errors'; | ||
import { withTempDir } from '../utils/files'; | ||
import { isDryRun } from '../utils/helpers'; | ||
import { SpawnProcessOptions, checkExecutableIsPresent, extractZipArchive, spawnProcess } from '../utils/system'; | ||
import { BaseTarget } from './base'; | ||
|
||
/** Command to launch PowerShell */ | ||
export const POWERSHELL_BIN = process.env.POWERSHELL_BIN || 'pwsh'; | ||
|
||
/** Default repository */ | ||
export const DEFAULT_POWERSHELL_REPOSITORY = 'PSGallery'; | ||
|
||
/** PowerShell target configuration options */ | ||
export interface PowerShellTargetOptions { | ||
/** API token */ | ||
apiKey: string; | ||
/** PowerShell repository name */ | ||
repository: string; | ||
/** Module name */ | ||
module: string; | ||
} | ||
|
||
/** | ||
* Target responsible for publishing modules to a PowerShell repository | ||
*/ | ||
export class PowerShellTarget extends BaseTarget { | ||
/** Target name */ | ||
public readonly name: string = 'powershell'; | ||
/** Target options */ | ||
public readonly psConfig: PowerShellTargetOptions; | ||
private readonly defaultSpawnOptions = { enableInDryRunMode: true, showStdout: true } | ||
|
||
public constructor( | ||
config: TargetConfig, | ||
artifactProvider: BaseArtifactProvider | ||
) { | ||
super(config, artifactProvider); | ||
this.psConfig = { | ||
apiKey: process.env.POWERSHELL_API_KEY || '', | ||
repository: this.config.repository || DEFAULT_POWERSHELL_REPOSITORY, | ||
module: this.config.module || '', | ||
}; | ||
checkExecutableIsPresent(POWERSHELL_BIN); | ||
} | ||
|
||
/** | ||
* Executes a PowerShell command. | ||
*/ | ||
private async spawnPwsh( | ||
command: string, | ||
spawnProcessOptions: SpawnProcessOptions = this.defaultSpawnOptions | ||
): Promise<Buffer | undefined> { | ||
command = `$ErrorActionPreference = 'Stop'\n` + command; | ||
this.logger.trace("Executing PowerShell command:", command); | ||
return spawnProcess(POWERSHELL_BIN, ['-Command', command], {}, spawnProcessOptions); | ||
} | ||
|
||
/** | ||
* Checks if the required project configuration parameters are available. | ||
* The required parameters are `layerName` and `compatibleRuntimes`. | ||
* There is also an optional parameter `includeNames`. | ||
*/ | ||
private checkProjectConfig(): void { | ||
const missingConfigOptions = []; | ||
if (this.psConfig.apiKey.length === 0) { | ||
missingConfigOptions.push('apiKey'); | ||
} | ||
if (this.psConfig.repository.length === 0) { | ||
missingConfigOptions.push('repository'); | ||
} | ||
if (this.psConfig.module.length === 0) { | ||
missingConfigOptions.push('module'); | ||
} | ||
if (missingConfigOptions.length > 0) { | ||
throw new ConfigurationError( | ||
'Missing project configuration parameter(s): ' + missingConfigOptions | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Publishes a module to a PowerShell repository. | ||
* @param _version ignored; the version must be set in the module manifest. | ||
* @param revision Git commit SHA to be published. | ||
*/ | ||
public async publish(_version: string, revision: string): Promise<any> { | ||
this.checkProjectConfig(); | ||
|
||
// Emit the PowerShell executable for informational purposes. | ||
this.logger.info(`PowerShell (${POWERSHELL_BIN}) info:`); | ||
await spawnProcess(POWERSHELL_BIN, ['--version'], {}, this.defaultSpawnOptions); | ||
|
||
// Also check the command and its its module version in case there are issues: | ||
this.logger.info('Publish-Module command info:'); | ||
await this.spawnPwsh(` | ||
$info = Get-Command -Name Publish-Module | ||
"Module name: $($info.ModuleName)" | ||
"Module version: $($info.Module.Version)" | ||
"Module path: $($info.Module.Path)" | ||
`); | ||
|
||
// Escape the given module artifact name to avoid regex issues. | ||
let moduleArtifactRegex = `${this.psConfig.module}`.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); | ||
moduleArtifactRegex = `/^${moduleArtifactRegex}\\.zip$/` | ||
|
||
this.logger.debug(`Looking for artifact matching ${moduleArtifactRegex}`); | ||
const packageFiles = await this.getArtifactsForRevision(revision, { | ||
includeNames: moduleArtifactRegex, | ||
}); | ||
if (!packageFiles.length) { | ||
reportError( | ||
`Cannot release the module to ${this.psConfig.repository}: there are no matching artifacts!` | ||
); | ||
} else if (packageFiles.length > 1) { | ||
reportError( | ||
`Cannot release the module to ${this.psConfig.repository}: found multiple matching artifacts!` | ||
); | ||
} | ||
const artifact = packageFiles[0]; | ||
const zipPath = await this.artifactProvider.downloadArtifact(artifact); | ||
|
||
this.logger.info(`Extracting artifact "${artifact.filename}"`) | ||
await withTempDir(async dir => { | ||
const moduleDir = join(dir, this.psConfig.module); | ||
await extractZipArchive(zipPath, moduleDir); | ||
await this.publishModule(moduleDir); | ||
}); | ||
|
||
this.logger.info(`PowerShell module upload complete`); | ||
} | ||
|
||
public async publishModule(moduleDir: string): Promise<void> { | ||
this.logger.info(`Publishing PowerShell module "${this.psConfig.module}" to ${this.psConfig.repository}`) | ||
await this.spawnPwsh(` | ||
Publish-Module -Path '${moduleDir}' \` | ||
-Repository '${this.psConfig.repository}' \` | ||
-NuGetApiKey '${this.psConfig.apiKey}' \` | ||
-WhatIf:$${isDryRun()} | ||
`); | ||
} | ||
} |