diff --git a/API.md b/API.md index 4023a93..50112e2 100644 --- a/API.md +++ b/API.md @@ -2,6 +2,215 @@ ## Constructs +### StackMetadata + +Construct for adding repository and pipeline metadata to a CloudFormation stack. + +This construct adds metadata to the stack containing information about the +repository (provider, owner, repository name, branch, commit) and the +CI/CD pipeline (job ID, job URL, triggered by, workflow name). + +*Example* + +```typescript +// Automatically extract from environment variables +new StackMetadata(this, 'Metadata'); + +// Use custom environment variable names +new StackMetadata(this, 'Metadata', { + customEnvVars: { + repoOwner: 'MY_REPO_OWNER', + repoName: 'MY_REPO_NAME', + branch: 'MY_BRANCH', + commitHash: 'MY_COMMIT_HASH', + }, +}); + +// Manually provide information +new StackMetadata(this, 'Metadata', { + repoInfo: { + provider: CiProvider.GITHUB, + owner: 'my-org', + repository: 'my-repo', + branch: 'main', + commitHash: 'abc123', + }, + pipelineInfo: { + provider: CiProvider.GITHUB, + jobId: '12345', + jobUrl: 'https://github.com/my-org/my-repo/actions/runs/12345', + triggeredBy: 'user@example.com', + }, +}); +``` + + +#### Initializers + +```typescript +import { StackMetadata } from 'cdk-devops' + +new StackMetadata(scope: Construct, id: string, props?: StackMetadataProps) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| scope | constructs.Construct | *No description.* | +| id | string | *No description.* | +| props | StackMetadataProps | *No description.* | + +--- + +##### `scope`Required + +- *Type:* constructs.Construct + +--- + +##### `id`Required + +- *Type:* string + +--- + +##### `props`Optional + +- *Type:* StackMetadataProps + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| toString | Returns a string representation of this construct. | + +--- + +##### `toString` + +```typescript +public toString(): string +``` + +Returns a string representation of this construct. + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| isConstruct | Checks if `x` is a construct. | +| fromEnvironment | Create a new StackMetadata construct that extracts information from environment variables. | + +--- + +##### `isConstruct` + +```typescript +import { StackMetadata } from 'cdk-devops' + +StackMetadata.isConstruct(x: any) +``` + +Checks if `x` is a construct. + +Use this method instead of `instanceof` to properly detect `Construct` +instances, even when the construct library is symlinked. + +Explanation: in JavaScript, multiple copies of the `constructs` library on +disk are seen as independent, completely different libraries. As a +consequence, the class `Construct` in each copy of the `constructs` library +is seen as a different class, and an instance of one class will not test as +`instanceof` the other class. `npm install` will not create installations +like this, but users may manually symlink construct libraries together or +use a monorepo tool: in those cases, multiple copies of the `constructs` +library can be accidentally installed, and `instanceof` will behave +unpredictably. It is safest to avoid using `instanceof`, and using +this type-testing method instead. + +###### `x`Required + +- *Type:* any + +Any object. + +--- + +##### `fromEnvironment` + +```typescript +import { StackMetadata } from 'cdk-devops' + +StackMetadata.fromEnvironment(scope: Construct, id: string, customEnvVars?: CustomEnvVarConfig) +``` + +Create a new StackMetadata construct that extracts information from environment variables. + +###### `scope`Required + +- *Type:* constructs.Construct + +--- + +###### `id`Required + +- *Type:* string + +--- + +###### `customEnvVars`Optional + +- *Type:* CustomEnvVarConfig + +--- + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| node | constructs.Node | The tree node. | +| pipelineInfo | PipelineInfo | The pipeline information. | +| repoInfo | RepoInfo | The repository information. | + +--- + +##### `node`Required + +```typescript +public readonly node: Node; +``` + +- *Type:* constructs.Node + +The tree node. + +--- + +##### `pipelineInfo`Required + +```typescript +public readonly pipelineInfo: PipelineInfo; +``` + +- *Type:* PipelineInfo + +The pipeline information. + +--- + +##### `repoInfo`Required + +```typescript +public readonly repoInfo: RepoInfo; +``` + +- *Type:* RepoInfo + +The repository information. + +--- + + ### VersionOutputs Construct for creating version outputs in CloudFormation and SSM Parameter Store. @@ -467,6 +676,116 @@ Repository URL. --- +### CustomEnvVarConfig + +Custom environment variable names for overriding defaults. + +#### Initializer + +```typescript +import { CustomEnvVarConfig } from 'cdk-devops' + +const customEnvVarConfig: CustomEnvVarConfig = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| branch | string | Custom environment variable for branch name. | +| commitHash | string | Custom environment variable for commit hash. | +| jobId | string | Custom environment variable for job ID. | +| jobUrl | string | Custom environment variable for job URL. | +| repoName | string | Custom environment variable for repository name. | +| repoOwner | string | Custom environment variable for repository owner. | +| triggeredBy | string | Custom environment variable for triggered by user. | + +--- + +##### `branch`Optional + +```typescript +public readonly branch: string; +``` + +- *Type:* string + +Custom environment variable for branch name. + +--- + +##### `commitHash`Optional + +```typescript +public readonly commitHash: string; +``` + +- *Type:* string + +Custom environment variable for commit hash. + +--- + +##### `jobId`Optional + +```typescript +public readonly jobId: string; +``` + +- *Type:* string + +Custom environment variable for job ID. + +--- + +##### `jobUrl`Optional + +```typescript +public readonly jobUrl: string; +``` + +- *Type:* string + +Custom environment variable for job URL. + +--- + +##### `repoName`Optional + +```typescript +public readonly repoName: string; +``` + +- *Type:* string + +Custom environment variable for repository name. + +--- + +##### `repoOwner`Optional + +```typescript +public readonly repoOwner: string; +``` + +- *Type:* string + +Custom environment variable for repository owner. + +--- + +##### `triggeredBy`Optional + +```typescript +public readonly triggeredBy: string; +``` + +- *Type:* string + +Custom environment variable for triggered by user. + +--- + ### GitInfo Git repository information. @@ -546,49 +865,515 @@ Short commit hash (typically 8 characters). public readonly commitsSinceTag: number; ``` -- *Type:* number +- *Type:* number + +Commit count since last tag. + +--- + +##### `tag`Optional + +```typescript +public readonly tag: string; +``` + +- *Type:* string + +Git tag (if on a tagged commit). + +--- + +### GitInfoProps + +Props for creating GitInfo. + +#### Initializer + +```typescript +import { GitInfoProps } from 'cdk-devops' + +const gitInfoProps: GitInfoProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| branch | string | Current branch name. | +| commitCount | number | Total commit count. | +| commitHash | string | Full commit hash. | +| commitsSinceTag | number | Commit count since last tag. | +| tag | string | Git tag (if on a tagged commit). | + +--- + +##### `branch`Required + +```typescript +public readonly branch: string; +``` + +- *Type:* string + +Current branch name. + +--- + +##### `commitCount`Required + +```typescript +public readonly commitCount: number; +``` + +- *Type:* number + +Total commit count. + +--- + +##### `commitHash`Required + +```typescript +public readonly commitHash: string; +``` + +- *Type:* string + +Full commit hash. + +--- + +##### `commitsSinceTag`Optional + +```typescript +public readonly commitsSinceTag: number; +``` + +- *Type:* number + +Commit count since last tag. + +--- + +##### `tag`Optional + +```typescript +public readonly tag: string; +``` + +- *Type:* string + +Git tag (if on a tagged commit). + +--- + +### GitTagConfig + +Git tag configuration for version extraction. + +#### Initializer + +```typescript +import { GitTagConfig } from 'cdk-devops' + +const gitTagConfig: GitTagConfig = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| countCommitsSince | boolean | Whether to count commits since the last tag. | +| pattern | string | Pattern to match git tags. | +| prefix | string | Prefix to strip from git tags (e.g., 'v' for tags like 'v1.2.3'). | + +--- + +##### `countCommitsSince`Optional + +```typescript +public readonly countCommitsSince: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Whether to count commits since the last tag. + +--- + +##### `pattern`Optional + +```typescript +public readonly pattern: string; +``` + +- *Type:* string +- *Default:* '*.*.*' + +Pattern to match git tags. + +--- + +##### `prefix`Optional + +```typescript +public readonly prefix: string; +``` + +- *Type:* string +- *Default:* 'v' + +Prefix to strip from git tags (e.g., 'v' for tags like 'v1.2.3'). + +--- + +### HierarchicalParametersOptions + +Options for hierarchical parameter configuration. + +#### Initializer + +```typescript +import { HierarchicalParametersOptions } from 'cdk-devops' + +const hierarchicalParametersOptions: HierarchicalParametersOptions = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| format | string | Output format. | +| includeCloudFormation | boolean | Whether to include CloudFormation outputs. | + +--- + +##### `format`Optional + +```typescript +public readonly format: string; +``` + +- *Type:* string +- *Default:* 'plain' + +Output format. + +--- + +##### `includeCloudFormation`Optional + +```typescript +public readonly includeCloudFormation: boolean; +``` + +- *Type:* boolean +- *Default:* false + +Whether to include CloudFormation outputs. + +--- + +### PackageJsonConfig + +Package.json version configuration. + +#### Initializer + +```typescript +import { PackageJsonConfig } from 'cdk-devops' + +const packageJsonConfig: PackageJsonConfig = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| includePrerelease | boolean | Whether to include prerelease identifiers. | + +--- + +##### `includePrerelease`Optional + +```typescript +public readonly includePrerelease: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Whether to include prerelease identifiers. + +--- + +### ParameterStoreOutputConfig + +SSM Parameter Store output configuration. + +#### Initializer + +```typescript +import { ParameterStoreOutputConfig } from 'cdk-devops' + +const parameterStoreOutputConfig: ParameterStoreOutputConfig = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| basePath | string | Base path for parameters (e.g., '/myapp/version'). | +| description | string | Description for the parameter. | +| enabled | boolean | Whether to create SSM parameters. | +| splitParameters | boolean | Whether to split version info into separate parameters. | + +--- + +##### `basePath`Optional + +```typescript +public readonly basePath: string; +``` + +- *Type:* string + +Base path for parameters (e.g., '/myapp/version'). + +--- + +##### `description`Optional + +```typescript +public readonly description: string; +``` + +- *Type:* string + +Description for the parameter. + +--- + +##### `enabled`Optional + +```typescript +public readonly enabled: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Whether to create SSM parameters. + +--- + +##### `splitParameters`Optional + +```typescript +public readonly splitParameters: boolean; +``` + +- *Type:* boolean +- *Default:* false + +Whether to split version info into separate parameters. + +--- + +### PipelineInfo + +Pipeline execution information. + +#### Initializer + +```typescript +import { PipelineInfo } from 'cdk-devops' + +const pipelineInfo: PipelineInfo = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| provider | CiProvider | CI/CD provider. | +| jobId | string | Job/build ID. | +| jobUrl | string | Job/build URL. | +| runNumber | string | Run number. | +| triggeredBy | string | User who triggered the job. | + +--- + +##### `provider`Required + +```typescript +public readonly provider: CiProvider; +``` + +- *Type:* CiProvider + +CI/CD provider. + +--- + +##### `jobId`Optional + +```typescript +public readonly jobId: string; +``` + +- *Type:* string + +Job/build ID. + +--- + +##### `jobUrl`Optional + +```typescript +public readonly jobUrl: string; +``` + +- *Type:* string + +Job/build URL. + +--- + +##### `runNumber`Optional + +```typescript +public readonly runNumber: string; +``` + +- *Type:* string + +Run number. + +--- + +##### `triggeredBy`Optional + +```typescript +public readonly triggeredBy: string; +``` + +- *Type:* string + +User who triggered the job. + +--- + +### PipelineInfoProps + +Props for creating PipelineInfo. + +#### Initializer + +```typescript +import { PipelineInfoProps } from 'cdk-devops' + +const pipelineInfoProps: PipelineInfoProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| provider | CiProvider | CI/CD provider. | +| jobId | string | Job/build ID. | +| jobUrl | string | Job/build URL. | +| runNumber | string | Run number. | +| triggeredBy | string | User who triggered the job. | + +--- + +##### `provider`Required + +```typescript +public readonly provider: CiProvider; +``` + +- *Type:* CiProvider + +CI/CD provider. + +--- + +##### `jobId`Optional + +```typescript +public readonly jobId: string; +``` + +- *Type:* string + +Job/build ID. + +--- + +##### `jobUrl`Optional + +```typescript +public readonly jobUrl: string; +``` + +- *Type:* string + +Job/build URL. + +--- + +##### `runNumber`Optional + +```typescript +public readonly runNumber: string; +``` + +- *Type:* string -Commit count since last tag. +Run number. --- -##### `tag`Optional +##### `triggeredBy`Optional ```typescript -public readonly tag: string; +public readonly triggeredBy: string; ``` - *Type:* string -Git tag (if on a tagged commit). +User who triggered the job. --- -### GitInfoProps +### RepoInfo -Props for creating GitInfo. +Repository information. -#### Initializer +#### Initializer ```typescript -import { GitInfoProps } from 'cdk-devops' +import { RepoInfo } from 'cdk-devops' -const gitInfoProps: GitInfoProps = { ... } +const repoInfo: RepoInfo = { ... } ``` #### Properties | **Name** | **Type** | **Description** | | --- | --- | --- | -| branch | string | Current branch name. | -| commitCount | number | Total commit count. | -| commitHash | string | Full commit hash. | -| commitsSinceTag | number | Commit count since last tag. | -| tag | string | Git tag (if on a tagged commit). | +| branch | string | Branch name. | +| commitHash | string | Commit hash. | +| provider | CiProvider | CI/CD provider (github/gitlab/codebuild). | +| repository | string | Repository name. | +| owner | string | Repository owner/organization. | --- -##### `branch`Required +##### `branch`Required ```typescript public readonly branch: string; @@ -596,269 +1381,225 @@ public readonly branch: string; - *Type:* string -Current branch name. +Branch name. --- -##### `commitCount`Required +##### `commitHash`Required ```typescript -public readonly commitCount: number; +public readonly commitHash: string; ``` -- *Type:* number +- *Type:* string -Total commit count. +Commit hash. --- -##### `commitHash`Required +##### `provider`Required ```typescript -public readonly commitHash: string; +public readonly provider: CiProvider; ``` -- *Type:* string +- *Type:* CiProvider -Full commit hash. +CI/CD provider (github/gitlab/codebuild). --- -##### `commitsSinceTag`Optional +##### `repository`Required ```typescript -public readonly commitsSinceTag: number; +public readonly repository: string; ``` -- *Type:* number +- *Type:* string -Commit count since last tag. +Repository name. --- -##### `tag`Optional +##### `owner`Optional ```typescript -public readonly tag: string; +public readonly owner: string; ``` - *Type:* string -Git tag (if on a tagged commit). +Repository owner/organization. --- -### GitTagConfig +### RepoInfoProps -Git tag configuration for version extraction. +Props for creating RepoInfo. -#### Initializer +#### Initializer ```typescript -import { GitTagConfig } from 'cdk-devops' +import { RepoInfoProps } from 'cdk-devops' -const gitTagConfig: GitTagConfig = { ... } +const repoInfoProps: RepoInfoProps = { ... } ``` #### Properties | **Name** | **Type** | **Description** | | --- | --- | --- | -| countCommitsSince | boolean | Whether to count commits since the last tag. | -| pattern | string | Pattern to match git tags. | -| prefix | string | Prefix to strip from git tags (e.g., 'v' for tags like 'v1.2.3'). | - ---- - -##### `countCommitsSince`Optional - -```typescript -public readonly countCommitsSince: boolean; -``` - -- *Type:* boolean -- *Default:* true - -Whether to count commits since the last tag. +| branch | string | Branch name. | +| commitHash | string | Commit hash. | +| provider | CiProvider | CI/CD provider. | +| repository | string | Repository name. | +| owner | string | Repository owner/organization. | --- -##### `pattern`Optional +##### `branch`Required ```typescript -public readonly pattern: string; +public readonly branch: string; ``` - *Type:* string -- *Default:* '*.*.*' -Pattern to match git tags. +Branch name. --- -##### `prefix`Optional +##### `commitHash`Required ```typescript -public readonly prefix: string; +public readonly commitHash: string; ``` - *Type:* string -- *Default:* 'v' -Prefix to strip from git tags (e.g., 'v' for tags like 'v1.2.3'). +Commit hash. --- -### HierarchicalParametersOptions - -Options for hierarchical parameter configuration. - -#### Initializer +##### `provider`Required ```typescript -import { HierarchicalParametersOptions } from 'cdk-devops' - -const hierarchicalParametersOptions: HierarchicalParametersOptions = { ... } +public readonly provider: CiProvider; ``` -#### Properties +- *Type:* CiProvider -| **Name** | **Type** | **Description** | -| --- | --- | --- | -| format | string | Output format. | -| includeCloudFormation | boolean | Whether to include CloudFormation outputs. | +CI/CD provider. --- -##### `format`Optional +##### `repository`Required ```typescript -public readonly format: string; +public readonly repository: string; ``` - *Type:* string -- *Default:* 'plain' -Output format. +Repository name. --- -##### `includeCloudFormation`Optional +##### `owner`Optional ```typescript -public readonly includeCloudFormation: boolean; +public readonly owner: string; ``` -- *Type:* boolean -- *Default:* false +- *Type:* string -Whether to include CloudFormation outputs. +Repository owner/organization. --- -### PackageJsonConfig +### StackMetadataProps -Package.json version configuration. +Props for StackMetadata construct. -#### Initializer +#### Initializer ```typescript -import { PackageJsonConfig } from 'cdk-devops' +import { StackMetadataProps } from 'cdk-devops' -const packageJsonConfig: PackageJsonConfig = { ... } +const stackMetadataProps: StackMetadataProps = { ... } ``` #### Properties | **Name** | **Type** | **Description** | | --- | --- | --- | -| includePrerelease | boolean | Whether to include prerelease identifiers. | +| customEnvVars | CustomEnvVarConfig | Custom environment variable names for overriding defaults Only used when repoInfo and pipelineInfo are not provided. | +| pipelineInfo | PipelineInfo | Pipeline information If not provided, will be extracted from environment variables. | +| pipelineMetadataKey | string | Metadata key for pipeline information. | +| repoInfo | RepoInfo | Repository information If not provided, will be extracted from environment variables. | +| repoMetadataKey | string | Metadata key for repository information. | --- -##### `includePrerelease`Optional - -```typescript -public readonly includePrerelease: boolean; -``` - -- *Type:* boolean -- *Default:* true - -Whether to include prerelease identifiers. - ---- - -### ParameterStoreOutputConfig - -SSM Parameter Store output configuration. - -#### Initializer +##### `customEnvVars`Optional ```typescript -import { ParameterStoreOutputConfig } from 'cdk-devops' - -const parameterStoreOutputConfig: ParameterStoreOutputConfig = { ... } +public readonly customEnvVars: CustomEnvVarConfig; ``` -#### Properties +- *Type:* CustomEnvVarConfig -| **Name** | **Type** | **Description** | -| --- | --- | --- | -| basePath | string | Base path for parameters (e.g., '/myapp/version'). | -| description | string | Description for the parameter. | -| enabled | boolean | Whether to create SSM parameters. | -| splitParameters | boolean | Whether to split version info into separate parameters. | +Custom environment variable names for overriding defaults Only used when repoInfo and pipelineInfo are not provided. --- -##### `basePath`Optional +##### `pipelineInfo`Optional ```typescript -public readonly basePath: string; +public readonly pipelineInfo: PipelineInfo; ``` -- *Type:* string +- *Type:* PipelineInfo -Base path for parameters (e.g., '/myapp/version'). +Pipeline information If not provided, will be extracted from environment variables. --- -##### `description`Optional +##### `pipelineMetadataKey`Optional ```typescript -public readonly description: string; +public readonly pipelineMetadataKey: string; ``` - *Type:* string +- *Default:* 'Pipeline' -Description for the parameter. +Metadata key for pipeline information. --- -##### `enabled`Optional +##### `repoInfo`Optional ```typescript -public readonly enabled: boolean; +public readonly repoInfo: RepoInfo; ``` -- *Type:* boolean -- *Default:* true +- *Type:* RepoInfo -Whether to create SSM parameters. +Repository information If not provided, will be extracted from environment variables. --- -##### `splitParameters`Optional +##### `repoMetadataKey`Optional ```typescript -public readonly splitParameters: boolean; +public readonly repoMetadataKey: string; ``` -- *Type:* boolean -- *Default:* false +- *Type:* string +- *Default:* 'Repo' -Whether to split version info into separate parameters. +Metadata key for repository information. --- @@ -1484,6 +2225,139 @@ Shorten a git commit hash to 8 characters. +### PipelineInfoHelper + +Helper class for working with pipeline information. + +#### Initializers + +```typescript +import { PipelineInfoHelper } from 'cdk-devops' + +new PipelineInfoHelper() +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | + +--- + + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| create | Create PipelineInfo from individual components. | +| fromEnvironment | Create PipelineInfo from environment variables (CI/CD context). | + +--- + +##### `create` + +```typescript +import { PipelineInfoHelper } from 'cdk-devops' + +PipelineInfoHelper.create(props: PipelineInfoProps) +``` + +Create PipelineInfo from individual components. + +###### `props`Required + +- *Type:* PipelineInfoProps + +--- + +##### `fromEnvironment` + +```typescript +import { PipelineInfoHelper } from 'cdk-devops' + +PipelineInfoHelper.fromEnvironment(customEnvVars?: CustomEnvVarConfig) +``` + +Create PipelineInfo from environment variables (CI/CD context). + +###### `customEnvVars`Optional + +- *Type:* CustomEnvVarConfig + +--- + + + +### RepoInfoHelper + +Helper class for working with repository information. + +#### Initializers + +```typescript +import { RepoInfoHelper } from 'cdk-devops' + +new RepoInfoHelper() +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | + +--- + + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| create | Create RepoInfo from individual components. | +| detectProvider | Detect CI/CD provider from environment variables. | +| fromEnvironment | Create RepoInfo from environment variables (CI/CD context). | + +--- + +##### `create` + +```typescript +import { RepoInfoHelper } from 'cdk-devops' + +RepoInfoHelper.create(props: RepoInfoProps) +``` + +Create RepoInfo from individual components. + +###### `props`Required + +- *Type:* RepoInfoProps + +--- + +##### `detectProvider` + +```typescript +import { RepoInfoHelper } from 'cdk-devops' + +RepoInfoHelper.detectProvider() +``` + +Detect CI/CD provider from environment variables. + +##### `fromEnvironment` + +```typescript +import { RepoInfoHelper } from 'cdk-devops' + +RepoInfoHelper.fromEnvironment(customEnvVars?: CustomEnvVarConfig) +``` + +Create RepoInfo from environment variables (CI/CD context). + +###### `customEnvVars`Optional + +- *Type:* CustomEnvVarConfig + +--- + + + ### VersionComputationStrategy Abstract base class for version computation strategies. @@ -2634,3 +3508,39 @@ Format string for version computation Supports placeholders: {git-tag}, {package --- +## Enums + +### CiProvider + +CI/CD provider type. + +#### Members + +| **Name** | **Description** | +| --- | --- | +| GITHUB | *No description.* | +| GITLAB | *No description.* | +| CODEBUILD | *No description.* | +| UNKNOWN | *No description.* | + +--- + +##### `GITHUB` + +--- + + +##### `GITLAB` + +--- + + +##### `CODEBUILD` + +--- + + +##### `UNKNOWN` + +--- + diff --git a/README.md b/README.md index b3fa7dd..6082ea7 100644 --- a/README.md +++ b/README.md @@ -1 +1,67 @@ -# replace this \ No newline at end of file +# cdk-devops + +A collection of AWS CDK constructs for DevOps automation, providing features like versioning and deployment metadata capabilities for your CDK applications. + +## Installation + +```bash +npm install cdk-devops +``` + +## Features + +### Versioning + +Compute and track version information for your CDK deployments. Supports multiple versioning strategies including git tags, package.json versions, commit counts, and custom formats. + +```typescript +import { VersioningStrategy, VersionInfo, VersionOutputs } from 'cdk-devops'; + +// Use a pre-built strategy +const strategy = VersioningStrategy.gitTag(); + +// Create version info from environment +const versionInfo = VersionInfo.fromEnvironment('1.0.0', 'production'); + +// Output version to CloudFormation and SSM Parameter Store +new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + cloudFormation: { enabled: true }, + parameterStore: { enabled: true, basePath: '/myapp/version' }, +}); +``` + +For detailed documentation, see [Versioning Guide](docs/VERSIONING.md). + +### Stack Metadata + +Add repository and CI/CD pipeline metadata to your CloudFormation stacks, making it easy to track deployments back to their source code and build pipelines. + +```typescript +import { StackMetadata } from 'cdk-devops'; + +// Automatically extracts repo and pipeline info from CI/CD environment +new StackMetadata(stack, 'Metadata'); +``` + +Works out of the box with GitHub Actions, GitLab CI, AWS CodeBuild, and generic CI/CD systems. + +For detailed documentation, see [Metadata Guide](docs/METADATA.md). + +## CLI Tools + +### compute-version + +A CLI utility for computing versions based on git information: + +```bash +npx compute-version --strategy git-tag --environment production +``` + +## API Reference + +For complete API documentation, see [API.md](docs/API.md). + +## License + +Apache-2.0 diff --git a/docs/METADATA.md b/docs/METADATA.md new file mode 100644 index 0000000..050da9f --- /dev/null +++ b/docs/METADATA.md @@ -0,0 +1,211 @@ +# Stack Metadata Usage Guide + +The `StackMetadata` construct adds repository and CI/CD pipeline metadata to your CloudFormation stacks, making it easy to track deployments back to their source code and build pipelines. + +## Quick Start + +### Basic Usage (Automatic Detection) + +The simplest way to use the construct is to let it automatically detect and extract information from your CI/CD environment: + +```typescript +import * as cdk from 'aws-cdk-lib'; +import { StackMetadata } from 'cdk-devops'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'MyStack'); + +// Automatically extracts repo and pipeline info from environment variables +new StackMetadata(stack, 'Metadata'); + +app.synth(); +``` + +This works out of the box with: +- **GitHub Actions** +- **GitLab CI** +- **AWS CodeBuild** +- Generic CI/CD systems + +### Using Static Factory Method + +You can also use the `fromEnvironment` static method: + +```typescript +StackMetadata.fromEnvironment(stack, 'Metadata'); +``` + +## Advanced Usage + +### Custom Environment Variables + +If your CI/CD system uses non-standard environment variable names, you can override them: + +```typescript +new StackMetadata(stack, 'Metadata', { + customEnvVars: { + repoOwner: 'MY_CUSTOM_OWNER_VAR', + repoName: 'MY_CUSTOM_REPO_VAR', + branch: 'MY_CUSTOM_BRANCH_VAR', + commitHash: 'MY_CUSTOM_COMMIT_VAR', + jobId: 'MY_CUSTOM_JOB_ID', + jobUrl: 'MY_CUSTOM_JOB_URL', + triggeredBy: 'MY_CUSTOM_USER', + }, +}); +``` + +### Manual Configuration + +For complete control, provide the information manually: + +```typescript +import { CiProvider } from 'cdk-devops'; + +new StackMetadata(stack, 'Metadata', { + repoInfo: { + provider: CiProvider.GITHUB, + owner: 'my-organization', + repository: 'my-repository', + branch: 'main', + commitHash: 'abc123def456789', + }, + pipelineInfo: { + provider: CiProvider.GITHUB, + jobId: '12345', + jobUrl: 'https://github.com/my-org/my-repo/actions/runs/12345', + triggeredBy: 'developer@example.com', + runNumber: '42', + }, +}); +``` + +### Custom Metadata Keys + +By default, the construct uses "Repo" and "Pipeline" as metadata keys. You can customize these: + +```typescript +new StackMetadata(stack, 'Metadata', { + repoMetadataKey: 'SourceCode', + pipelineMetadataKey: 'BuildInfo', +}); +``` + +## Supported CI/CD Platforms + +### GitHub Actions + +Automatically detected when `GITHUB_ACTIONS=true`. Extracts: + +**Repository Info:** +- Owner from `GITHUB_REPOSITORY_OWNER` or `GITHUB_REPOSITORY` +- Repository name from `GITHUB_REPOSITORY` +- Branch from `GITHUB_REF` or `GITHUB_HEAD_REF` +- Commit hash from `GITHUB_SHA` + +**Pipeline Info:** +- Job ID from `GITHUB_RUN_ID` +- Job URL (automatically constructed) +- Triggered by from `GITHUB_ACTOR` +- Run number from `GITHUB_RUN_NUMBER` + +### GitLab CI + +Automatically detected when `GITLAB_CI=true`. Extracts: + +**Repository Info:** +- Owner from `CI_PROJECT_NAMESPACE` +- Repository name from `CI_PROJECT_NAME` +- Branch from `CI_COMMIT_REF_NAME` +- Commit hash from `CI_COMMIT_SHA` + +**Pipeline Info:** +- Job ID from `CI_JOB_ID` or `CI_PIPELINE_ID` +- Job URL from `CI_JOB_URL` or `CI_PIPELINE_URL` +- Triggered by from `GITLAB_USER_LOGIN` or `CI_COMMIT_AUTHOR` +- Run number from `CI_PIPELINE_IID` + +### AWS CodeBuild + +Automatically detected when `CODEBUILD_BUILD_ID` is present. Extracts: + +**Repository Info:** +- Owner and repository parsed from `CODEBUILD_SOURCE_REPO_URL` +- Branch from `CODEBUILD_WEBHOOK_HEAD_REF` or `CODEBUILD_SOURCE_VERSION` +- Commit hash from `CODEBUILD_RESOLVED_SOURCE_VERSION` + +**Pipeline Info:** +- Job ID from `CODEBUILD_BUILD_ID` +- Job URL (automatically constructed from ARN) +- Triggered by from `CODEBUILD_INITIATOR` +- Build number from `CODEBUILD_BUILD_NUMBER` + +### Generic CI/CD + +Fallback support for any CI/CD system using standard environment variables: + +- `REPO_OWNER`, `REPO_NAME`, `REPOSITORY` +- `BRANCH`, `GIT_BRANCH` +- `COMMIT_HASH`, `GIT_COMMIT` +- `BUILD_ID`, `JOB_ID` +- `BUILD_URL`, `JOB_URL` +- `BUILD_USER`, `USER` + +## Output + +The construct adds metadata to your CloudFormation template: + +```json +{ + "Metadata": { + "Repo": { + "provider": "github", + "owner": "open-constructs", + "repository": "cdk-devops", + "branch": "main", + "commitHash": "abc123def456789" + }, + "Pipeline": { + "provider": "github", + "jobId": "12345", + "jobUrl": "https://github.com/open-constructs/cdk-devops/actions/runs/12345", + "triggeredBy": "developer@example.com", + "runNumber": "42" + } + } +} +``` + +## Accessing Metadata in Code + +You can access the extracted metadata programmatically: + +```typescript +const metadata = new StackMetadata(stack, 'Metadata'); + +const repoInfo = metadata.repoInfo; +console.log(`Deployed from ${repoInfo.owner}/${repoInfo.repository}@${repoInfo.commitHash}`); + +const pipelineInfo = metadata.pipelineInfo; +console.log(`Build triggered by ${pipelineInfo.triggeredBy}`); +``` + +## API Reference + +For complete API documentation, see [API.md](./API.md). + +### Main Classes + +- **`StackMetadata`** - CDK construct for adding metadata to stacks +- **`RepoInfoHelper`** - Helper for extracting repository information +- **`PipelineInfoHelper`** - Helper for extracting pipeline information + +### Interfaces + +- **`RepoInfo`** - Repository information interface +- **`PipelineInfo`** - Pipeline information interface +- **`CustomEnvVarConfig`** - Custom environment variable configuration + +### Enums + +- **`CiProvider`** - CI/CD provider enumeration (GITHUB, GITLAB, CODEBUILD, UNKNOWN) diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md new file mode 100644 index 0000000..2b80ede --- /dev/null +++ b/docs/VERSIONING.md @@ -0,0 +1,307 @@ +# Versioning Guide + +The versioning module provides tools for computing, tracking, and outputting version information for your CDK deployments. + +## Quick Start + +```typescript +import * as cdk from 'aws-cdk-lib'; +import { VersionInfo, VersionOutputs, VersioningStrategy } from 'cdk-devops'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'MyStack'); + +// Create version info from environment +const versionInfo = VersionInfo.fromEnvironment('1.0.0', 'production'); + +// Output version information +new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + cloudFormation: { enabled: true }, + parameterStore: { enabled: true, basePath: '/myapp/version' }, +}); + +app.synth(); +``` + +## Versioning Strategies + +The module provides several pre-built versioning strategies: + +### Git Tag Strategy + +Uses git tags as the version source. If the current commit is not on a tag, it appends the commit count since the last tag. + +```typescript +const strategy = VersioningStrategy.gitTag({ + prefix: 'v', // Strip 'v' prefix from tags (e.g., v1.2.3 -> 1.2.3) + pattern: '*.*.*', // Match semver tags + countCommitsSince: true, +}); +``` + +### Package.json Strategy + +Uses the version from package.json: + +```typescript +const strategy = VersioningStrategy.packageJson({ + includePrerelease: true, +}); +``` + +### Commit Count Strategy + +Uses the total commit count as version: + +```typescript +const strategy = VersioningStrategy.commitCount({ + mode: 'all', // 'all', 'branch', or 'since-tag' + padding: 5, // Pad to 5 digits (e.g., 00042) +}); +``` + +### Commit Hash Strategy + +Uses the short commit hash as version: + +```typescript +const strategy = VersioningStrategy.commitHash(); +// Result: abc12345 +``` + +### Build Number Strategy + +Uses commit count and hash: + +```typescript +const strategy = VersioningStrategy.buildNumber(); +// Result: build-42-abc12345 +``` + +### Git Tag with Dev Versions + +Combines git tags with dev versions for non-tagged commits: + +```typescript +const strategy = VersioningStrategy.gitTagWithDevVersions(); +// On tag: 1.2.3 +// After tag: 1.2.3-dev.5 +``` + +### Package with Branch + +Combines package version with branch and commit info: + +```typescript +const strategy = VersioningStrategy.packageWithBranch(); +// Result: 1.0.0-main.42 +``` + +### Semantic with Patch + +Appends commit count as patch version: + +```typescript +const strategy = VersioningStrategy.semanticWithPatch(); +// Result: 1.0.42 +``` + +### Custom Strategy + +Create a custom format using placeholders: + +```typescript +const strategy = VersioningStrategy.create( + '{package-version}-{branch}.{commit-count}+{commit-hash:8}', + { + commitCount: { mode: 'all' }, + packageJson: { includePrerelease: true }, + } +); +``` + +**Available placeholders:** +- `{git-tag}` - Git tag (if available) +- `{package-version}` - Version from package.json +- `{commit-count}` - Commit count +- `{commit-hash}` or `{commit-hash:N}` - Full or N-character commit hash +- `{branch}` - Current branch name +- `{build-number}` - Build number from environment + +## Version Information + +The `VersionInfo` class holds comprehensive version and deployment information: + +```typescript +const versionInfo = VersionInfo.fromEnvironment('1.0.0', 'production'); + +console.log(versionInfo.version); // Computed version +console.log(versionInfo.commitHash); // Full commit hash +console.log(versionInfo.shortCommitHash); // 8-char commit hash +console.log(versionInfo.branch); // Branch name +console.log(versionInfo.tag); // Git tag (if on a tag) +console.log(versionInfo.commitCount); // Total commit count +console.log(versionInfo.environment); // Environment name +console.log(versionInfo.deploymentTime); // Deployment timestamp +console.log(versionInfo.deploymentUser); // User who triggered deployment +``` + +### Creating Version Info + +**From environment:** + +```typescript +const versionInfo = VersionInfo.fromEnvironment('1.0.0', 'production'); +``` + +**With builder:** + +```typescript +import { VersionInfoBuilder, GitInfoHelper } from 'cdk-devops'; + +const versionInfo = new VersionInfoBuilder() + .withVersion('1.0.0') + .withGitInfo(GitInfoHelper.fromEnvironment()) + .withEnvironment('production') + .withDeploymentUser('ci-bot') + .buildVersionInfo(); +``` + +**From props:** + +```typescript +const versionInfo = VersionInfo.create({ + version: '1.0.0', + gitInfo: { + commitHash: 'abc123def456789', + shortCommitHash: 'abc123de', + branch: 'main', + commitCount: 42, + }, + environment: 'production', +}); +``` + +## Version Outputs + +The `VersionOutputs` construct creates CloudFormation outputs and SSM parameters for version information. + +### CloudFormation Outputs + +```typescript +new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + cloudFormation: { + enabled: true, + export: true, // Export for cross-stack references + exportNameTemplate: '{environment}-version', + }, +}); +``` + +Creates outputs for: +- Version string +- Commit hash +- Branch +- Commit count +- Deployment time +- Environment +- Tag (if available) +- Package version (if available) + +### SSM Parameter Store + +```typescript +new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + parameterStore: { + enabled: true, + basePath: '/myapp/production/version', + splitParameters: false, // Single JSON parameter + description: 'Version information', + }, +}); +``` + +With `splitParameters: true`, creates individual parameters: +- `/myapp/production/version/version` +- `/myapp/production/version/commit-hash` +- `/myapp/production/version/branch` +- `/myapp/production/version/commit-count` +- `/myapp/production/version/deployment-time` +- `/myapp/production/version/environment` + +### Using Factory Methods + +```typescript +import { VersioningOutputsFactory } from 'cdk-devops'; + +// Standard: CloudFormation + single SSM parameter +const config = VersioningOutputsFactory.standard(); + +// CloudFormation only +const config = VersioningOutputsFactory.cloudFormationOnly({ + exportName: 'MyApp-Version', +}); + +// Hierarchical SSM parameters +const config = VersioningOutputsFactory.hierarchicalParameters('/myapp/version', { + includeCloudFormation: true, +}); + +// Minimal: CloudFormation outputs only +const config = VersioningOutputsFactory.minimal(); +``` + +## CLI Usage + +The `compute-version` CLI computes version information from git: + +```bash +# Basic usage +npx compute-version --environment production + +# With strategy +npx compute-version --strategy git-tag --environment staging + +# Output as JSON +npx compute-version --format json +``` + +## Environment Variables + +The module automatically extracts information from these environment variables: + +| Variable | Description | +|----------|-------------| +| `GITHUB_ACTOR` | GitHub Actions user | +| `GITLAB_USER_LOGIN` | GitLab CI user | +| `GITHUB_REPOSITORY` | GitHub repository | +| `GITHUB_RUN_NUMBER` | GitHub Actions run number | +| `CODEBUILD_BUILD_ID` | AWS CodeBuild build ID | +| `BUILD_NUMBER` | Generic build number | +| `PACKAGE_VERSION` | Package version | +| `DEPLOYMENT_TIME` | Deployment timestamp | +| `PIPELINE_VERSION` | Pipeline version/execution ID | + +## API Reference + +For complete API documentation, see [API.md](./API.md). + +### Main Classes + +- **`VersionInfo`** - Version information container +- **`VersionInfoBuilder`** - Builder for VersionInfo +- **`VersioningStrategy`** - Versioning strategy configuration +- **`VersionOutputs`** - CDK construct for version outputs +- **`VersioningOutputsFactory`** - Factory for output configurations +- **`GitInfoHelper`** - Helper for extracting git information + +### Interfaces + +- **`IVersionInfo`** - Version information interface +- **`IVersioningStrategy`** - Strategy interface +- **`VersioningOutputsConfig`** - Output configuration +- **`CloudFormationOutputConfig`** - CloudFormation output options +- **`ParameterStoreOutputConfig`** - SSM parameter options diff --git a/src/index.ts b/src/index.ts index e384345..b5aa183 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,5 @@ // Versioning module -export * from './versioning'; \ No newline at end of file +export * from './versioning'; + +// Metadata module +export * from './metadata'; \ No newline at end of file diff --git a/src/metadata/index.ts b/src/metadata/index.ts new file mode 100644 index 0000000..9024331 --- /dev/null +++ b/src/metadata/index.ts @@ -0,0 +1,11 @@ +// Types +export * from './types'; + +// Repository Information +export * from './repo-info'; + +// Pipeline Information +export * from './pipeline-info'; + +// CDK Construct +export * from './stack-metadata'; diff --git a/src/metadata/pipeline-info.ts b/src/metadata/pipeline-info.ts new file mode 100644 index 0000000..53c994a --- /dev/null +++ b/src/metadata/pipeline-info.ts @@ -0,0 +1,170 @@ +import { RepoInfoHelper } from './repo-info'; +import { CiProvider, CustomEnvVarConfig, PipelineInfo } from './types'; + +/** + * Props for creating PipelineInfo + */ +export interface PipelineInfoProps { + /** + * CI/CD provider + */ + readonly provider: CiProvider; + + /** + * Job/build ID + */ + readonly jobId?: string; + + /** + * Job/build URL + */ + readonly jobUrl?: string; + + /** + * User who triggered the job + */ + readonly triggeredBy?: string; + + /** + * Run number + */ + readonly runNumber?: string; + +} + +/** + * Helper class for working with pipeline information + */ +export class PipelineInfoHelper { + /** + * Create PipelineInfo from individual components + */ + public static create(props: PipelineInfoProps): PipelineInfo { + return { + provider: props.provider, + jobId: props.jobId, + jobUrl: props.jobUrl, + triggeredBy: props.triggeredBy, + runNumber: props.runNumber, + }; + } + + /** + * Create PipelineInfo from environment variables (CI/CD context) + */ + public static fromEnvironment(customEnvVars?: CustomEnvVarConfig): PipelineInfo { + const provider = RepoInfoHelper.detectProvider(); + + switch (provider) { + case CiProvider.GITHUB: + return this.fromGitHubEnvironment(customEnvVars); + case CiProvider.GITLAB: + return this.fromGitLabEnvironment(customEnvVars); + case CiProvider.CODEBUILD: + return this.fromCodeBuildEnvironment(customEnvVars); + default: + return this.fromGenericEnvironment(customEnvVars); + } + } + + /** + * Extract pipeline information from GitHub Actions environment + */ + private static fromGitHubEnvironment(customEnvVars?: CustomEnvVarConfig): PipelineInfo { + const repository = process.env.GITHUB_REPOSITORY || ''; + const runId = process.env.GITHUB_RUN_ID || ''; + const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com'; + + const jobUrl = repository && runId + ? `${serverUrl}/${repository}/actions/runs/${runId}` + : this.getEnvVar(customEnvVars?.jobUrl); + + return this.create({ + provider: CiProvider.GITHUB, + jobId: this.getEnvVar(customEnvVars?.jobId, 'GITHUB_RUN_ID'), + jobUrl, + triggeredBy: this.getEnvVar(customEnvVars?.triggeredBy, 'GITHUB_ACTOR', 'GITHUB_TRIGGERING_ACTOR'), + runNumber: process.env.GITHUB_RUN_NUMBER, + }); + } + + /** + * Extract pipeline information from GitLab CI environment + */ + private static fromGitLabEnvironment(customEnvVars?: CustomEnvVarConfig): PipelineInfo { + return this.create({ + provider: CiProvider.GITLAB, + jobId: this.getEnvVar(customEnvVars?.jobId, 'CI_JOB_ID', 'CI_PIPELINE_ID'), + jobUrl: this.getEnvVar(customEnvVars?.jobUrl, 'CI_JOB_URL', 'CI_PIPELINE_URL'), + triggeredBy: this.getEnvVar( + customEnvVars?.triggeredBy, + 'GITLAB_USER_LOGIN', + 'CI_COMMIT_AUTHOR', + 'GITLAB_USER_NAME', + ), + runNumber: process.env.CI_PIPELINE_IID, + }); + } + + /** + * Extract pipeline information from AWS CodeBuild environment + */ + private static fromCodeBuildEnvironment(customEnvVars?: CustomEnvVarConfig): PipelineInfo { + const buildId = process.env.CODEBUILD_BUILD_ID || ''; + const region = this.extractRegionFromArn(process.env.CODEBUILD_BUILD_ARN); + const buildNumber = process.env.CODEBUILD_BUILD_NUMBER; + + // Construct CodeBuild console URL + const jobUrl = buildId && region + ? `https://${region}.console.aws.amazon.com/codesuite/codebuild/projects/${buildId.split(':')[0]}/build/${encodeURIComponent(buildId)}` + : this.getEnvVar(customEnvVars?.jobUrl); + + return this.create({ + provider: CiProvider.CODEBUILD, + jobId: this.getEnvVar(customEnvVars?.jobId, 'CODEBUILD_BUILD_ID'), + jobUrl, + triggeredBy: this.getEnvVar(customEnvVars?.triggeredBy, 'CODEBUILD_INITIATOR'), + runNumber: buildNumber, + }); + } + + /** + * Extract pipeline information from generic environment variables + */ + private static fromGenericEnvironment(customEnvVars?: CustomEnvVarConfig): PipelineInfo { + return this.create({ + provider: CiProvider.UNKNOWN, + jobId: this.getEnvVar(customEnvVars?.jobId, 'BUILD_ID', 'JOB_ID'), + jobUrl: this.getEnvVar(customEnvVars?.jobUrl, 'BUILD_URL', 'JOB_URL'), + triggeredBy: this.getEnvVar(customEnvVars?.triggeredBy, 'BUILD_USER', 'USER'), + runNumber: process.env.BUILD_NUMBER, + }); + } + + /** + * Get environment variable value with fallbacks + */ + private static getEnvVar(customVar?: string, ...fallbackVars: string[]): string | undefined { + if (customVar && process.env[customVar]) { + return process.env[customVar]; + } + for (const varName of fallbackVars) { + if (process.env[varName]) { + return process.env[varName]; + } + } + return undefined; + } + + /** + * Extract AWS region from ARN + */ + private static extractRegionFromArn(arn?: string): string | undefined { + if (!arn) { + return undefined; + } + // ARN format: arn:aws:codebuild:region:account-id:build/project-name:build-id + const parts = arn.split(':'); + return parts.length > 3 ? parts[3] : undefined; + } +} diff --git a/src/metadata/repo-info.ts b/src/metadata/repo-info.ts new file mode 100644 index 0000000..1b3b59d --- /dev/null +++ b/src/metadata/repo-info.ts @@ -0,0 +1,228 @@ +import { CiProvider, CustomEnvVarConfig, RepoInfo } from './types'; + +/** + * Props for creating RepoInfo + */ +export interface RepoInfoProps { + /** + * CI/CD provider + */ + readonly provider: CiProvider; + + /** + * Repository owner/organization + */ + readonly owner?: string; + + /** + * Repository name + */ + readonly repository: string; + + /** + * Branch name + */ + readonly branch: string; + + /** + * Commit hash + */ + readonly commitHash: string; +} + +/** + * Helper class for working with repository information + */ +export class RepoInfoHelper { + /** + * Create RepoInfo from individual components + */ + public static create(props: RepoInfoProps): RepoInfo { + return { + provider: props.provider, + owner: props.owner, + repository: props.repository, + branch: props.branch, + commitHash: props.commitHash, + }; + } + + /** + * Create RepoInfo from environment variables (CI/CD context) + */ + public static fromEnvironment(customEnvVars?: CustomEnvVarConfig): RepoInfo { + const provider = this.detectProvider(); + + switch (provider) { + case CiProvider.GITHUB: + return this.fromGitHubEnvironment(customEnvVars); + case CiProvider.GITLAB: + return this.fromGitLabEnvironment(customEnvVars); + case CiProvider.CODEBUILD: + return this.fromCodeBuildEnvironment(customEnvVars); + default: + return this.fromGenericEnvironment(customEnvVars); + } + } + + /** + * Detect CI/CD provider from environment variables + */ + public static detectProvider(): CiProvider { + if (process.env.GITHUB_ACTIONS === 'true' || process.env.GITHUB_SHA) { + return CiProvider.GITHUB; + } + if (process.env.GITLAB_CI === 'true' || process.env.CI_COMMIT_SHA) { + return CiProvider.GITLAB; + } + if (process.env.CODEBUILD_BUILD_ID || process.env.CODEBUILD_BUILD_ARN) { + return CiProvider.CODEBUILD; + } + return CiProvider.UNKNOWN; + } + + /** + * Extract repository information from GitHub Actions environment + */ + private static fromGitHubEnvironment(customEnvVars?: CustomEnvVarConfig): RepoInfo { + const repository = this.getEnvVar(customEnvVars?.repoName, 'GITHUB_REPOSITORY') || 'unknown/unknown'; + const [owner, repo] = repository.includes('/') + ? repository.split('/', 2) + : ['unknown', repository]; + + return this.create({ + provider: CiProvider.GITHUB, + owner: this.getEnvVar(customEnvVars?.repoOwner, 'GITHUB_REPOSITORY_OWNER') || owner, + repository: repo, + branch: this.extractBranchName( + this.getEnvVar(customEnvVars?.branch, 'GITHUB_REF') || '', + process.env.GITHUB_HEAD_REF, + ), + commitHash: this.getEnvVar(customEnvVars?.commitHash, 'GITHUB_SHA') || 'unknown', + }); + } + + /** + * Extract repository information from GitLab CI environment + */ + private static fromGitLabEnvironment(customEnvVars?: CustomEnvVarConfig): RepoInfo { + // Prefer CI_PROJECT_NAME if available, otherwise parse from CI_PROJECT_PATH + const projectName = this.getEnvVar(customEnvVars?.repoName, 'CI_PROJECT_NAME'); + const projectPath = process.env.CI_PROJECT_PATH || 'unknown/unknown'; + const [owner, repoFromPath] = projectPath.includes('/') + ? projectPath.split('/', 2) + : ['unknown', projectPath]; + + return this.create({ + provider: CiProvider.GITLAB, + owner: this.getEnvVar(customEnvVars?.repoOwner, 'CI_PROJECT_NAMESPACE') || owner, + repository: projectName || repoFromPath || 'unknown', + branch: this.getEnvVar(customEnvVars?.branch, 'CI_COMMIT_REF_NAME') || 'unknown', + commitHash: this.getEnvVar(customEnvVars?.commitHash, 'CI_COMMIT_SHA') || 'unknown', + }); + } + + /** + * Extract repository information from AWS CodeBuild environment + */ + private static fromCodeBuildEnvironment(customEnvVars?: CustomEnvVarConfig): RepoInfo { + // CodeBuild source information from CODEBUILD_SOURCE_REPO_URL + const repoUrl = process.env.CODEBUILD_SOURCE_REPO_URL || ''; + const { owner, repository } = this.parseRepoUrl(repoUrl); + + return this.create({ + provider: CiProvider.CODEBUILD, + owner: this.getEnvVar(customEnvVars?.repoOwner) || owner, + repository: this.getEnvVar(customEnvVars?.repoName) || repository || 'unknown', + branch: this.getEnvVar(customEnvVars?.branch, 'CODEBUILD_WEBHOOK_HEAD_REF') + || this.getEnvVar(customEnvVars?.branch, 'CODEBUILD_SOURCE_VERSION') + || 'unknown', + commitHash: this.getEnvVar(customEnvVars?.commitHash, 'CODEBUILD_RESOLVED_SOURCE_VERSION') + || this.getEnvVar(customEnvVars?.commitHash, 'CODEBUILD_SOURCE_VERSION') + || 'unknown', + }); + } + + /** + * Extract repository information from generic environment variables + */ + private static fromGenericEnvironment(customEnvVars?: CustomEnvVarConfig): RepoInfo { + return this.create({ + provider: CiProvider.UNKNOWN, + owner: this.getEnvVar(customEnvVars?.repoOwner, 'REPO_OWNER'), + repository: this.getEnvVar(customEnvVars?.repoName, 'REPO_NAME', 'REPOSITORY') || 'unknown', + branch: this.getEnvVar(customEnvVars?.branch, 'BRANCH', 'GIT_BRANCH') || 'unknown', + commitHash: this.getEnvVar(customEnvVars?.commitHash, 'COMMIT_HASH', 'GIT_COMMIT') || 'unknown', + }); + } + + /** + * Get environment variable value with fallbacks + */ + private static getEnvVar(customVar?: string, ...fallbackVars: string[]): string | undefined { + if (customVar && process.env[customVar]) { + return process.env[customVar]; + } + for (const varName of fallbackVars) { + if (process.env[varName]) { + return process.env[varName]; + } + } + return undefined; + } + + /** + * Extract branch name from Git ref + */ + private static extractBranchName(ref: string, headRef?: string): string { + // For pull requests, prefer head ref + if (headRef) { + return headRef; + } + + // Extract from refs/heads/branch-name + if (ref.startsWith('refs/heads/')) { + return ref.substring('refs/heads/'.length); + } + + // Extract from refs/tags/tag-name + if (ref.startsWith('refs/tags/')) { + return ref.substring('refs/tags/'.length); + } + + // Extract from refs/pull/123/merge + if (ref.includes('/pull/')) { + return ref; + } + + return ref || 'unknown'; + } + + /** + * Parse repository owner and name from URL + */ + private static parseRepoUrl(url: string): { owner?: string; repository?: string } { + if (!url) { + return { owner: undefined, repository: undefined }; + } + + // Handle GitHub/GitLab URLs like https://github.com/owner/repo.git + const githubMatch = url.match(/github\.com[/:]([^/]+)\/([^/.]+)/); + if (githubMatch) { + return { owner: githubMatch[1], repository: githubMatch[2] }; + } + + const gitlabMatch = url.match(/gitlab\.com[/:]([^/]+)\/([^/.]+)/); + if (gitlabMatch) { + return { owner: gitlabMatch[1], repository: gitlabMatch[2] }; + } + + // Handle AWS CodeCommit URLs like https://git-codecommit.region.amazonaws.com/v1/repos/repo-name + const codecommitMatch = url.match(/codecommit\.[\w-]+\.amazonaws\.com\/v1\/repos\/([^/.]+)/); + if (codecommitMatch) { + return { owner: undefined, repository: codecommitMatch[1] }; + } + + return { owner: undefined, repository: undefined }; + } +} diff --git a/src/metadata/stack-metadata.ts b/src/metadata/stack-metadata.ts new file mode 100644 index 0000000..08cf8b1 --- /dev/null +++ b/src/metadata/stack-metadata.ts @@ -0,0 +1,143 @@ +import { Stack } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { PipelineInfoHelper } from './pipeline-info'; +import { RepoInfoHelper } from './repo-info'; +import { CustomEnvVarConfig, PipelineInfo, RepoInfo } from './types'; + +/** + * Props for StackMetadata construct + */ +export interface StackMetadataProps { + /** + * Repository information + * If not provided, will be extracted from environment variables + */ + readonly repoInfo?: RepoInfo; + + /** + * Pipeline information + * If not provided, will be extracted from environment variables + */ + readonly pipelineInfo?: PipelineInfo; + + /** + * Custom environment variable names for overriding defaults + * Only used when repoInfo and pipelineInfo are not provided + */ + readonly customEnvVars?: CustomEnvVarConfig; + + /** + * Metadata key for repository information + * @default 'Repo' + */ + readonly repoMetadataKey?: string; + + /** + * Metadata key for pipeline information + * @default 'Pipeline' + */ + readonly pipelineMetadataKey?: string; +} + +/** + * Construct for adding repository and pipeline metadata to a CloudFormation stack + * + * This construct adds metadata to the stack containing information about the + * repository (provider, owner, repository name, branch, commit) and the + * CI/CD pipeline (job ID, job URL, triggered by, workflow name). + * + * @example + * + * // Automatically extract from environment variables + * new StackMetadata(this, 'Metadata'); + * + * // Use custom environment variable names + * new StackMetadata(this, 'Metadata', { + * customEnvVars: { + * repoOwner: 'MY_REPO_OWNER', + * repoName: 'MY_REPO_NAME', + * branch: 'MY_BRANCH', + * commitHash: 'MY_COMMIT_HASH', + * }, + * }); + * + * // Manually provide information + * new StackMetadata(this, 'Metadata', { + * repoInfo: { + * provider: CiProvider.GITHUB, + * owner: 'my-org', + * repository: 'my-repo', + * branch: 'main', + * commitHash: 'abc123', + * }, + * pipelineInfo: { + * provider: CiProvider.GITHUB, + * jobId: '12345', + * jobUrl: 'https://github.com/my-org/my-repo/actions/runs/12345', + * triggeredBy: 'user@example.com', + * }, + * }); + */ +export class StackMetadata extends Construct { + /** + * Create a new StackMetadata construct that extracts information from environment variables + */ + public static fromEnvironment(scope: Construct, id: string, customEnvVars?: CustomEnvVarConfig): StackMetadata { + return new StackMetadata(scope, id, { + customEnvVars, + }); + } + + /** + * The repository information + */ + public readonly repoInfo: RepoInfo; + + /** + * The pipeline information + */ + public readonly pipelineInfo: PipelineInfo; + + constructor(scope: Construct, id: string, props?: StackMetadataProps) { + super(scope, id); + + // Extract or use provided repository information + this.repoInfo = props?.repoInfo || RepoInfoHelper.fromEnvironment(props?.customEnvVars); + + // Extract or use provided pipeline information + this.pipelineInfo = props?.pipelineInfo || PipelineInfoHelper.fromEnvironment(props?.customEnvVars); + + // Add metadata to the stack + this.addMetadataToStack(props); + } + + /** + * Add repository and pipeline metadata to the stack + */ + private addMetadataToStack(props?: StackMetadataProps): void { + const stack = Stack.of(this); + const repoKey = props?.repoMetadataKey || 'Repo'; + const pipelineKey = props?.pipelineMetadataKey || 'Pipeline'; + + // Add repository metadata + stack.addMetadata(repoKey, { + provider: this.repoInfo.provider, + owner: this.repoInfo.owner, + repository: this.repoInfo.repository, + branch: this.repoInfo.branch, + commitHash: this.repoInfo.commitHash, + }); + + // Add pipeline metadata + const pipelineMetadata: Record = { + provider: this.pipelineInfo.provider, + jobId: this.pipelineInfo.jobId, + jobUrl: this.pipelineInfo.jobUrl, + triggeredBy: this.pipelineInfo.triggeredBy, + runNumber: this.pipelineInfo.runNumber, + }; + + stack.addMetadata(pipelineKey, pipelineMetadata); + } + +} diff --git a/src/metadata/types.ts b/src/metadata/types.ts new file mode 100644 index 0000000..a1ad59e --- /dev/null +++ b/src/metadata/types.ts @@ -0,0 +1,111 @@ +/** + * CI/CD provider type + */ +export enum CiProvider { + GITHUB = 'github', + GITLAB = 'gitlab', + CODEBUILD = 'codebuild', + UNKNOWN = 'unknown', +} + +/** + * Repository information + */ +export interface RepoInfo { + /** + * CI/CD provider (github/gitlab/codebuild) + */ + readonly provider: CiProvider; + + /** + * Repository owner/organization + */ + readonly owner?: string; + + /** + * Repository name + */ + readonly repository: string; + + /** + * Branch name + */ + readonly branch: string; + + /** + * Commit hash + */ + readonly commitHash: string; +} + +/** + * Pipeline execution information + */ +export interface PipelineInfo { + /** + * CI/CD provider + */ + readonly provider: CiProvider; + + /** + * Job/build ID + */ + readonly jobId?: string; + + /** + * Job/build URL + */ + readonly jobUrl?: string; + + /** + * User who triggered the job + */ + readonly triggeredBy?: string; + + /** + * Run number + */ + readonly runNumber?: string; + +} + +/** + * Custom environment variable names for overriding defaults + */ +export interface CustomEnvVarConfig { + /** + * Custom environment variable for repository owner + */ + readonly repoOwner?: string; + + /** + * Custom environment variable for repository name + */ + readonly repoName?: string; + + /** + * Custom environment variable for branch name + */ + readonly branch?: string; + + /** + * Custom environment variable for commit hash + */ + readonly commitHash?: string; + + /** + * Custom environment variable for job ID + */ + readonly jobId?: string; + + /** + * Custom environment variable for job URL + */ + readonly jobUrl?: string; + + /** + * Custom environment variable for triggered by user + */ + readonly triggeredBy?: string; + +} diff --git a/test/jest.setup.ts b/test/jest.setup.ts index 0f93ea1..8a83490 100644 --- a/test/jest.setup.ts +++ b/test/jest.setup.ts @@ -5,18 +5,81 @@ const ciEnvVars = [ 'GITHUB_REF', 'GITHUB_HEAD_REF', 'GITHUB_ACTIONS', + 'GITHUB_REPOSITORY', + 'GITHUB_REPOSITORY_OWNER', + 'GITHUB_RUN_ID', + 'GITHUB_RUN_NUMBER', + 'GITHUB_RUN_ATTEMPT', + 'GITHUB_ACTOR', + 'GITHUB_TRIGGERING_ACTOR', + 'GITHUB_WORKFLOW', + 'GITHUB_EVENT_NAME', + 'GITHUB_JOB', + 'GITHUB_ACTION', + 'GITHUB_SERVER_URL', // GitLab CI 'CI_COMMIT_SHA', 'CI_COMMIT_REF_NAME', 'CI_COMMIT_TAG', + 'CI_COMMIT_AUTHOR', 'GITLAB_CI', + 'GITLAB_USER_LOGIN', + 'GITLAB_USER_NAME', + 'CI_PROJECT_PATH', + 'CI_PROJECT_NAMESPACE', + 'CI_PROJECT_NAME', + 'CI_JOB_ID', + 'CI_JOB_URL', + 'CI_JOB_NAME', + 'CI_JOB_STAGE', + 'CI_PIPELINE_ID', + 'CI_PIPELINE_URL', + 'CI_PIPELINE_IID', + 'CI_PIPELINE_SOURCE', + 'CI_PIPELINE_NAME', + // AWS CodeBuild + 'CODEBUILD_BUILD_ID', + 'CODEBUILD_BUILD_ARN', + 'CODEBUILD_BUILD_NUMBER', + 'CODEBUILD_INITIATOR', + 'CODEBUILD_SOURCE_REPO_URL', + 'CODEBUILD_SOURCE_VERSION', + 'CODEBUILD_RESOLVED_SOURCE_VERSION', + 'CODEBUILD_WEBHOOK_HEAD_REF', + 'CODEBUILD_WEBHOOK_EVENT', + 'CODEBUILD_WEBHOOK_TRIGGER', + 'CODEBUILD_PUBLIC_BUILD_URL', // Generic CI 'GIT_COMMIT', 'GIT_BRANCH', 'GIT_TAG', + 'REPO_OWNER', + 'REPO_NAME', + 'REPOSITORY', + 'BRANCH', + 'COMMIT_HASH', + 'BUILD_ID', + 'BUILD_URL', + 'BUILD_USER', + 'BUILD_NUMBER', + 'JOB_ID', + 'JOB_URL', + 'WORKFLOW_NAME', + 'PIPELINE_NAME', + 'USER', // Common 'COMMIT_COUNT', 'COMMITS_SINCE_TAG', + // Custom env vars used in tests + 'MY_CUSTOM_OWNER', + 'MY_CUSTOM_REPO', + 'MY_CUSTOM_BRANCH', + 'MY_CUSTOM_COMMIT', + 'MY_CUSTOM_JOB_ID', + 'MY_CUSTOM_JOB_URL', + 'MY_CUSTOM_TRIGGERED_BY', + 'MY_CUSTOM_USER', + 'MY_CUSTOM_WORKFLOW', ]; beforeEach(() => { diff --git a/test/metadata/pipeline-info.test.ts b/test/metadata/pipeline-info.test.ts new file mode 100644 index 0000000..ec1cde4 --- /dev/null +++ b/test/metadata/pipeline-info.test.ts @@ -0,0 +1,223 @@ +import { PipelineInfoHelper } from '../../src/metadata/pipeline-info'; +import { CiProvider } from '../../src/metadata/types'; + +describe('PipelineInfoHelper', () => { + describe('create', () => { + it('should create PipelineInfo with all properties', () => { + const pipelineInfo = PipelineInfoHelper.create({ + provider: CiProvider.GITHUB, + jobId: '12345', + jobUrl: 'https://github.com/org/repo/actions/runs/12345', + triggeredBy: 'user@example.com', + runNumber: '42', + }); + + expect(pipelineInfo.provider).toBe(CiProvider.GITHUB); + expect(pipelineInfo.jobId).toBe('12345'); + expect(pipelineInfo.jobUrl).toBe('https://github.com/org/repo/actions/runs/12345'); + expect(pipelineInfo.triggeredBy).toBe('user@example.com'); + expect(pipelineInfo.runNumber).toBe('42'); + }); + + it('should create PipelineInfo with minimal properties', () => { + const pipelineInfo = PipelineInfoHelper.create({ + provider: CiProvider.GITLAB, + }); + + expect(pipelineInfo.provider).toBe(CiProvider.GITLAB); + expect(pipelineInfo.jobId).toBeUndefined(); + expect(pipelineInfo.jobUrl).toBeUndefined(); + }); + }); + + describe('fromEnvironment - GitHub', () => { + beforeEach(() => { + process.env.GITHUB_ACTIONS = 'true'; + }); + + it('should extract from GitHub Actions environment', () => { + process.env.GITHUB_RUN_ID = '12345'; + process.env.GITHUB_REPOSITORY = 'my-org/my-repo'; + process.env.GITHUB_SERVER_URL = 'https://github.com'; + process.env.GITHUB_ACTOR = 'john-doe'; + process.env.GITHUB_RUN_NUMBER = '42'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment(); + + expect(pipelineInfo.provider).toBe(CiProvider.GITHUB); + expect(pipelineInfo.jobId).toBe('12345'); + expect(pipelineInfo.jobUrl).toBe('https://github.com/my-org/my-repo/actions/runs/12345'); + expect(pipelineInfo.triggeredBy).toBe('john-doe'); + expect(pipelineInfo.runNumber).toBe('42'); + }); + + it('should prefer GITHUB_TRIGGERING_ACTOR over GITHUB_ACTOR', () => { + process.env.GITHUB_RUN_ID = '12345'; + process.env.GITHUB_ACTOR = 'john-doe'; + process.env.GITHUB_TRIGGERING_ACTOR = 'jane-smith'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment(); + + expect(pipelineInfo.triggeredBy).toBe('john-doe'); + }); + + it('should use custom environment variables', () => { + process.env.MY_CUSTOM_JOB_ID = 'custom-job-id'; + process.env.MY_CUSTOM_JOB_URL = 'https://custom.url/job/123'; + process.env.MY_CUSTOM_TRIGGERED_BY = 'custom-user'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment({ + jobId: 'MY_CUSTOM_JOB_ID', + jobUrl: 'MY_CUSTOM_JOB_URL', + triggeredBy: 'MY_CUSTOM_TRIGGERED_BY', + }); + + expect(pipelineInfo.jobId).toBe('custom-job-id'); + expect(pipelineInfo.jobUrl).toBe('https://custom.url/job/123'); + expect(pipelineInfo.triggeredBy).toBe('custom-user'); + }); + }); + + describe('fromEnvironment - GitLab', () => { + beforeEach(() => { + process.env.GITLAB_CI = 'true'; + }); + + it('should extract from GitLab CI environment', () => { + process.env.CI_JOB_ID = '67890'; + process.env.CI_PIPELINE_ID = '12345'; + process.env.CI_JOB_URL = 'https://gitlab.com/group/project/-/jobs/67890'; + process.env.CI_PIPELINE_URL = 'https://gitlab.com/group/project/-/pipelines/12345'; + process.env.GITLAB_USER_LOGIN = 'john.doe'; + process.env.CI_PIPELINE_IID = '42'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment(); + + expect(pipelineInfo.provider).toBe(CiProvider.GITLAB); + expect(pipelineInfo.jobId).toBe('67890'); + expect(pipelineInfo.jobUrl).toBe('https://gitlab.com/group/project/-/jobs/67890'); + expect(pipelineInfo.triggeredBy).toBe('john.doe'); + expect(pipelineInfo.runNumber).toBe('42'); + }); + + it('should fallback to CI_COMMIT_AUTHOR and GITLAB_USER_NAME', () => { + process.env.CI_JOB_ID = '67890'; + process.env.CI_COMMIT_AUTHOR = 'John Doe '; + process.env.GITLAB_USER_NAME = 'John Doe'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment(); + + expect(pipelineInfo.triggeredBy).toBe('John Doe '); + }); + + it('should prefer CI_JOB_ID over CI_PIPELINE_ID', () => { + process.env.CI_JOB_ID = '67890'; + process.env.CI_PIPELINE_ID = '12345'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment(); + + expect(pipelineInfo.jobId).toBe('67890'); + }); + + it('should use custom environment variables', () => { + process.env.MY_CUSTOM_JOB_ID = 'custom-job-id'; + process.env.MY_CUSTOM_JOB_URL = 'https://custom.url/job/123'; + process.env.MY_CUSTOM_TRIGGERED_BY = 'custom-user'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment({ + jobId: 'MY_CUSTOM_JOB_ID', + jobUrl: 'MY_CUSTOM_JOB_URL', + triggeredBy: 'MY_CUSTOM_TRIGGERED_BY', + }); + + expect(pipelineInfo.jobId).toBe('custom-job-id'); + expect(pipelineInfo.jobUrl).toBe('https://custom.url/job/123'); + expect(pipelineInfo.triggeredBy).toBe('custom-user'); + }); + }); + + describe('fromEnvironment - CodeBuild', () => { + beforeEach(() => { + process.env.CODEBUILD_BUILD_ID = 'my-project:abc-123'; + }); + + it('should extract from CodeBuild environment', () => { + process.env.CODEBUILD_BUILD_ARN = + 'arn:aws:codebuild:us-east-1:123456789012:build/my-project:abc-123'; + process.env.CODEBUILD_BUILD_NUMBER = '42'; + process.env.CODEBUILD_INITIATOR = 'john-doe'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment(); + + expect(pipelineInfo.provider).toBe(CiProvider.CODEBUILD); + expect(pipelineInfo.jobId).toBe('my-project:abc-123'); + expect(pipelineInfo.triggeredBy).toBe('john-doe'); + expect(pipelineInfo.runNumber).toBe('42'); + }); + + it('should construct job URL from build ID and ARN', () => { + process.env.CODEBUILD_BUILD_ID = 'my-project:abc-123'; + process.env.CODEBUILD_BUILD_ARN = + 'arn:aws:codebuild:us-west-2:123456789012:build/my-project:abc-123'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment(); + + expect(pipelineInfo.jobUrl).toContain('us-west-2'); + expect(pipelineInfo.jobUrl).toContain('my-project'); + }); + + it('should use custom environment variables', () => { + process.env.MY_CUSTOM_JOB_ID = 'custom-job-id'; + process.env.MY_CUSTOM_JOB_URL = 'https://custom.url/job/123'; + process.env.MY_CUSTOM_TRIGGERED_BY = 'custom-user'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment({ + jobId: 'MY_CUSTOM_JOB_ID', + jobUrl: 'MY_CUSTOM_JOB_URL', + triggeredBy: 'MY_CUSTOM_TRIGGERED_BY', + }); + + expect(pipelineInfo.jobId).toBe('custom-job-id'); + expect(pipelineInfo.jobUrl).toBe('https://custom.url/job/123'); + expect(pipelineInfo.triggeredBy).toBe('custom-user'); + }); + }); + + describe('fromEnvironment - Generic', () => { + it('should extract from generic environment variables', () => { + process.env.BUILD_ID = '12345'; + process.env.BUILD_URL = 'https://ci.example.com/build/12345'; + process.env.BUILD_USER = 'john-doe'; + process.env.BUILD_NUMBER = '42'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment(); + + expect(pipelineInfo.provider).toBe(CiProvider.UNKNOWN); + expect(pipelineInfo.jobId).toBe('12345'); + expect(pipelineInfo.jobUrl).toBe('https://ci.example.com/build/12345'); + expect(pipelineInfo.triggeredBy).toBe('john-doe'); + expect(pipelineInfo.runNumber).toBe('42'); + }); + + it('should use fallback environment variables', () => { + process.env.JOB_ID = '67890'; + process.env.JOB_URL = 'https://jenkins.example.com/job/123'; + process.env.USER = 'jenkins-user'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment(); + + expect(pipelineInfo.jobId).toBe('67890'); + expect(pipelineInfo.jobUrl).toBe('https://jenkins.example.com/job/123'); + expect(pipelineInfo.triggeredBy).toBe('jenkins-user'); + }); + + it('should handle missing values gracefully', () => { + const pipelineInfo = PipelineInfoHelper.fromEnvironment(); + + expect(pipelineInfo.provider).toBe(CiProvider.UNKNOWN); + expect(pipelineInfo.jobId).toBeUndefined(); + expect(pipelineInfo.jobUrl).toBeUndefined(); + expect(pipelineInfo.triggeredBy).toBeUndefined(); + }); + }); +}); diff --git a/test/metadata/repo-info.test.ts b/test/metadata/repo-info.test.ts new file mode 100644 index 0000000..44096de --- /dev/null +++ b/test/metadata/repo-info.test.ts @@ -0,0 +1,297 @@ +import { RepoInfoHelper } from '../../src/metadata/repo-info'; +import { CiProvider } from '../../src/metadata/types'; + +describe('RepoInfoHelper', () => { + describe('create', () => { + it('should create RepoInfo with all properties', () => { + const repoInfo = RepoInfoHelper.create({ + provider: CiProvider.GITHUB, + owner: 'my-org', + repository: 'my-repo', + branch: 'main', + commitHash: 'abcdef1234567890', + }); + + expect(repoInfo.provider).toBe(CiProvider.GITHUB); + expect(repoInfo.owner).toBe('my-org'); + expect(repoInfo.repository).toBe('my-repo'); + expect(repoInfo.branch).toBe('main'); + expect(repoInfo.commitHash).toBe('abcdef1234567890'); + }); + + it('should create RepoInfo without owner', () => { + const repoInfo = RepoInfoHelper.create({ + provider: CiProvider.CODEBUILD, + repository: 'my-repo', + branch: 'main', + commitHash: 'abcdef1234567890', + }); + + expect(repoInfo.provider).toBe(CiProvider.CODEBUILD); + expect(repoInfo.owner).toBeUndefined(); + expect(repoInfo.repository).toBe('my-repo'); + }); + }); + + describe('detectProvider', () => { + it('should detect GitHub Actions', () => { + process.env.GITHUB_ACTIONS = 'true'; + process.env.GITHUB_SHA = 'abc123'; + + const provider = RepoInfoHelper.detectProvider(); + expect(provider).toBe(CiProvider.GITHUB); + }); + + it('should detect GitLab CI', () => { + process.env.GITLAB_CI = 'true'; + process.env.CI_COMMIT_SHA = 'abc123'; + + const provider = RepoInfoHelper.detectProvider(); + expect(provider).toBe(CiProvider.GITLAB); + }); + + it('should detect AWS CodeBuild', () => { + process.env.CODEBUILD_BUILD_ID = 'my-build:123'; + + const provider = RepoInfoHelper.detectProvider(); + expect(provider).toBe(CiProvider.CODEBUILD); + }); + + it('should return UNKNOWN for unrecognized environment', () => { + const provider = RepoInfoHelper.detectProvider(); + expect(provider).toBe(CiProvider.UNKNOWN); + }); + }); + + describe('fromEnvironment - GitHub', () => { + beforeEach(() => { + process.env.GITHUB_ACTIONS = 'true'; + }); + + it('should extract from GitHub Actions environment', () => { + process.env.GITHUB_REPOSITORY = 'my-org/my-repo'; + process.env.GITHUB_REPOSITORY_OWNER = 'my-org'; + process.env.GITHUB_REF = 'refs/heads/feature-branch'; + process.env.GITHUB_SHA = 'abcdef1234567890'; + + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.provider).toBe(CiProvider.GITHUB); + expect(repoInfo.owner).toBe('my-org'); + expect(repoInfo.repository).toBe('my-repo'); + expect(repoInfo.branch).toBe('feature-branch'); + expect(repoInfo.commitHash).toBe('abcdef1234567890'); + }); + + it('should handle pull request with head ref', () => { + process.env.GITHUB_REPOSITORY = 'my-org/my-repo'; + process.env.GITHUB_REF = 'refs/pull/123/merge'; + process.env.GITHUB_HEAD_REF = 'feature-branch'; + process.env.GITHUB_SHA = 'abcdef1234567890'; + + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.branch).toBe('feature-branch'); + }); + + it('should extract tag name', () => { + process.env.GITHUB_REPOSITORY = 'my-org/my-repo'; + process.env.GITHUB_REF = 'refs/tags/v1.2.3'; + process.env.GITHUB_SHA = 'abcdef1234567890'; + + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.branch).toBe('v1.2.3'); + }); + + it('should use custom environment variables', () => { + process.env.MY_CUSTOM_OWNER = 'custom-org'; + process.env.MY_CUSTOM_REPO = 'custom-repo'; + process.env.MY_CUSTOM_BRANCH = 'custom-branch'; + process.env.MY_CUSTOM_COMMIT = 'custom-commit-hash'; + + const repoInfo = RepoInfoHelper.fromEnvironment({ + repoOwner: 'MY_CUSTOM_OWNER', + repoName: 'MY_CUSTOM_REPO', + branch: 'MY_CUSTOM_BRANCH', + commitHash: 'MY_CUSTOM_COMMIT', + }); + + expect(repoInfo.owner).toBe('custom-org'); + expect(repoInfo.repository).toBe('custom-repo'); + expect(repoInfo.branch).toBe('custom-branch'); + expect(repoInfo.commitHash).toBe('custom-commit-hash'); + }); + + it('should handle repository without slash', () => { + process.env.GITHUB_REPOSITORY = 'my-repo'; + process.env.GITHUB_REF = 'refs/heads/main'; + process.env.GITHUB_SHA = 'abcdef1234567890'; + + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.owner).toBe('unknown'); + expect(repoInfo.repository).toBe('my-repo'); + }); + }); + + describe('fromEnvironment - GitLab', () => { + beforeEach(() => { + process.env.GITLAB_CI = 'true'; + }); + + it('should extract from GitLab CI environment', () => { + process.env.CI_PROJECT_PATH = 'my-group/my-project'; + process.env.CI_PROJECT_NAMESPACE = 'my-group'; + process.env.CI_PROJECT_NAME = 'my-project'; + process.env.CI_COMMIT_REF_NAME = 'feature-branch'; + process.env.CI_COMMIT_SHA = 'abcdef1234567890'; + + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.provider).toBe(CiProvider.GITLAB); + expect(repoInfo.owner).toBe('my-group'); + expect(repoInfo.repository).toBe('my-project'); + expect(repoInfo.branch).toBe('feature-branch'); + expect(repoInfo.commitHash).toBe('abcdef1234567890'); + }); + + it('should fallback to CI_PROJECT_NAME when path has no slash', () => { + process.env.CI_PROJECT_PATH = 'my-project'; + process.env.CI_PROJECT_NAME = 'my-project-name'; + process.env.CI_COMMIT_REF_NAME = 'main'; + process.env.CI_COMMIT_SHA = 'abcdef1234567890'; + + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.repository).toBe('my-project-name'); + }); + + it('should use custom environment variables', () => { + process.env.MY_CUSTOM_OWNER = 'custom-group'; + process.env.MY_CUSTOM_REPO = 'custom-project'; + process.env.MY_CUSTOM_BRANCH = 'custom-branch'; + process.env.MY_CUSTOM_COMMIT = 'custom-commit-hash'; + + const repoInfo = RepoInfoHelper.fromEnvironment({ + repoOwner: 'MY_CUSTOM_OWNER', + repoName: 'MY_CUSTOM_REPO', + branch: 'MY_CUSTOM_BRANCH', + commitHash: 'MY_CUSTOM_COMMIT', + }); + + expect(repoInfo.owner).toBe('custom-group'); + expect(repoInfo.repository).toBe('custom-project'); + expect(repoInfo.branch).toBe('custom-branch'); + expect(repoInfo.commitHash).toBe('custom-commit-hash'); + }); + }); + + describe('fromEnvironment - CodeBuild', () => { + beforeEach(() => { + process.env.CODEBUILD_BUILD_ID = 'my-project:abc-123'; + }); + + it('should extract from CodeBuild environment with GitHub URL', () => { + process.env.CODEBUILD_SOURCE_REPO_URL = 'https://github.com/my-org/my-repo.git'; + process.env.CODEBUILD_WEBHOOK_HEAD_REF = 'refs/heads/feature-branch'; + process.env.CODEBUILD_RESOLVED_SOURCE_VERSION = 'abcdef1234567890'; + + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.provider).toBe(CiProvider.CODEBUILD); + expect(repoInfo.owner).toBe('my-org'); + expect(repoInfo.repository).toBe('my-repo'); + expect(repoInfo.branch).toBe('refs/heads/feature-branch'); + expect(repoInfo.commitHash).toBe('abcdef1234567890'); + }); + + it('should extract from CodeBuild environment with GitLab URL', () => { + process.env.CODEBUILD_SOURCE_REPO_URL = 'https://gitlab.com/my-group/my-project.git'; + process.env.CODEBUILD_SOURCE_VERSION = 'main'; + process.env.CODEBUILD_RESOLVED_SOURCE_VERSION = 'abcdef1234567890'; + + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.owner).toBe('my-group'); + expect(repoInfo.repository).toBe('my-project'); + }); + + it('should extract from CodeBuild environment with CodeCommit URL', () => { + process.env.CODEBUILD_SOURCE_REPO_URL = + 'https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo'; + process.env.CODEBUILD_SOURCE_VERSION = 'main'; + process.env.CODEBUILD_RESOLVED_SOURCE_VERSION = 'abcdef1234567890'; + + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.owner).toBeUndefined(); + expect(repoInfo.repository).toBe('my-repo'); + }); + + it('should fallback to CODEBUILD_SOURCE_VERSION when RESOLVED is missing', () => { + process.env.CODEBUILD_SOURCE_REPO_URL = 'https://github.com/my-org/my-repo.git'; + process.env.CODEBUILD_SOURCE_VERSION = 'commit-hash-123'; + + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.commitHash).toBe('commit-hash-123'); + }); + + it('should use custom environment variables', () => { + process.env.MY_CUSTOM_OWNER = 'custom-org'; + process.env.MY_CUSTOM_REPO = 'custom-repo'; + process.env.MY_CUSTOM_BRANCH = 'custom-branch'; + process.env.MY_CUSTOM_COMMIT = 'custom-commit-hash'; + + const repoInfo = RepoInfoHelper.fromEnvironment({ + repoOwner: 'MY_CUSTOM_OWNER', + repoName: 'MY_CUSTOM_REPO', + branch: 'MY_CUSTOM_BRANCH', + commitHash: 'MY_CUSTOM_COMMIT', + }); + + expect(repoInfo.owner).toBe('custom-org'); + expect(repoInfo.repository).toBe('custom-repo'); + expect(repoInfo.branch).toBe('custom-branch'); + expect(repoInfo.commitHash).toBe('custom-commit-hash'); + }); + }); + + describe('fromEnvironment - Generic', () => { + it('should extract from generic environment variables', () => { + process.env.REPO_OWNER = 'my-org'; + process.env.REPO_NAME = 'my-repo'; + process.env.BRANCH = 'feature-branch'; + process.env.COMMIT_HASH = 'abcdef1234567890'; + + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.provider).toBe(CiProvider.UNKNOWN); + expect(repoInfo.owner).toBe('my-org'); + expect(repoInfo.repository).toBe('my-repo'); + expect(repoInfo.branch).toBe('feature-branch'); + expect(repoInfo.commitHash).toBe('abcdef1234567890'); + }); + + it('should use fallback environment variables', () => { + process.env.REPOSITORY = 'my-repo'; + process.env.GIT_BRANCH = 'main'; + process.env.GIT_COMMIT = 'commit-123'; + + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.repository).toBe('my-repo'); + expect(repoInfo.branch).toBe('main'); + expect(repoInfo.commitHash).toBe('commit-123'); + }); + + it('should use unknown as default for missing values', () => { + const repoInfo = RepoInfoHelper.fromEnvironment(); + + expect(repoInfo.repository).toBe('unknown'); + expect(repoInfo.branch).toBe('unknown'); + expect(repoInfo.commitHash).toBe('unknown'); + }); + }); +}); diff --git a/test/metadata/stack-metadata.test.ts b/test/metadata/stack-metadata.test.ts new file mode 100644 index 0000000..18131d3 --- /dev/null +++ b/test/metadata/stack-metadata.test.ts @@ -0,0 +1,228 @@ +import * as cdk from 'aws-cdk-lib'; +import { StackMetadata } from '../../src/metadata/stack-metadata'; +import { CiProvider } from '../../src/metadata/types'; + +describe('StackMetadata', () => { + let app: cdk.App; + let stack: cdk.Stack; + + beforeEach(() => { + app = new cdk.App(); + stack = new cdk.Stack(app, 'TestStack'); + }); + + describe('constructor with manual configuration', () => { + it('should add metadata to stack with provided info', () => { + new StackMetadata(stack, 'Metadata', { + repoInfo: { + provider: CiProvider.GITHUB, + owner: 'my-org', + repository: 'my-repo', + branch: 'main', + commitHash: 'abcdef1234567890', + }, + pipelineInfo: { + provider: CiProvider.GITHUB, + jobId: '12345', + jobUrl: 'https://github.com/my-org/my-repo/actions/runs/12345', + triggeredBy: 'john-doe', + runNumber: '42', + }, + }); + + const template = app.synth().getStackByName('TestStack').template; + expect(template.Metadata).toBeDefined(); + expect(template.Metadata.Repo).toEqual({ + provider: 'github', + owner: 'my-org', + repository: 'my-repo', + branch: 'main', + commitHash: 'abcdef1234567890', + }); + expect(template.Metadata.Pipeline).toEqual({ + provider: 'github', + jobId: '12345', + jobUrl: 'https://github.com/my-org/my-repo/actions/runs/12345', + triggeredBy: 'john-doe', + runNumber: '42', + }); + }); + + it('should use custom metadata keys', () => { + new StackMetadata(stack, 'Metadata', { + repoInfo: { + provider: CiProvider.GITHUB, + owner: 'my-org', + repository: 'my-repo', + branch: 'main', + commitHash: 'abcdef1234567890', + }, + pipelineInfo: { + provider: CiProvider.GITHUB, + jobId: '12345', + }, + repoMetadataKey: 'CustomRepo', + pipelineMetadataKey: 'CustomPipeline', + }); + + const template = app.synth().getStackByName('TestStack').template; + expect(template.Metadata.CustomRepo).toBeDefined(); + expect(template.Metadata.CustomPipeline).toBeDefined(); + expect(template.Metadata.Repo).toBeUndefined(); + expect(template.Metadata.Pipeline).toBeUndefined(); + }); + + it('should include all pipeline fields even when undefined', () => { + new StackMetadata(stack, 'Metadata', { + repoInfo: { + provider: CiProvider.GITHUB, + owner: 'my-org', + repository: 'my-repo', + branch: 'main', + commitHash: 'abcdef1234567890', + }, + pipelineInfo: { + provider: CiProvider.GITHUB, + jobId: '12345', + }, + }); + + const template = app.synth().getStackByName('TestStack').template; + expect(template.Metadata.Pipeline.provider).toBe('github'); + expect(template.Metadata.Pipeline.jobId).toBe('12345'); + }); + }); + + describe('fromEnvironment - GitHub', () => { + beforeEach(() => { + process.env.GITHUB_ACTIONS = 'true'; + process.env.GITHUB_REPOSITORY = 'test-org/test-repo'; + process.env.GITHUB_REF = 'refs/heads/feature-branch'; + process.env.GITHUB_SHA = 'abc123def456'; + process.env.GITHUB_RUN_ID = '98765'; + process.env.GITHUB_ACTOR = 'test-user'; + process.env.GITHUB_WORKFLOW = 'Test Workflow'; + }); + + it('should extract metadata from GitHub Actions environment', () => { + const metadata = StackMetadata.fromEnvironment(stack, 'Metadata'); + + expect(metadata.repoInfo.provider).toBe(CiProvider.GITHUB); + expect(metadata.repoInfo.repository).toBe('test-repo'); + expect(metadata.repoInfo.branch).toBe('feature-branch'); + expect(metadata.repoInfo.commitHash).toBe('abc123def456'); + expect(metadata.pipelineInfo.provider).toBe(CiProvider.GITHUB); + expect(metadata.pipelineInfo.jobId).toBe('98765'); + expect(metadata.pipelineInfo.triggeredBy).toBe('test-user'); + }); + + it('should add metadata to stack from environment', () => { + StackMetadata.fromEnvironment(stack, 'Metadata'); + + const template = app.synth().getStackByName('TestStack').template; + expect(template.Metadata.Repo).toBeDefined(); + expect(template.Metadata.Repo.provider).toBe('github'); + expect(template.Metadata.Pipeline).toBeDefined(); + expect(template.Metadata.Pipeline.provider).toBe('github'); + }); + }); + + describe('fromEnvironment - GitLab', () => { + beforeEach(() => { + process.env.GITLAB_CI = 'true'; + process.env.CI_PROJECT_PATH = 'test-group/test-project'; + process.env.CI_COMMIT_REF_NAME = 'main'; + process.env.CI_COMMIT_SHA = 'gitlab123abc'; + process.env.CI_JOB_ID = '54321'; + process.env.GITLAB_USER_LOGIN = 'gitlab-user'; + }); + + it('should extract metadata from GitLab CI environment', () => { + const metadata = StackMetadata.fromEnvironment(stack, 'Metadata'); + + expect(metadata.repoInfo.provider).toBe(CiProvider.GITLAB); + expect(metadata.repoInfo.repository).toBe('test-project'); + expect(metadata.repoInfo.branch).toBe('main'); + expect(metadata.repoInfo.commitHash).toBe('gitlab123abc'); + expect(metadata.pipelineInfo.provider).toBe(CiProvider.GITLAB); + expect(metadata.pipelineInfo.jobId).toBe('54321'); + expect(metadata.pipelineInfo.triggeredBy).toBe('gitlab-user'); + }); + + it('should add metadata to stack from environment', () => { + StackMetadata.fromEnvironment(stack, 'Metadata'); + + const template = app.synth().getStackByName('TestStack').template; + expect(template.Metadata.Repo).toBeDefined(); + expect(template.Metadata.Repo.provider).toBe('gitlab'); + expect(template.Metadata.Pipeline).toBeDefined(); + expect(template.Metadata.Pipeline.provider).toBe('gitlab'); + }); + }); + + describe('fromEnvironment - CodeBuild', () => { + beforeEach(() => { + process.env.CODEBUILD_BUILD_ID = 'codebuild-project:build-123'; + process.env.CODEBUILD_SOURCE_REPO_URL = 'https://github.com/test-org/test-repo.git'; + process.env.CODEBUILD_SOURCE_VERSION = 'develop'; + process.env.CODEBUILD_RESOLVED_SOURCE_VERSION = 'codebuild-commit-hash'; + process.env.CODEBUILD_INITIATOR = 'codebuild-user'; + }); + + it('should extract metadata from CodeBuild environment', () => { + const metadata = StackMetadata.fromEnvironment(stack, 'Metadata'); + + expect(metadata.repoInfo.provider).toBe(CiProvider.CODEBUILD); + expect(metadata.repoInfo.repository).toBe('test-repo'); + expect(metadata.repoInfo.branch).toBe('develop'); + expect(metadata.repoInfo.commitHash).toBe('codebuild-commit-hash'); + expect(metadata.pipelineInfo.provider).toBe(CiProvider.CODEBUILD); + expect(metadata.pipelineInfo.jobId).toBe('codebuild-project:build-123'); + expect(metadata.pipelineInfo.triggeredBy).toBe('codebuild-user'); + }); + + it('should add metadata to stack from environment', () => { + StackMetadata.fromEnvironment(stack, 'Metadata'); + + const template = app.synth().getStackByName('TestStack').template; + expect(template.Metadata.Repo).toBeDefined(); + expect(template.Metadata.Repo.provider).toBe('codebuild'); + expect(template.Metadata.Pipeline).toBeDefined(); + expect(template.Metadata.Pipeline.provider).toBe('codebuild'); + }); + }); + + describe('fromEnvironment with custom environment variables', () => { + beforeEach(() => { + process.env.GITHUB_ACTIONS = 'true'; + process.env.MY_CUSTOM_OWNER = 'custom-owner'; + process.env.MY_CUSTOM_REPO = 'custom-repo'; + process.env.MY_CUSTOM_BRANCH = 'custom-branch'; + process.env.MY_CUSTOM_COMMIT = 'custom-commit-hash'; + process.env.MY_CUSTOM_JOB_ID = 'custom-job-123'; + process.env.MY_CUSTOM_JOB_URL = 'https://custom.ci.com/job/123'; + process.env.MY_CUSTOM_USER = 'custom-user'; + }); + + it('should use custom environment variables', () => { + const metadata = StackMetadata.fromEnvironment(stack, 'Metadata', { + repoOwner: 'MY_CUSTOM_OWNER', + repoName: 'MY_CUSTOM_REPO', + branch: 'MY_CUSTOM_BRANCH', + commitHash: 'MY_CUSTOM_COMMIT', + jobId: 'MY_CUSTOM_JOB_ID', + jobUrl: 'MY_CUSTOM_JOB_URL', + triggeredBy: 'MY_CUSTOM_USER', + }); + + expect(metadata.repoInfo.owner).toBe('custom-owner'); + expect(metadata.repoInfo.repository).toBe('custom-repo'); + expect(metadata.repoInfo.branch).toBe('custom-branch'); + expect(metadata.repoInfo.commitHash).toBe('custom-commit-hash'); + expect(metadata.pipelineInfo.jobId).toBe('custom-job-123'); + expect(metadata.pipelineInfo.jobUrl).toBe('https://custom.ci.com/job/123'); + expect(metadata.pipelineInfo.triggeredBy).toBe('custom-user'); + }); + }); + +});