From 3a46072dd63c0e5901953649001b7f80d6be13f7 Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:13:21 +0000 Subject: [PATCH 1/5] Add stack metadata construct for repo and pipeline info Co-authored-by: Thorsten Hoeger --- API.md | 1313 +++++++++++++++++++++++--- METADATA_USAGE.md | 229 +++++ examples/metadata-example.ts | 58 ++ package-lock.json | 15 +- src/index.ts | 5 +- src/metadata/index.ts | 11 + src/metadata/pipeline-info.ts | 230 +++++ src/metadata/repo-info.ts | 228 +++++ src/metadata/stack-metadata.ts | 181 ++++ src/metadata/types.ts | 134 +++ test/jest.setup.ts | 63 ++ test/metadata/pipeline-info.test.ts | 290 ++++++ test/metadata/repo-info.test.ts | 297 ++++++ test/metadata/stack-metadata.test.ts | 329 +++++++ 14 files changed, 3234 insertions(+), 149 deletions(-) create mode 100644 METADATA_USAGE.md create mode 100644 examples/metadata-example.ts create mode 100644 src/metadata/index.ts create mode 100644 src/metadata/pipeline-info.ts create mode 100644 src/metadata/repo-info.ts create mode 100644 src/metadata/stack-metadata.ts create mode 100644 src/metadata/types.ts create mode 100644 test/metadata/pipeline-info.test.ts create mode 100644 test/metadata/repo-info.test.ts create mode 100644 test/metadata/stack-metadata.test.ts diff --git a/API.md b/API.md index 4023a93..d74cea4 100644 --- a/API.md +++ b/API.md @@ -2,6 +2,233 @@ ## 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. | +| pipelineMetadata | Get the pipeline information as a plain object. | +| repoMetadata | Get the repository information as a plain object. | + +--- + +##### `toString` + +```typescript +public toString(): string +``` + +Returns a string representation of this construct. + +##### `pipelineMetadata` + +```typescript +public pipelineMetadata(): PipelineInfo +``` + +Get the pipeline information as a plain object. + +##### `repoMetadata` + +```typescript +public repoMetadata(): RepoInfo +``` + +Get the repository information as a plain object. + +#### 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 +694,129 @@ 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. | +| workflowName | string | Custom environment variable for workflow name. | + +--- + +##### `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. + +--- + +##### `workflowName`Optional + +```typescript +public readonly workflowName: string; +``` + +- *Type:* string + +Custom environment variable for workflow name. + +--- + ### GitInfo Git repository information. @@ -546,49 +896,619 @@ 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. | +| additionalInfo | {[ key: string ]: string} | Additional provider-specific information. | +| event | string | Event that triggered the workflow. | +| jobId | string | Job/build ID. | +| jobUrl | string | Job/build URL. | +| runAttempt | string | Run attempt (for retries). | +| runNumber | string | Run number. | +| triggeredBy | string | User who triggered the job. | +| workflowName | string | Workflow/pipeline name. | + +--- + +##### `provider`Required + +```typescript +public readonly provider: CiProvider; +``` + +- *Type:* CiProvider + +CI/CD provider. + +--- + +##### `additionalInfo`Optional + +```typescript +public readonly additionalInfo: {[ key: string ]: string}; +``` + +- *Type:* {[ key: string ]: string} + +Additional provider-specific information. + +--- + +##### `event`Optional + +```typescript +public readonly event: string; +``` + +- *Type:* string + +Event that triggered the workflow. + +--- + +##### `jobId`Optional + +```typescript +public readonly jobId: string; +``` + +- *Type:* string + +Job/build ID. + +--- + +##### `jobUrl`Optional + +```typescript +public readonly jobUrl: string; +``` + +- *Type:* string + +Job/build URL. + +--- + +##### `runAttempt`Optional + +```typescript +public readonly runAttempt: string; +``` + +- *Type:* string + +Run attempt (for retries). + +--- + +##### `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. + +--- + +##### `workflowName`Optional + +```typescript +public readonly workflowName: string; +``` + +- *Type:* string + +Workflow/pipeline name. + +--- + +### PipelineInfoProps + +Props for creating PipelineInfo. + +#### Initializer + +```typescript +import { PipelineInfoProps } from 'cdk-devops' + +const pipelineInfoProps: PipelineInfoProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| provider | CiProvider | CI/CD provider. | +| additionalInfo | {[ key: string ]: string} | Additional provider-specific information. | +| event | string | Event that triggered the workflow. | +| jobId | string | Job/build ID. | +| jobUrl | string | Job/build URL. | +| runAttempt | string | Run attempt (for retries). | +| runNumber | string | Run number. | +| triggeredBy | string | User who triggered the job. | +| workflowName | string | Workflow/pipeline name. | + +--- + +##### `provider`Required + +```typescript +public readonly provider: CiProvider; +``` + +- *Type:* CiProvider + +CI/CD provider. + +--- + +##### `additionalInfo`Optional + +```typescript +public readonly additionalInfo: {[ key: string ]: string}; +``` + +- *Type:* {[ key: string ]: string} + +Additional provider-specific information. + +--- + +##### `event`Optional + +```typescript +public readonly event: string; +``` + +- *Type:* string + +Event that triggered the workflow. + +--- + +##### `jobId`Optional + +```typescript +public readonly jobId: string; +``` + +- *Type:* string + +Job/build ID. + +--- + +##### `jobUrl`Optional + +```typescript +public readonly jobUrl: string; +``` + +- *Type:* string + +Job/build URL. + +--- + +##### `runAttempt`Optional + +```typescript +public readonly runAttempt: string; +``` + +- *Type:* string + +Run attempt (for retries). + +--- + +##### `runNumber`Optional + +```typescript +public readonly runNumber: string; +``` + +- *Type:* string + +Run number. + +--- + +##### `triggeredBy`Optional + +```typescript +public readonly triggeredBy: string; +``` + +- *Type:* string -Commit count since last tag. +User who triggered the job. --- -##### `tag`Optional +##### `workflowName`Optional ```typescript -public readonly tag: string; +public readonly workflowName: string; ``` - *Type:* string -Git tag (if on a tagged commit). +Workflow/pipeline name. --- -### 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 +1516,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 +2360,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 +3643,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/METADATA_USAGE.md b/METADATA_USAGE.md new file mode 100644 index 0000000..d2813da --- /dev/null +++ b/METADATA_USAGE.md @@ -0,0 +1,229 @@ +# 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', + workflowName: 'MY_CUSTOM_WORKFLOW', + }, +}); +``` + +### 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', + workflowName: 'Production Deployment', + 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` +- Workflow name from `GITHUB_WORKFLOW` +- Run number from `GITHUB_RUN_NUMBER` +- Run attempt from `GITHUB_RUN_ATTEMPT` +- Event from `GITHUB_EVENT_NAME` + +### 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` +- Workflow name from `CI_PROJECT_NAME` +- Run number from `CI_PIPELINE_IID` +- Event from `CI_PIPELINE_SOURCE` + +### 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` +- Workflow name parsed from `CODEBUILD_BUILD_ARN` +- 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` +- `WORKFLOW_NAME`, `PIPELINE_NAME` + +## 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", + "workflowName": "CI Pipeline", + "runNumber": "42", + "runAttempt": "1", + "event": "push", + "additionalInfo": { + "job": "build", + "action": "build-action", + "ref": "refs/heads/main", + "sha": "abc123def456789" + } + } + } +} +``` + +## Accessing Metadata in Code + +You can access the extracted metadata programmatically: + +```typescript +const metadata = new StackMetadata(stack, 'Metadata'); + +const repoInfo = metadata.repoMetadata(); +console.log(`Deployed from ${repoInfo.owner}/${repoInfo.repository}@${repoInfo.commitHash}`); + +const pipelineInfo = metadata.pipelineMetadata(); +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/examples/metadata-example.ts b/examples/metadata-example.ts new file mode 100644 index 0000000..4b0ff4f --- /dev/null +++ b/examples/metadata-example.ts @@ -0,0 +1,58 @@ +import * as cdk from 'aws-cdk-lib'; +import { StackMetadata, CiProvider } from '../lib'; + +// Example 1: Automatically extract from environment +const app = new cdk.App(); + +const stack1 = new cdk.Stack(app, 'Stack1', { + stackName: 'my-stack-auto', +}); + +// This will automatically extract repo and pipeline info from environment variables +new StackMetadata(stack1, 'Metadata'); + +// Example 2: Use custom environment variable names +const stack2 = new cdk.Stack(app, 'Stack2', { + stackName: 'my-stack-custom', +}); + +new StackMetadata(stack2, 'Metadata', { + customEnvVars: { + repoOwner: 'MY_CUSTOM_OWNER', + repoName: 'MY_CUSTOM_REPO', + branch: 'MY_CUSTOM_BRANCH', + commitHash: 'MY_CUSTOM_COMMIT', + jobId: 'MY_CUSTOM_JOB_ID', + }, +}); + +// Example 3: Manually provide information +const stack3 = new cdk.Stack(app, 'Stack3', { + stackName: 'my-stack-manual', +}); + +new StackMetadata(stack3, 'Metadata', { + repoInfo: { + provider: CiProvider.GITHUB, + owner: 'open-constructs', + repository: 'cdk-devops', + branch: 'main', + commitHash: 'abc123def456', + }, + pipelineInfo: { + provider: CiProvider.GITHUB, + jobId: '12345', + jobUrl: 'https://github.com/open-constructs/cdk-devops/actions/runs/12345', + triggeredBy: 'developer@example.com', + workflowName: 'CI Pipeline', + }, +}); + +// Example 4: Using the static fromEnvironment method +const stack4 = new cdk.Stack(app, 'Stack4', { + stackName: 'my-stack-from-env', +}); + +StackMetadata.fromEnvironment(stack4, 'Metadata'); + +app.synth(); diff --git a/package-lock.json b/package-lock.json index 01adecc..c3bd71b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,7 +125,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2345,7 +2344,6 @@ "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2416,7 +2414,6 @@ "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -2894,7 +2891,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3817,7 +3813,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4303,8 +4298,7 @@ "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/conventional-changelog": { "version": "4.0.0", @@ -5208,7 +5202,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5379,7 +5372,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7342,7 +7334,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -9411,7 +9402,6 @@ "integrity": "sha512-BMsFDilBLpSzIEdK38kYY4x0w4U5qZeLqOTiZUiyOwe9GsHZSfLCHWJ7TvTAAeBF36nnOzxSySL6+/Hp0N7pTQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@jsii/check-node": "^1.121.0", "@jsii/spec": "^1.121.0", @@ -15532,7 +15522,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15742,7 +15731,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15808,7 +15796,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, 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..cdfacac --- /dev/null +++ b/src/metadata/pipeline-info.ts @@ -0,0 +1,230 @@ +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; + + /** + * Workflow/pipeline name + */ + readonly workflowName?: string; + + /** + * Run number + */ + readonly runNumber?: string; + + /** + * Run attempt (for retries) + */ + readonly runAttempt?: string; + + /** + * Event that triggered the workflow + */ + readonly event?: string; + + /** + * Additional provider-specific information + */ + readonly additionalInfo?: Record; +} + +/** + * 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, + workflowName: props.workflowName, + runNumber: props.runNumber, + runAttempt: props.runAttempt, + event: props.event, + additionalInfo: props.additionalInfo, + }; + } + + /** + * 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'), + workflowName: this.getEnvVar(customEnvVars?.workflowName, 'GITHUB_WORKFLOW'), + runNumber: process.env.GITHUB_RUN_NUMBER, + runAttempt: process.env.GITHUB_RUN_ATTEMPT, + event: process.env.GITHUB_EVENT_NAME, + additionalInfo: { + job: process.env.GITHUB_JOB || '', + action: process.env.GITHUB_ACTION || '', + ref: process.env.GITHUB_REF || '', + sha: process.env.GITHUB_SHA || '', + }, + }); + } + + /** + * 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', + ), + workflowName: this.getEnvVar(customEnvVars?.workflowName, 'CI_PROJECT_NAME', 'CI_PIPELINE_NAME'), + runNumber: process.env.CI_PIPELINE_IID, + event: process.env.CI_PIPELINE_SOURCE, + additionalInfo: { + jobName: process.env.CI_JOB_NAME || '', + jobStage: process.env.CI_JOB_STAGE || '', + pipelineId: process.env.CI_PIPELINE_ID || '', + projectPath: process.env.CI_PROJECT_PATH || '', + commitRef: process.env.CI_COMMIT_REF_NAME || '', + }, + }); + } + + /** + * 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); + + // Extract workflow name from custom env var or build ARN + let workflowName: string | undefined; + if (customEnvVars?.workflowName && process.env[customEnvVars.workflowName]) { + workflowName = process.env[customEnvVars.workflowName]; + } else if (process.env.CODEBUILD_BUILD_ARN) { + workflowName = process.env.CODEBUILD_BUILD_ARN.split(':')[5]?.split('/')[1]; + } + + return this.create({ + provider: CiProvider.CODEBUILD, + jobId: this.getEnvVar(customEnvVars?.jobId, 'CODEBUILD_BUILD_ID'), + jobUrl, + triggeredBy: this.getEnvVar(customEnvVars?.triggeredBy, 'CODEBUILD_INITIATOR'), + workflowName, + runNumber: buildNumber, + additionalInfo: { + buildArn: process.env.CODEBUILD_BUILD_ARN || '', + buildNumber: buildNumber || '', + sourceVersion: process.env.CODEBUILD_SOURCE_VERSION || '', + webhookEvent: process.env.CODEBUILD_WEBHOOK_EVENT || '', + webhookTrigger: process.env.CODEBUILD_WEBHOOK_TRIGGER || '', + publicBuildUrl: process.env.CODEBUILD_PUBLIC_BUILD_URL || '', + }, + }); + } + + /** + * 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'), + workflowName: this.getEnvVar(customEnvVars?.workflowName, 'WORKFLOW_NAME', 'PIPELINE_NAME'), + runNumber: process.env.BUILD_NUMBER, + additionalInfo: {}, + }); + } + + /** + * 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..623db27 --- /dev/null +++ b/src/metadata/stack-metadata.ts @@ -0,0 +1,181 @@ +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, + }; + + if (this.pipelineInfo.jobId) { + pipelineMetadata.jobId = this.pipelineInfo.jobId; + } + if (this.pipelineInfo.jobUrl) { + pipelineMetadata.jobUrl = this.pipelineInfo.jobUrl; + } + if (this.pipelineInfo.triggeredBy) { + pipelineMetadata.triggeredBy = this.pipelineInfo.triggeredBy; + } + if (this.pipelineInfo.workflowName) { + pipelineMetadata.workflowName = this.pipelineInfo.workflowName; + } + if (this.pipelineInfo.runNumber) { + pipelineMetadata.runNumber = this.pipelineInfo.runNumber; + } + if (this.pipelineInfo.runAttempt) { + pipelineMetadata.runAttempt = this.pipelineInfo.runAttempt; + } + if (this.pipelineInfo.event) { + pipelineMetadata.event = this.pipelineInfo.event; + } + if (this.pipelineInfo.additionalInfo && Object.keys(this.pipelineInfo.additionalInfo).length > 0) { + pipelineMetadata.additionalInfo = this.pipelineInfo.additionalInfo; + } + + stack.addMetadata(pipelineKey, pipelineMetadata); + } + + /** + * Get the repository information as a plain object + */ + public repoMetadata(): RepoInfo { + return this.repoInfo; + } + + /** + * Get the pipeline information as a plain object + */ + public pipelineMetadata(): PipelineInfo { + return this.pipelineInfo; + } +} diff --git a/src/metadata/types.ts b/src/metadata/types.ts new file mode 100644 index 0000000..e0acb95 --- /dev/null +++ b/src/metadata/types.ts @@ -0,0 +1,134 @@ +/** + * 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; + + /** + * Workflow/pipeline name + */ + readonly workflowName?: string; + + /** + * Run number + */ + readonly runNumber?: string; + + /** + * Run attempt (for retries) + */ + readonly runAttempt?: string; + + /** + * Event that triggered the workflow + */ + readonly event?: string; + + /** + * Additional provider-specific information + */ + readonly additionalInfo?: Record; +} + +/** + * 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; + + /** + * Custom environment variable for workflow name + */ + readonly workflowName?: 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..a393e30 --- /dev/null +++ b/test/metadata/pipeline-info.test.ts @@ -0,0 +1,290 @@ +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', + workflowName: 'CI', + runNumber: '42', + runAttempt: '1', + event: 'push', + additionalInfo: { + custom: 'value', + }, + }); + + 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.workflowName).toBe('CI'); + expect(pipelineInfo.runNumber).toBe('42'); + expect(pipelineInfo.runAttempt).toBe('1'); + expect(pipelineInfo.event).toBe('push'); + expect(pipelineInfo.additionalInfo).toEqual({ custom: 'value' }); + }); + + 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_WORKFLOW = 'CI Pipeline'; + process.env.GITHUB_RUN_NUMBER = '42'; + process.env.GITHUB_RUN_ATTEMPT = '1'; + process.env.GITHUB_EVENT_NAME = 'push'; + process.env.GITHUB_JOB = 'build'; + process.env.GITHUB_ACTION = 'build-action'; + process.env.GITHUB_REF = 'refs/heads/main'; + process.env.GITHUB_SHA = 'abcdef1234567890'; + + 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.workflowName).toBe('CI Pipeline'); + expect(pipelineInfo.runNumber).toBe('42'); + expect(pipelineInfo.runAttempt).toBe('1'); + expect(pipelineInfo.event).toBe('push'); + expect(pipelineInfo.additionalInfo).toEqual({ + job: 'build', + action: 'build-action', + ref: 'refs/heads/main', + sha: 'abcdef1234567890', + }); + }); + + 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'; + process.env.MY_CUSTOM_WORKFLOW = 'Custom Workflow'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment({ + jobId: 'MY_CUSTOM_JOB_ID', + jobUrl: 'MY_CUSTOM_JOB_URL', + triggeredBy: 'MY_CUSTOM_TRIGGERED_BY', + workflowName: 'MY_CUSTOM_WORKFLOW', + }); + + expect(pipelineInfo.jobId).toBe('custom-job-id'); + expect(pipelineInfo.jobUrl).toBe('https://custom.url/job/123'); + expect(pipelineInfo.triggeredBy).toBe('custom-user'); + expect(pipelineInfo.workflowName).toBe('Custom Workflow'); + }); + }); + + 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_PROJECT_NAME = 'my-project'; + process.env.CI_PIPELINE_IID = '42'; + process.env.CI_PIPELINE_SOURCE = 'push'; + process.env.CI_JOB_NAME = 'build'; + process.env.CI_JOB_STAGE = 'test'; + process.env.CI_PROJECT_PATH = 'group/project'; + process.env.CI_COMMIT_REF_NAME = 'main'; + + 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.workflowName).toBe('my-project'); + expect(pipelineInfo.runNumber).toBe('42'); + expect(pipelineInfo.event).toBe('push'); + expect(pipelineInfo.additionalInfo).toEqual({ + jobName: 'build', + jobStage: 'test', + pipelineId: '12345', + projectPath: 'group/project', + commitRef: 'main', + }); + }); + + 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'; + process.env.MY_CUSTOM_WORKFLOW = 'Custom Workflow'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment({ + jobId: 'MY_CUSTOM_JOB_ID', + jobUrl: 'MY_CUSTOM_JOB_URL', + triggeredBy: 'MY_CUSTOM_TRIGGERED_BY', + workflowName: 'MY_CUSTOM_WORKFLOW', + }); + + expect(pipelineInfo.jobId).toBe('custom-job-id'); + expect(pipelineInfo.jobUrl).toBe('https://custom.url/job/123'); + expect(pipelineInfo.triggeredBy).toBe('custom-user'); + expect(pipelineInfo.workflowName).toBe('Custom Workflow'); + }); + }); + + 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'; + process.env.CODEBUILD_SOURCE_VERSION = 'main'; + process.env.CODEBUILD_WEBHOOK_EVENT = 'PULL_REQUEST_CREATED'; + process.env.CODEBUILD_WEBHOOK_TRIGGER = 'pr/123'; + process.env.CODEBUILD_PUBLIC_BUILD_URL = 'https://public.build.url'; + + 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.workflowName).toBe('my-project'); + expect(pipelineInfo.runNumber).toBe('42'); + expect(pipelineInfo.additionalInfo).toEqual({ + buildArn: 'arn:aws:codebuild:us-east-1:123456789012:build/my-project:abc-123', + buildNumber: '42', + sourceVersion: 'main', + webhookEvent: 'PULL_REQUEST_CREATED', + webhookTrigger: 'pr/123', + publicBuildUrl: 'https://public.build.url', + }); + }); + + 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'; + process.env.MY_CUSTOM_WORKFLOW = 'Custom Workflow'; + + const pipelineInfo = PipelineInfoHelper.fromEnvironment({ + jobId: 'MY_CUSTOM_JOB_ID', + jobUrl: 'MY_CUSTOM_JOB_URL', + triggeredBy: 'MY_CUSTOM_TRIGGERED_BY', + workflowName: 'MY_CUSTOM_WORKFLOW', + }); + + expect(pipelineInfo.jobId).toBe('custom-job-id'); + expect(pipelineInfo.jobUrl).toBe('https://custom.url/job/123'); + expect(pipelineInfo.triggeredBy).toBe('custom-user'); + expect(pipelineInfo.workflowName).toBe('Custom Workflow'); + }); + }); + + 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.WORKFLOW_NAME = 'CI Pipeline'; + 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.workflowName).toBe('CI Pipeline'); + 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'; + process.env.PIPELINE_NAME = 'Build Pipeline'; + + 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'); + expect(pipelineInfo.workflowName).toBe('Build Pipeline'); + }); + + 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..3a01097 --- /dev/null +++ b/test/metadata/stack-metadata.test.ts @@ -0,0 +1,329 @@ +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', + workflowName: 'CI', + 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', + workflowName: 'CI', + 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 omit undefined optional pipeline fields', () => { + new StackMetadata(stack, 'Metadata', { + repoInfo: { + provider: CiProvider.GITHUB, + owner: 'my-org', + repository: 'my-repo', + branch: 'main', + commitHash: 'abcdef1234567890', + }, + pipelineInfo: { + provider: CiProvider.GITHUB, + jobId: '12345', + // No optional fields + }, + }); + + const template = app.synth().getStackByName('TestStack').template; + expect(template.Metadata.Pipeline.provider).toBe('github'); + expect(template.Metadata.Pipeline.jobId).toBe('12345'); + expect(template.Metadata.Pipeline.jobUrl).toBeUndefined(); + expect(template.Metadata.Pipeline.triggeredBy).toBeUndefined(); + expect(template.Metadata.Pipeline.workflowName).toBeUndefined(); + }); + }); + + 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'; + process.env.MY_CUSTOM_WORKFLOW = 'Custom Workflow'; + }); + + 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', + workflowName: 'MY_CUSTOM_WORKFLOW', + }); + + 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'); + expect(metadata.pipelineInfo.workflowName).toBe('Custom Workflow'); + }); + }); + + describe('repoMetadata and pipelineMetadata', () => { + it('should return repo info', () => { + const metadata = 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 repoInfo = metadata.repoMetadata(); + expect(repoInfo.provider).toBe(CiProvider.GITHUB); + expect(repoInfo.owner).toBe('my-org'); + expect(repoInfo.repository).toBe('my-repo'); + }); + + it('should return pipeline info', () => { + const metadata = new StackMetadata(stack, 'Metadata', { + repoInfo: { + provider: CiProvider.GITHUB, + owner: 'my-org', + repository: 'my-repo', + branch: 'main', + commitHash: 'abcdef1234567890', + }, + pipelineInfo: { + provider: CiProvider.GITHUB, + jobId: '12345', + triggeredBy: 'john-doe', + }, + }); + + const pipelineInfo = metadata.pipelineMetadata(); + expect(pipelineInfo.provider).toBe(CiProvider.GITHUB); + expect(pipelineInfo.jobId).toBe('12345'); + expect(pipelineInfo.triggeredBy).toBe('john-doe'); + }); + }); + + describe('additionalInfo handling', () => { + it('should include additionalInfo when present', () => { + new StackMetadata(stack, 'Metadata', { + repoInfo: { + provider: CiProvider.GITHUB, + owner: 'my-org', + repository: 'my-repo', + branch: 'main', + commitHash: 'abcdef1234567890', + }, + pipelineInfo: { + provider: CiProvider.GITHUB, + jobId: '12345', + additionalInfo: { + customKey1: 'value1', + customKey2: 'value2', + }, + }, + }); + + const template = app.synth().getStackByName('TestStack').template; + expect(template.Metadata.Pipeline.additionalInfo).toEqual({ + customKey1: 'value1', + customKey2: 'value2', + }); + }); + + it('should not include additionalInfo when empty', () => { + new StackMetadata(stack, 'Metadata', { + repoInfo: { + provider: CiProvider.GITHUB, + owner: 'my-org', + repository: 'my-repo', + branch: 'main', + commitHash: 'abcdef1234567890', + }, + pipelineInfo: { + provider: CiProvider.GITHUB, + jobId: '12345', + additionalInfo: {}, + }, + }); + + const template = app.synth().getStackByName('TestStack').template; + expect(template.Metadata.Pipeline.additionalInfo).toBeUndefined(); + }); + }); +}); From 3e0222df47d20d05f91ee29c624970e07dfa92ab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:16:30 +0000 Subject: [PATCH 2/5] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- package-lock.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index c3bd71b..01adecc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,6 +125,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2344,6 +2345,7 @@ "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2414,6 +2416,7 @@ "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -2891,6 +2894,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3813,6 +3817,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4298,7 +4303,8 @@ "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/conventional-changelog": { "version": "4.0.0", @@ -5202,6 +5208,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5372,6 +5379,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7334,6 +7342,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -9402,6 +9411,7 @@ "integrity": "sha512-BMsFDilBLpSzIEdK38kYY4x0w4U5qZeLqOTiZUiyOwe9GsHZSfLCHWJ7TvTAAeBF36nnOzxSySL6+/Hp0N7pTQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@jsii/check-node": "^1.121.0", "@jsii/spec": "^1.121.0", @@ -15522,6 +15532,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15731,6 +15742,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15796,6 +15808,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, From be622d251f8d0697493f5610496912fb5440213d Mon Sep 17 00:00:00 2001 From: Thorsten Hoeger Date: Sat, 20 Dec 2025 00:01:24 +0100 Subject: [PATCH 3/5] progress --- API.md | 135 -------------------------- METADATA_USAGE.md => docs/METADATA.md | 24 +---- examples/metadata-example.ts | 58 ----------- package-lock.json | 15 +-- src/metadata/pipeline-info.ts | 60 ------------ src/metadata/stack-metadata.ts | 48 +-------- src/metadata/types.ts | 23 ----- test/metadata/pipeline-info.test.ts | 67 ------------- test/metadata/stack-metadata.test.ts | 103 +------------------- 9 files changed, 10 insertions(+), 523 deletions(-) rename METADATA_USAGE.md => docs/METADATA.md (88%) delete mode 100644 examples/metadata-example.ts diff --git a/API.md b/API.md index d74cea4..50112e2 100644 --- a/API.md +++ b/API.md @@ -84,8 +84,6 @@ new StackMetadata(scope: Construct, id: string, props?: StackMetadataProps) | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | -| pipelineMetadata | Get the pipeline information as a plain object. | -| repoMetadata | Get the repository information as a plain object. | --- @@ -97,22 +95,6 @@ public toString(): string Returns a string representation of this construct. -##### `pipelineMetadata` - -```typescript -public pipelineMetadata(): PipelineInfo -``` - -Get the pipeline information as a plain object. - -##### `repoMetadata` - -```typescript -public repoMetadata(): RepoInfo -``` - -Get the repository information as a plain object. - #### Static Functions | **Name** | **Description** | @@ -717,7 +699,6 @@ const customEnvVarConfig: CustomEnvVarConfig = { ... } | 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. | -| workflowName | string | Custom environment variable for workflow name. | --- @@ -805,18 +786,6 @@ Custom environment variable for triggered by user. --- -##### `workflowName`Optional - -```typescript -public readonly workflowName: string; -``` - -- *Type:* string - -Custom environment variable for workflow name. - ---- - ### GitInfo Git repository information. @@ -1229,14 +1198,10 @@ const pipelineInfo: PipelineInfo = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | | provider | CiProvider | CI/CD provider. | -| additionalInfo | {[ key: string ]: string} | Additional provider-specific information. | -| event | string | Event that triggered the workflow. | | jobId | string | Job/build ID. | | jobUrl | string | Job/build URL. | -| runAttempt | string | Run attempt (for retries). | | runNumber | string | Run number. | | triggeredBy | string | User who triggered the job. | -| workflowName | string | Workflow/pipeline name. | --- @@ -1252,30 +1217,6 @@ CI/CD provider. --- -##### `additionalInfo`Optional - -```typescript -public readonly additionalInfo: {[ key: string ]: string}; -``` - -- *Type:* {[ key: string ]: string} - -Additional provider-specific information. - ---- - -##### `event`Optional - -```typescript -public readonly event: string; -``` - -- *Type:* string - -Event that triggered the workflow. - ---- - ##### `jobId`Optional ```typescript @@ -1300,18 +1241,6 @@ Job/build URL. --- -##### `runAttempt`Optional - -```typescript -public readonly runAttempt: string; -``` - -- *Type:* string - -Run attempt (for retries). - ---- - ##### `runNumber`Optional ```typescript @@ -1336,18 +1265,6 @@ User who triggered the job. --- -##### `workflowName`Optional - -```typescript -public readonly workflowName: string; -``` - -- *Type:* string - -Workflow/pipeline name. - ---- - ### PipelineInfoProps Props for creating PipelineInfo. @@ -1365,14 +1282,10 @@ const pipelineInfoProps: PipelineInfoProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | | provider | CiProvider | CI/CD provider. | -| additionalInfo | {[ key: string ]: string} | Additional provider-specific information. | -| event | string | Event that triggered the workflow. | | jobId | string | Job/build ID. | | jobUrl | string | Job/build URL. | -| runAttempt | string | Run attempt (for retries). | | runNumber | string | Run number. | | triggeredBy | string | User who triggered the job. | -| workflowName | string | Workflow/pipeline name. | --- @@ -1388,30 +1301,6 @@ CI/CD provider. --- -##### `additionalInfo`Optional - -```typescript -public readonly additionalInfo: {[ key: string ]: string}; -``` - -- *Type:* {[ key: string ]: string} - -Additional provider-specific information. - ---- - -##### `event`Optional - -```typescript -public readonly event: string; -``` - -- *Type:* string - -Event that triggered the workflow. - ---- - ##### `jobId`Optional ```typescript @@ -1436,18 +1325,6 @@ Job/build URL. --- -##### `runAttempt`Optional - -```typescript -public readonly runAttempt: string; -``` - -- *Type:* string - -Run attempt (for retries). - ---- - ##### `runNumber`Optional ```typescript @@ -1472,18 +1349,6 @@ User who triggered the job. --- -##### `workflowName`Optional - -```typescript -public readonly workflowName: string; -``` - -- *Type:* string - -Workflow/pipeline name. - ---- - ### RepoInfo Repository information. diff --git a/METADATA_USAGE.md b/docs/METADATA.md similarity index 88% rename from METADATA_USAGE.md rename to docs/METADATA.md index d2813da..050da9f 100644 --- a/METADATA_USAGE.md +++ b/docs/METADATA.md @@ -51,7 +51,6 @@ new StackMetadata(stack, 'Metadata', { jobId: 'MY_CUSTOM_JOB_ID', jobUrl: 'MY_CUSTOM_JOB_URL', triggeredBy: 'MY_CUSTOM_USER', - workflowName: 'MY_CUSTOM_WORKFLOW', }, }); ``` @@ -76,7 +75,6 @@ new StackMetadata(stack, 'Metadata', { jobId: '12345', jobUrl: 'https://github.com/my-org/my-repo/actions/runs/12345', triggeredBy: 'developer@example.com', - workflowName: 'Production Deployment', runNumber: '42', }, }); @@ -109,10 +107,7 @@ Automatically detected when `GITHUB_ACTIONS=true`. Extracts: - Job ID from `GITHUB_RUN_ID` - Job URL (automatically constructed) - Triggered by from `GITHUB_ACTOR` -- Workflow name from `GITHUB_WORKFLOW` - Run number from `GITHUB_RUN_NUMBER` -- Run attempt from `GITHUB_RUN_ATTEMPT` -- Event from `GITHUB_EVENT_NAME` ### GitLab CI @@ -128,9 +123,7 @@ Automatically detected when `GITLAB_CI=true`. Extracts: - 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` -- Workflow name from `CI_PROJECT_NAME` - Run number from `CI_PIPELINE_IID` -- Event from `CI_PIPELINE_SOURCE` ### AWS CodeBuild @@ -145,7 +138,6 @@ Automatically detected when `CODEBUILD_BUILD_ID` is present. Extracts: - Job ID from `CODEBUILD_BUILD_ID` - Job URL (automatically constructed from ARN) - Triggered by from `CODEBUILD_INITIATOR` -- Workflow name parsed from `CODEBUILD_BUILD_ARN` - Build number from `CODEBUILD_BUILD_NUMBER` ### Generic CI/CD @@ -158,7 +150,6 @@ Fallback support for any CI/CD system using standard environment variables: - `BUILD_ID`, `JOB_ID` - `BUILD_URL`, `JOB_URL` - `BUILD_USER`, `USER` -- `WORKFLOW_NAME`, `PIPELINE_NAME` ## Output @@ -179,16 +170,7 @@ The construct adds metadata to your CloudFormation template: "jobId": "12345", "jobUrl": "https://github.com/open-constructs/cdk-devops/actions/runs/12345", "triggeredBy": "developer@example.com", - "workflowName": "CI Pipeline", - "runNumber": "42", - "runAttempt": "1", - "event": "push", - "additionalInfo": { - "job": "build", - "action": "build-action", - "ref": "refs/heads/main", - "sha": "abc123def456789" - } + "runNumber": "42" } } } @@ -201,10 +183,10 @@ You can access the extracted metadata programmatically: ```typescript const metadata = new StackMetadata(stack, 'Metadata'); -const repoInfo = metadata.repoMetadata(); +const repoInfo = metadata.repoInfo; console.log(`Deployed from ${repoInfo.owner}/${repoInfo.repository}@${repoInfo.commitHash}`); -const pipelineInfo = metadata.pipelineMetadata(); +const pipelineInfo = metadata.pipelineInfo; console.log(`Build triggered by ${pipelineInfo.triggeredBy}`); ``` diff --git a/examples/metadata-example.ts b/examples/metadata-example.ts deleted file mode 100644 index 4b0ff4f..0000000 --- a/examples/metadata-example.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as cdk from 'aws-cdk-lib'; -import { StackMetadata, CiProvider } from '../lib'; - -// Example 1: Automatically extract from environment -const app = new cdk.App(); - -const stack1 = new cdk.Stack(app, 'Stack1', { - stackName: 'my-stack-auto', -}); - -// This will automatically extract repo and pipeline info from environment variables -new StackMetadata(stack1, 'Metadata'); - -// Example 2: Use custom environment variable names -const stack2 = new cdk.Stack(app, 'Stack2', { - stackName: 'my-stack-custom', -}); - -new StackMetadata(stack2, 'Metadata', { - customEnvVars: { - repoOwner: 'MY_CUSTOM_OWNER', - repoName: 'MY_CUSTOM_REPO', - branch: 'MY_CUSTOM_BRANCH', - commitHash: 'MY_CUSTOM_COMMIT', - jobId: 'MY_CUSTOM_JOB_ID', - }, -}); - -// Example 3: Manually provide information -const stack3 = new cdk.Stack(app, 'Stack3', { - stackName: 'my-stack-manual', -}); - -new StackMetadata(stack3, 'Metadata', { - repoInfo: { - provider: CiProvider.GITHUB, - owner: 'open-constructs', - repository: 'cdk-devops', - branch: 'main', - commitHash: 'abc123def456', - }, - pipelineInfo: { - provider: CiProvider.GITHUB, - jobId: '12345', - jobUrl: 'https://github.com/open-constructs/cdk-devops/actions/runs/12345', - triggeredBy: 'developer@example.com', - workflowName: 'CI Pipeline', - }, -}); - -// Example 4: Using the static fromEnvironment method -const stack4 = new cdk.Stack(app, 'Stack4', { - stackName: 'my-stack-from-env', -}); - -StackMetadata.fromEnvironment(stack4, 'Metadata'); - -app.synth(); diff --git a/package-lock.json b/package-lock.json index 01adecc..c3bd71b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,7 +125,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2345,7 +2344,6 @@ "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2416,7 +2414,6 @@ "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -2894,7 +2891,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3817,7 +3813,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4303,8 +4298,7 @@ "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/conventional-changelog": { "version": "4.0.0", @@ -5208,7 +5202,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5379,7 +5372,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7342,7 +7334,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -9411,7 +9402,6 @@ "integrity": "sha512-BMsFDilBLpSzIEdK38kYY4x0w4U5qZeLqOTiZUiyOwe9GsHZSfLCHWJ7TvTAAeBF36nnOzxSySL6+/Hp0N7pTQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@jsii/check-node": "^1.121.0", "@jsii/spec": "^1.121.0", @@ -15532,7 +15522,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15742,7 +15731,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15808,7 +15796,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, diff --git a/src/metadata/pipeline-info.ts b/src/metadata/pipeline-info.ts index cdfacac..53c994a 100644 --- a/src/metadata/pipeline-info.ts +++ b/src/metadata/pipeline-info.ts @@ -25,30 +25,11 @@ export interface PipelineInfoProps { */ readonly triggeredBy?: string; - /** - * Workflow/pipeline name - */ - readonly workflowName?: string; - /** * Run number */ readonly runNumber?: string; - /** - * Run attempt (for retries) - */ - readonly runAttempt?: string; - - /** - * Event that triggered the workflow - */ - readonly event?: string; - - /** - * Additional provider-specific information - */ - readonly additionalInfo?: Record; } /** @@ -64,11 +45,7 @@ export class PipelineInfoHelper { jobId: props.jobId, jobUrl: props.jobUrl, triggeredBy: props.triggeredBy, - workflowName: props.workflowName, runNumber: props.runNumber, - runAttempt: props.runAttempt, - event: props.event, - additionalInfo: props.additionalInfo, }; } @@ -107,16 +84,7 @@ export class PipelineInfoHelper { jobId: this.getEnvVar(customEnvVars?.jobId, 'GITHUB_RUN_ID'), jobUrl, triggeredBy: this.getEnvVar(customEnvVars?.triggeredBy, 'GITHUB_ACTOR', 'GITHUB_TRIGGERING_ACTOR'), - workflowName: this.getEnvVar(customEnvVars?.workflowName, 'GITHUB_WORKFLOW'), runNumber: process.env.GITHUB_RUN_NUMBER, - runAttempt: process.env.GITHUB_RUN_ATTEMPT, - event: process.env.GITHUB_EVENT_NAME, - additionalInfo: { - job: process.env.GITHUB_JOB || '', - action: process.env.GITHUB_ACTION || '', - ref: process.env.GITHUB_REF || '', - sha: process.env.GITHUB_SHA || '', - }, }); } @@ -134,16 +102,7 @@ export class PipelineInfoHelper { 'CI_COMMIT_AUTHOR', 'GITLAB_USER_NAME', ), - workflowName: this.getEnvVar(customEnvVars?.workflowName, 'CI_PROJECT_NAME', 'CI_PIPELINE_NAME'), runNumber: process.env.CI_PIPELINE_IID, - event: process.env.CI_PIPELINE_SOURCE, - additionalInfo: { - jobName: process.env.CI_JOB_NAME || '', - jobStage: process.env.CI_JOB_STAGE || '', - pipelineId: process.env.CI_PIPELINE_ID || '', - projectPath: process.env.CI_PROJECT_PATH || '', - commitRef: process.env.CI_COMMIT_REF_NAME || '', - }, }); } @@ -160,29 +119,12 @@ export class PipelineInfoHelper { ? `https://${region}.console.aws.amazon.com/codesuite/codebuild/projects/${buildId.split(':')[0]}/build/${encodeURIComponent(buildId)}` : this.getEnvVar(customEnvVars?.jobUrl); - // Extract workflow name from custom env var or build ARN - let workflowName: string | undefined; - if (customEnvVars?.workflowName && process.env[customEnvVars.workflowName]) { - workflowName = process.env[customEnvVars.workflowName]; - } else if (process.env.CODEBUILD_BUILD_ARN) { - workflowName = process.env.CODEBUILD_BUILD_ARN.split(':')[5]?.split('/')[1]; - } - return this.create({ provider: CiProvider.CODEBUILD, jobId: this.getEnvVar(customEnvVars?.jobId, 'CODEBUILD_BUILD_ID'), jobUrl, triggeredBy: this.getEnvVar(customEnvVars?.triggeredBy, 'CODEBUILD_INITIATOR'), - workflowName, runNumber: buildNumber, - additionalInfo: { - buildArn: process.env.CODEBUILD_BUILD_ARN || '', - buildNumber: buildNumber || '', - sourceVersion: process.env.CODEBUILD_SOURCE_VERSION || '', - webhookEvent: process.env.CODEBUILD_WEBHOOK_EVENT || '', - webhookTrigger: process.env.CODEBUILD_WEBHOOK_TRIGGER || '', - publicBuildUrl: process.env.CODEBUILD_PUBLIC_BUILD_URL || '', - }, }); } @@ -195,9 +137,7 @@ export class PipelineInfoHelper { 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'), - workflowName: this.getEnvVar(customEnvVars?.workflowName, 'WORKFLOW_NAME', 'PIPELINE_NAME'), runNumber: process.env.BUILD_NUMBER, - additionalInfo: {}, }); } diff --git a/src/metadata/stack-metadata.ts b/src/metadata/stack-metadata.ts index 623db27..08cf8b1 100644 --- a/src/metadata/stack-metadata.ts +++ b/src/metadata/stack-metadata.ts @@ -82,11 +82,7 @@ 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 { + public static fromEnvironment(scope: Construct, id: string, customEnvVars?: CustomEnvVarConfig): StackMetadata { return new StackMetadata(scope, id, { customEnvVars, }); @@ -135,47 +131,13 @@ export class StackMetadata extends Construct { // 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, }; - if (this.pipelineInfo.jobId) { - pipelineMetadata.jobId = this.pipelineInfo.jobId; - } - if (this.pipelineInfo.jobUrl) { - pipelineMetadata.jobUrl = this.pipelineInfo.jobUrl; - } - if (this.pipelineInfo.triggeredBy) { - pipelineMetadata.triggeredBy = this.pipelineInfo.triggeredBy; - } - if (this.pipelineInfo.workflowName) { - pipelineMetadata.workflowName = this.pipelineInfo.workflowName; - } - if (this.pipelineInfo.runNumber) { - pipelineMetadata.runNumber = this.pipelineInfo.runNumber; - } - if (this.pipelineInfo.runAttempt) { - pipelineMetadata.runAttempt = this.pipelineInfo.runAttempt; - } - if (this.pipelineInfo.event) { - pipelineMetadata.event = this.pipelineInfo.event; - } - if (this.pipelineInfo.additionalInfo && Object.keys(this.pipelineInfo.additionalInfo).length > 0) { - pipelineMetadata.additionalInfo = this.pipelineInfo.additionalInfo; - } - stack.addMetadata(pipelineKey, pipelineMetadata); } - /** - * Get the repository information as a plain object - */ - public repoMetadata(): RepoInfo { - return this.repoInfo; - } - - /** - * Get the pipeline information as a plain object - */ - public pipelineMetadata(): PipelineInfo { - return this.pipelineInfo; - } } diff --git a/src/metadata/types.ts b/src/metadata/types.ts index e0acb95..a1ad59e 100644 --- a/src/metadata/types.ts +++ b/src/metadata/types.ts @@ -62,30 +62,11 @@ export interface PipelineInfo { */ readonly triggeredBy?: string; - /** - * Workflow/pipeline name - */ - readonly workflowName?: string; - /** * Run number */ readonly runNumber?: string; - /** - * Run attempt (for retries) - */ - readonly runAttempt?: string; - - /** - * Event that triggered the workflow - */ - readonly event?: string; - - /** - * Additional provider-specific information - */ - readonly additionalInfo?: Record; } /** @@ -127,8 +108,4 @@ export interface CustomEnvVarConfig { */ readonly triggeredBy?: string; - /** - * Custom environment variable for workflow name - */ - readonly workflowName?: string; } diff --git a/test/metadata/pipeline-info.test.ts b/test/metadata/pipeline-info.test.ts index a393e30..ec1cde4 100644 --- a/test/metadata/pipeline-info.test.ts +++ b/test/metadata/pipeline-info.test.ts @@ -9,24 +9,14 @@ describe('PipelineInfoHelper', () => { jobId: '12345', jobUrl: 'https://github.com/org/repo/actions/runs/12345', triggeredBy: 'user@example.com', - workflowName: 'CI', runNumber: '42', - runAttempt: '1', - event: 'push', - additionalInfo: { - custom: 'value', - }, }); 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.workflowName).toBe('CI'); expect(pipelineInfo.runNumber).toBe('42'); - expect(pipelineInfo.runAttempt).toBe('1'); - expect(pipelineInfo.event).toBe('push'); - expect(pipelineInfo.additionalInfo).toEqual({ custom: 'value' }); }); it('should create PipelineInfo with minimal properties', () => { @@ -50,14 +40,7 @@ describe('PipelineInfoHelper', () => { 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_WORKFLOW = 'CI Pipeline'; process.env.GITHUB_RUN_NUMBER = '42'; - process.env.GITHUB_RUN_ATTEMPT = '1'; - process.env.GITHUB_EVENT_NAME = 'push'; - process.env.GITHUB_JOB = 'build'; - process.env.GITHUB_ACTION = 'build-action'; - process.env.GITHUB_REF = 'refs/heads/main'; - process.env.GITHUB_SHA = 'abcdef1234567890'; const pipelineInfo = PipelineInfoHelper.fromEnvironment(); @@ -65,16 +48,7 @@ describe('PipelineInfoHelper', () => { 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.workflowName).toBe('CI Pipeline'); expect(pipelineInfo.runNumber).toBe('42'); - expect(pipelineInfo.runAttempt).toBe('1'); - expect(pipelineInfo.event).toBe('push'); - expect(pipelineInfo.additionalInfo).toEqual({ - job: 'build', - action: 'build-action', - ref: 'refs/heads/main', - sha: 'abcdef1234567890', - }); }); it('should prefer GITHUB_TRIGGERING_ACTOR over GITHUB_ACTOR', () => { @@ -91,19 +65,16 @@ describe('PipelineInfoHelper', () => { 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'; - process.env.MY_CUSTOM_WORKFLOW = 'Custom Workflow'; const pipelineInfo = PipelineInfoHelper.fromEnvironment({ jobId: 'MY_CUSTOM_JOB_ID', jobUrl: 'MY_CUSTOM_JOB_URL', triggeredBy: 'MY_CUSTOM_TRIGGERED_BY', - workflowName: 'MY_CUSTOM_WORKFLOW', }); expect(pipelineInfo.jobId).toBe('custom-job-id'); expect(pipelineInfo.jobUrl).toBe('https://custom.url/job/123'); expect(pipelineInfo.triggeredBy).toBe('custom-user'); - expect(pipelineInfo.workflowName).toBe('Custom Workflow'); }); }); @@ -118,13 +89,7 @@ describe('PipelineInfoHelper', () => { 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_PROJECT_NAME = 'my-project'; process.env.CI_PIPELINE_IID = '42'; - process.env.CI_PIPELINE_SOURCE = 'push'; - process.env.CI_JOB_NAME = 'build'; - process.env.CI_JOB_STAGE = 'test'; - process.env.CI_PROJECT_PATH = 'group/project'; - process.env.CI_COMMIT_REF_NAME = 'main'; const pipelineInfo = PipelineInfoHelper.fromEnvironment(); @@ -132,16 +97,7 @@ describe('PipelineInfoHelper', () => { expect(pipelineInfo.jobId).toBe('67890'); expect(pipelineInfo.jobUrl).toBe('https://gitlab.com/group/project/-/jobs/67890'); expect(pipelineInfo.triggeredBy).toBe('john.doe'); - expect(pipelineInfo.workflowName).toBe('my-project'); expect(pipelineInfo.runNumber).toBe('42'); - expect(pipelineInfo.event).toBe('push'); - expect(pipelineInfo.additionalInfo).toEqual({ - jobName: 'build', - jobStage: 'test', - pipelineId: '12345', - projectPath: 'group/project', - commitRef: 'main', - }); }); it('should fallback to CI_COMMIT_AUTHOR and GITLAB_USER_NAME', () => { @@ -167,19 +123,16 @@ describe('PipelineInfoHelper', () => { 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'; - process.env.MY_CUSTOM_WORKFLOW = 'Custom Workflow'; const pipelineInfo = PipelineInfoHelper.fromEnvironment({ jobId: 'MY_CUSTOM_JOB_ID', jobUrl: 'MY_CUSTOM_JOB_URL', triggeredBy: 'MY_CUSTOM_TRIGGERED_BY', - workflowName: 'MY_CUSTOM_WORKFLOW', }); expect(pipelineInfo.jobId).toBe('custom-job-id'); expect(pipelineInfo.jobUrl).toBe('https://custom.url/job/123'); expect(pipelineInfo.triggeredBy).toBe('custom-user'); - expect(pipelineInfo.workflowName).toBe('Custom Workflow'); }); }); @@ -193,26 +146,13 @@ describe('PipelineInfoHelper', () => { 'arn:aws:codebuild:us-east-1:123456789012:build/my-project:abc-123'; process.env.CODEBUILD_BUILD_NUMBER = '42'; process.env.CODEBUILD_INITIATOR = 'john-doe'; - process.env.CODEBUILD_SOURCE_VERSION = 'main'; - process.env.CODEBUILD_WEBHOOK_EVENT = 'PULL_REQUEST_CREATED'; - process.env.CODEBUILD_WEBHOOK_TRIGGER = 'pr/123'; - process.env.CODEBUILD_PUBLIC_BUILD_URL = 'https://public.build.url'; 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.workflowName).toBe('my-project'); expect(pipelineInfo.runNumber).toBe('42'); - expect(pipelineInfo.additionalInfo).toEqual({ - buildArn: 'arn:aws:codebuild:us-east-1:123456789012:build/my-project:abc-123', - buildNumber: '42', - sourceVersion: 'main', - webhookEvent: 'PULL_REQUEST_CREATED', - webhookTrigger: 'pr/123', - publicBuildUrl: 'https://public.build.url', - }); }); it('should construct job URL from build ID and ARN', () => { @@ -230,19 +170,16 @@ describe('PipelineInfoHelper', () => { 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'; - process.env.MY_CUSTOM_WORKFLOW = 'Custom Workflow'; const pipelineInfo = PipelineInfoHelper.fromEnvironment({ jobId: 'MY_CUSTOM_JOB_ID', jobUrl: 'MY_CUSTOM_JOB_URL', triggeredBy: 'MY_CUSTOM_TRIGGERED_BY', - workflowName: 'MY_CUSTOM_WORKFLOW', }); expect(pipelineInfo.jobId).toBe('custom-job-id'); expect(pipelineInfo.jobUrl).toBe('https://custom.url/job/123'); expect(pipelineInfo.triggeredBy).toBe('custom-user'); - expect(pipelineInfo.workflowName).toBe('Custom Workflow'); }); }); @@ -251,7 +188,6 @@ describe('PipelineInfoHelper', () => { process.env.BUILD_ID = '12345'; process.env.BUILD_URL = 'https://ci.example.com/build/12345'; process.env.BUILD_USER = 'john-doe'; - process.env.WORKFLOW_NAME = 'CI Pipeline'; process.env.BUILD_NUMBER = '42'; const pipelineInfo = PipelineInfoHelper.fromEnvironment(); @@ -260,7 +196,6 @@ describe('PipelineInfoHelper', () => { expect(pipelineInfo.jobId).toBe('12345'); expect(pipelineInfo.jobUrl).toBe('https://ci.example.com/build/12345'); expect(pipelineInfo.triggeredBy).toBe('john-doe'); - expect(pipelineInfo.workflowName).toBe('CI Pipeline'); expect(pipelineInfo.runNumber).toBe('42'); }); @@ -268,14 +203,12 @@ describe('PipelineInfoHelper', () => { process.env.JOB_ID = '67890'; process.env.JOB_URL = 'https://jenkins.example.com/job/123'; process.env.USER = 'jenkins-user'; - process.env.PIPELINE_NAME = 'Build Pipeline'; 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'); - expect(pipelineInfo.workflowName).toBe('Build Pipeline'); }); it('should handle missing values gracefully', () => { diff --git a/test/metadata/stack-metadata.test.ts b/test/metadata/stack-metadata.test.ts index 3a01097..18131d3 100644 --- a/test/metadata/stack-metadata.test.ts +++ b/test/metadata/stack-metadata.test.ts @@ -26,7 +26,6 @@ describe('StackMetadata', () => { jobId: '12345', jobUrl: 'https://github.com/my-org/my-repo/actions/runs/12345', triggeredBy: 'john-doe', - workflowName: 'CI', runNumber: '42', }, }); @@ -45,7 +44,6 @@ describe('StackMetadata', () => { jobId: '12345', jobUrl: 'https://github.com/my-org/my-repo/actions/runs/12345', triggeredBy: 'john-doe', - workflowName: 'CI', runNumber: '42', }); }); @@ -74,7 +72,7 @@ describe('StackMetadata', () => { expect(template.Metadata.Pipeline).toBeUndefined(); }); - it('should omit undefined optional pipeline fields', () => { + it('should include all pipeline fields even when undefined', () => { new StackMetadata(stack, 'Metadata', { repoInfo: { provider: CiProvider.GITHUB, @@ -86,16 +84,12 @@ describe('StackMetadata', () => { pipelineInfo: { provider: CiProvider.GITHUB, jobId: '12345', - // No optional fields }, }); const template = app.synth().getStackByName('TestStack').template; expect(template.Metadata.Pipeline.provider).toBe('github'); expect(template.Metadata.Pipeline.jobId).toBe('12345'); - expect(template.Metadata.Pipeline.jobUrl).toBeUndefined(); - expect(template.Metadata.Pipeline.triggeredBy).toBeUndefined(); - expect(template.Metadata.Pipeline.workflowName).toBeUndefined(); }); }); @@ -208,7 +202,6 @@ describe('StackMetadata', () => { 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'; - process.env.MY_CUSTOM_WORKFLOW = 'Custom Workflow'; }); it('should use custom environment variables', () => { @@ -220,7 +213,6 @@ describe('StackMetadata', () => { jobId: 'MY_CUSTOM_JOB_ID', jobUrl: 'MY_CUSTOM_JOB_URL', triggeredBy: 'MY_CUSTOM_USER', - workflowName: 'MY_CUSTOM_WORKFLOW', }); expect(metadata.repoInfo.owner).toBe('custom-owner'); @@ -230,100 +222,7 @@ describe('StackMetadata', () => { 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'); - expect(metadata.pipelineInfo.workflowName).toBe('Custom Workflow'); }); }); - describe('repoMetadata and pipelineMetadata', () => { - it('should return repo info', () => { - const metadata = 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 repoInfo = metadata.repoMetadata(); - expect(repoInfo.provider).toBe(CiProvider.GITHUB); - expect(repoInfo.owner).toBe('my-org'); - expect(repoInfo.repository).toBe('my-repo'); - }); - - it('should return pipeline info', () => { - const metadata = new StackMetadata(stack, 'Metadata', { - repoInfo: { - provider: CiProvider.GITHUB, - owner: 'my-org', - repository: 'my-repo', - branch: 'main', - commitHash: 'abcdef1234567890', - }, - pipelineInfo: { - provider: CiProvider.GITHUB, - jobId: '12345', - triggeredBy: 'john-doe', - }, - }); - - const pipelineInfo = metadata.pipelineMetadata(); - expect(pipelineInfo.provider).toBe(CiProvider.GITHUB); - expect(pipelineInfo.jobId).toBe('12345'); - expect(pipelineInfo.triggeredBy).toBe('john-doe'); - }); - }); - - describe('additionalInfo handling', () => { - it('should include additionalInfo when present', () => { - new StackMetadata(stack, 'Metadata', { - repoInfo: { - provider: CiProvider.GITHUB, - owner: 'my-org', - repository: 'my-repo', - branch: 'main', - commitHash: 'abcdef1234567890', - }, - pipelineInfo: { - provider: CiProvider.GITHUB, - jobId: '12345', - additionalInfo: { - customKey1: 'value1', - customKey2: 'value2', - }, - }, - }); - - const template = app.synth().getStackByName('TestStack').template; - expect(template.Metadata.Pipeline.additionalInfo).toEqual({ - customKey1: 'value1', - customKey2: 'value2', - }); - }); - - it('should not include additionalInfo when empty', () => { - new StackMetadata(stack, 'Metadata', { - repoInfo: { - provider: CiProvider.GITHUB, - owner: 'my-org', - repository: 'my-repo', - branch: 'main', - commitHash: 'abcdef1234567890', - }, - pipelineInfo: { - provider: CiProvider.GITHUB, - jobId: '12345', - additionalInfo: {}, - }, - }); - - const template = app.synth().getStackByName('TestStack').template; - expect(template.Metadata.Pipeline.additionalInfo).toBeUndefined(); - }); - }); }); From 96c971282d8785740c93a3a3d877c3f0ddb245e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:03:45 +0000 Subject: [PATCH 4/5] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- package-lock.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index c3bd71b..01adecc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,6 +125,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2344,6 +2345,7 @@ "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2414,6 +2416,7 @@ "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -2891,6 +2894,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3813,6 +3817,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4298,7 +4303,8 @@ "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/conventional-changelog": { "version": "4.0.0", @@ -5202,6 +5208,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5372,6 +5379,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7334,6 +7342,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -9402,6 +9411,7 @@ "integrity": "sha512-BMsFDilBLpSzIEdK38kYY4x0w4U5qZeLqOTiZUiyOwe9GsHZSfLCHWJ7TvTAAeBF36nnOzxSySL6+/Hp0N7pTQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@jsii/check-node": "^1.121.0", "@jsii/spec": "^1.121.0", @@ -15522,6 +15532,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15731,6 +15742,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15796,6 +15808,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, From 5ea7454f841e43fc3dc17dde9b6a01be7f13cfca Mon Sep 17 00:00:00 2001 From: Thorsten Hoeger Date: Sat, 20 Dec 2025 00:06:50 +0100 Subject: [PATCH 5/5] docs --- README.md | 68 +++++++++- docs/VERSIONING.md | 307 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 docs/VERSIONING.md 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/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