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