Skip to content

Commit

Permalink
feat(release): publishing npm packages to AWS CodeArtifact (projen#1021)
Browse files Browse the repository at this point in the history
This PR introduces publishing npm packages to AWS CodeArtifact.

The authentication will be done using AWS CLI. This depends on PR cdklabs/publib#150 which has to be merged first.

`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` have to be defined as GitHub Secrets to follow the [IAM best practices](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html#iam-user-access-keys
). The names of those variables can be overridden. The default values will be applied if an AWS CodeArtifact URL is detected.

Closes projen#986.

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
  • Loading branch information
jumic authored Nov 2, 2021
1 parent bfcbd28 commit de0c1fc
Show file tree
Hide file tree
Showing 9 changed files with 1,032 additions and 6 deletions.
69 changes: 68 additions & 1 deletion API.md

Large diffs are not rendered by default.

25 changes: 24 additions & 1 deletion docs/publisher.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,29 @@ publisher.publishToMaven({
})
```

## Publishing to AWS CodeArtifact

The NPM target comes with dynamic defaults that support AWS CodeArtifact.
If the respective registry URL is detected to be AWS CodeArtifact, other relevant options will automatically be set to fitting values.
The authentication will done using [AWS CLI](https://docs.aws.amazon.com/codeartifact/latest/ug/tokens-authentication.html). It is neccessary to provide AWS IAM CLI `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in GitHub Secrets.

**npm**
```ts
publisher.publishToNpm({
registry: 'my-domain-111122223333.d.codeartifact.us-west-2.amazonaws.com/npm/my_repo/',
});
```

The names of the GitHub Secrets can be overridden if different names should be used.
```ts
publisher.publishToNpm({
registry: 'my-domain-111122223333.d.codeartifact.us-west-2.amazonaws.com/npm/my_repo/',
codeArtifactOptions: {
accessKeyIdSecret: 'CUSTOM_AWS_ACCESS_KEY_ID',
secretAccessKeySecret: 'CUSTOM_AWS_SECRET_ACCESS_KEY',
},
});
```
## Handling Failures

You can instruct the publisher to create GitHub issues for publish failures:
Expand All @@ -88,4 +111,4 @@ const publisher = new Publisher(project, {
This will create an issue labeled with the `failed-release` label for every individual failed publish task.
For example, if Nuget publishing failed for a specific version, it will create an issue titled *Publishing v1.0.4 to Nuget gallery failed*.

This can be helpful to keep track of failed releases as well as integrate with third-party ticketing systems by querying issues labeled with `failed-release`.
This can be helpful to keep track of failed releases as well as integrate with third-party ticketing systems by querying issues labeled with `failed-release`.
66 changes: 64 additions & 2 deletions src/node-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Component } from './component';
import { DependencyType } from './deps';
import { JsonFile } from './json';
import { Project } from './project';
import { isAwsCodeArtifactRegistry } from './release/publisher';
import { Task } from './tasks';
import { exec, isTruthy, sorted, writeFile } from './util';

Expand Down Expand Up @@ -290,6 +291,31 @@ export interface NodePackageOptions {
* @default "NPM_TOKEN"
*/
readonly npmTokenSecret?: string;

/**
* Options for publishing npm package to AWS CodeArtifact.
*
* @default - undefined
*/
readonly codeArtifactOptions?: CodeArtifactOptions;
}

export interface CodeArtifactOptions {
/**
* GitHub secret which contains the AWS access key ID to use when publishing packages to AWS CodeArtifact.
* This property must be specified only when publishing to AWS CodeArtifact (`npmRegistryUrl` contains AWS CodeArtifact URL).
*
* @default "AWS_ACCESS_KEY_ID"
*/
readonly accessKeyIdSecret?: string;

/**
* GitHub secret which contains the AWS secret access key to use when publishing packages to AWS CodeArtifact.
* This property must be specified only when publishing to AWS CodeArtifact (`npmRegistryUrl` contains AWS CodeArtifact URL).
*
* @default "AWS_SECRET_ACCESS_KEY"
*/
readonly secretAccessKeySecret?: string;
}

/**
Expand Down Expand Up @@ -362,7 +388,14 @@ export class NodePackage extends Component {
/**
* GitHub secret which contains the NPM token to use when publishing packages.
*/
public readonly npmTokenSecret: string;
public readonly npmTokenSecret?: string;

/**
* Options for publishing npm package to AWS CodeArtifact.
*
* @default - undefined
*/
readonly codeArtifactOptions?: CodeArtifactOptions;

/**
* npm package access level.
Expand Down Expand Up @@ -394,12 +427,15 @@ export class NodePackage extends Component {

this.project.annotateGenerated(`/${this.lockFile}`);

const { npmDistTag, npmAccess, npmRegistry, npmRegistryUrl, npmTokenSecret } = this.parseNpmOptions(options);
const {
npmDistTag, npmAccess, npmRegistry, npmRegistryUrl, npmTokenSecret, codeArtifactOptions,
} = this.parseNpmOptions(options);
this.npmDistTag = npmDistTag;
this.npmAccess = npmAccess;
this.npmRegistry = npmRegistry;
this.npmRegistryUrl = npmRegistryUrl;
this.npmTokenSecret = npmTokenSecret;
this.codeArtifactOptions = codeArtifactOptions;

this.processDeps(options);

Expand Down Expand Up @@ -728,12 +764,33 @@ export class NodePackage extends Component {
throw new Error(`"npmAccess" cannot be RESTRICTED for non-scoped npm package "${this.packageName}"`);
}

const isAwsCodeArtifact = isAwsCodeArtifactRegistry(npmRegistryUrl);
if (isAwsCodeArtifact) {
if (options.npmTokenSecret) {
throw new Error('"npmTokenSecret" must not be specified when publishing AWS CodeArtifact.');
}
} else {
if (options.codeArtifactOptions?.accessKeyIdSecret || options.codeArtifactOptions?.secretAccessKeySecret) {
throw new Error('"codeArtifactOptions.accessKeyIdSecret" and "codeArtifactOptions.secretAccessKeySecret" must only be specified when publishing AWS CodeArtifact.');
}
}

// apply defaults for AWS CodeArtifact
let codeArtifactOptions: CodeArtifactOptions | undefined;
if (isAwsCodeArtifact) {
codeArtifactOptions = {
accessKeyIdSecret: options.codeArtifactOptions?.accessKeyIdSecret ?? 'AWS_ACCESS_KEY_ID',
secretAccessKeySecret: options.codeArtifactOptions?.secretAccessKeySecret ?? 'AWS_SECRET_ACCESS_KEY',
};
}

return {
npmDistTag: options.npmDistTag ?? DEFAULT_NPM_TAG,
npmAccess,
npmRegistry: npmr.hostname + this.renderNpmRegistryPath(npmr.pathname!),
npmRegistryUrl: npmr.href,
npmTokenSecret: defaultNpmToken(options.npmTokenSecret, npmr.hostname),
codeArtifactOptions,
};
}

Expand Down Expand Up @@ -1080,6 +1137,11 @@ function defaultNpmAccess(packageName: string) {
}

export function defaultNpmToken(npmToken: string | undefined, registry: string | undefined) {
// if we are publishing to AWS CdodeArtifact, no NPM_TOKEN used (will be requested using AWS CLI later).
if (isAwsCodeArtifactRegistry(registry)) {
return undefined;
}

// if we are publishing to GitHub Packages, default to GITHUB_TOKEN.
const isGitHubPackages = registry === GITHUB_PACKAGES_REGISTRY;
return npmToken ?? (isGitHubPackages ? DEFAULT_GITHUB_TOKEN_SECRET : DEFAULT_NPM_TOKEN_SECRET);
Expand Down
4 changes: 4 additions & 0 deletions src/node-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,10 @@ export class NodeProject extends GitHubProject {
distTag: this.package.npmDistTag,
registry: this.package.npmRegistry,
npmTokenSecret: this.package.npmTokenSecret,
codeArtifactOptions: {
accessKeyIdSecret: options.codeArtifactOptions?.accessKeyIdSecret,
secretAccessKeySecret: options.codeArtifactOptions?.secretAccessKeySecret,
},
});
}

Expand Down
42 changes: 41 additions & 1 deletion src/release/publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const GITHUB_PACKAGES_REGISTRY = 'npm.pkg.github.com';
const GITHUB_PACKAGES_MAVEN_REPOSITORY = 'https://maven.pkg.github.com';
const ARTIFACTS_DOWNLOAD_DIR = 'dist';
const JSII_RELEASE_IMAGE = 'jsii/superchain:1-buster-slim-node14';
const AWS_CODEARTIFACT_REGISTRY_REGEX = /.codeartifact.*.amazonaws.com/;

/**
* Options for `Publisher`.
Expand Down Expand Up @@ -183,6 +184,8 @@ export class Publisher extends Component {
*/
public publishToNpm(options: NpmPublishOptions = {}) {
const isGitHubPackages = options.registry?.startsWith(GITHUB_PACKAGES_REGISTRY);
const isAwsCodeArtifact = isAwsCodeArtifactRegistry(options.registry);
const npmToken = defaultNpmToken(options.npmTokenSecret, options.registry);

this.addPublishJob({
name: 'npm',
Expand All @@ -198,7 +201,10 @@ export class Publisher extends Component {
packages: isGitHubPackages ? JobPermission.WRITE : undefined,
},
workflowEnv: {
NPM_TOKEN: secret(defaultNpmToken(options.npmTokenSecret, options.registry)),
NPM_TOKEN: npmToken ? secret(npmToken) : undefined,
// if we are publishing to AWS CodeArtifact, pass AWS access keys that will be used to generate NPM_TOKEN using AWS CLI.
AWS_ACCESS_KEY_ID: isAwsCodeArtifact ? secret(options.codeArtifactOptions?.accessKeyIdSecret ?? 'AWS_ACCESS_KEY_ID') : undefined,
AWS_SECRET_ACCESS_KEY: isAwsCodeArtifact ? secret(options.codeArtifactOptions?.secretAccessKeySecret ?? 'AWS_SECRET_ACCESS_KEY') : undefined,
},
});
}
Expand Down Expand Up @@ -476,6 +482,31 @@ export interface NpmPublishOptions {
* @default - "NPM_TOKEN" or "GITHUB_TOKEN" if `registry` is set to `npm.pkg.github.com`.
*/
readonly npmTokenSecret?: string;

/**
* Options for publishing npm package to AWS CodeArtifact.
*
* @default - undefined
*/
readonly codeArtifactOptions?: CodeArtifactOptions;
}

export interface CodeArtifactOptions {
/**
* GitHub secret which contains the AWS access key ID to use when publishing packages to AWS CodeArtifact.
* This property must be specified only when publishing to AWS CodeArtifact (`registry` contains AWS CodeArtifact URL).
*
* @default "AWS_ACCESS_KEY_ID"
*/
readonly accessKeyIdSecret?: string;

/**
* GitHub secret which contains the AWS secret access key to use when publishing packages to AWS CodeArtifact.
* This property must be specified only when publishing to AWS CodeArtifact (`registry` contains AWS CodeArtifact URL).
*
* @default "AWS_SECRET_ACCESS_KEY"
*/
readonly secretAccessKeySecret?: string;
}

/**
Expand Down Expand Up @@ -684,6 +715,15 @@ interface VersionArtifactOptions {
readonly changelogFile: string;
}

/**
* Evaluates if the `registryUrl` is a AWS CodeArtifact registry.
* @param registryUrl url of registry
* @returns true for AWS CodeArtifact
*/
export function isAwsCodeArtifactRegistry(registryUrl: string | undefined) {
return registryUrl && AWS_CODEARTIFACT_REGISTRY_REGEX.test(registryUrl);
}

/**
* Publishing options for GitHub releases.
*/
Expand Down
Loading

0 comments on commit de0c1fc

Please sign in to comment.