diff --git a/.projenrc.ts b/.projenrc.ts index 249c0f2..254d523 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -32,14 +32,16 @@ const project = new awscdk.AwsCdkConstructLibrary({ releasableCommits: ReleasableCommits.ofType(['feat', 'fix', 'revert', 'Revert', 'docs']), releaseTrigger: ReleaseTrigger.manual(), devDeps: [ - 'projen-pipelines' - ] + 'projen-pipelines', + ], // deps: [], /* Runtime dependencies of this module. */ // description: undefined, /* The description is just a string that helps people understand the purpose of the package. */ // devDeps: [], /* Build dependencies for this module. */ // packageName: undefined, /* The "name" in package.json. */ }); +project.jest?.addSetupFileAfterEnv('/test/jest.setup.ts'); + new GitHubAssignApprover(project, { approverMapping: [ { author: 'hoegertn', approvers: ['Lock128'] }, diff --git a/API.md b/API.md index 85641f0..b3f2d80 100644 --- a/API.md +++ b/API.md @@ -1,21 +1,42 @@ # API Reference +## Constructs +### VersionOutputs -## Classes - -### Hello +Construct for creating version outputs in CloudFormation and SSM Parameter Store. -#### Initializers +#### Initializers ```typescript -import { Hello } from 'cdk-devops' +import { VersionOutputs } from 'cdk-devops' -new Hello() +new VersionOutputs(scope: Construct, id: string, props: VersionOutputsProps) ``` | **Name** | **Type** | **Description** | | --- | --- | --- | +| scope | constructs.Construct | *No description.* | +| id | string | *No description.* | +| props | VersionOutputsProps | *No description.* | + +--- + +##### `scope`Required + +- *Type:* constructs.Construct + +--- + +##### `id`Required + +- *Type:* string + +--- + +##### `props`Required + +- *Type:* VersionOutputsProps --- @@ -23,17 +44,2338 @@ new Hello() | **Name** | **Description** | | --- | --- | -| sayHello | *No 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. | + +--- + +##### `isConstruct` + +```typescript +import { VersionOutputs } from 'cdk-devops' + +VersionOutputs.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. + +--- + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| node | constructs.Node | The tree node. | +| versionInfo | VersionInfo | The version information. | +| outputs | {[ key: string ]: aws-cdk-lib.CfnOutput} | CloudFormation outputs (if enabled). | +| parameters | {[ key: string ]: aws-cdk-lib.aws_ssm.StringParameter} | SSM Parameters (if enabled). | + +--- + +##### `node`Required + +```typescript +public readonly node: Node; +``` + +- *Type:* constructs.Node + +The tree node. + +--- + +##### `versionInfo`Required + +```typescript +public readonly versionInfo: VersionInfo; +``` + +- *Type:* VersionInfo + +The version information. + +--- + +##### `outputs`Optional + +```typescript +public readonly outputs: {[ key: string ]: CfnOutput}; +``` + +- *Type:* {[ key: string ]: aws-cdk-lib.CfnOutput} + +CloudFormation outputs (if enabled). + +--- + +##### `parameters`Optional + +```typescript +public readonly parameters: {[ key: string ]: StringParameter}; +``` + +- *Type:* {[ key: string ]: aws-cdk-lib.aws_ssm.StringParameter} + +SSM Parameters (if enabled). + +--- + + +## Structs + +### BuildNumberConfig + +Build number configuration. + +#### Initializer + +```typescript +import { BuildNumberConfig } from 'cdk-devops' + +const buildNumberConfig: BuildNumberConfig = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| envVar | string | Environment variable to read build number from. | + +--- + +##### `envVar`Optional + +```typescript +public readonly envVar: string; +``` + +- *Type:* string +- *Default:* 'BUILD_NUMBER' + +Environment variable to read build number from. + +--- + +### CloudFormationOutputConfig + +CloudFormation output configuration. + +#### Initializer + +```typescript +import { CloudFormationOutputConfig } from 'cdk-devops' + +const cloudFormationOutputConfig: CloudFormationOutputConfig = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| enabled | boolean | Whether to create CloudFormation outputs. | +| export | boolean | Whether to export the outputs for cross-stack references. | +| exportNameTemplate | string | Export name template (supports {version}, {environment}, etc.). | + +--- + +##### `enabled`Optional + +```typescript +public readonly enabled: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Whether to create CloudFormation outputs. + +--- + +##### `export`Optional + +```typescript +public readonly export: boolean; +``` + +- *Type:* boolean +- *Default:* false + +Whether to export the outputs for cross-stack references. --- -##### `sayHello` +##### `exportNameTemplate`Optional ```typescript -public sayHello(): string +public readonly exportNameTemplate: string; ``` +- *Type:* string +Export name template (supports {version}, {environment}, etc.). +--- + +### CommitCountConfig + +Commit count configuration. + +#### Initializer + +```typescript +import { CommitCountConfig } from 'cdk-devops' + +const commitCountConfig: CommitCountConfig = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| mode | string | Commit counting mode - 'all': Count all commits - 'branch': Count commits on current branch - 'since-tag': Count commits since last tag. | +| padding | number | Padding for commit count (e.g., 5 means '00042'). | + +--- + +##### `mode`Optional + +```typescript +public readonly mode: string; +``` + +- *Type:* string +- *Default:* 'all' + +Commit counting mode - 'all': Count all commits - 'branch': Count commits on current branch - 'since-tag': Count commits since last tag. + +--- + +##### `padding`Optional + +```typescript +public readonly padding: number; +``` + +- *Type:* number +- *Default:* 0 + +Padding for commit count (e.g., 5 means '00042'). + +--- + +### ComputationContext + +Context for version computation. + +#### Initializer + +```typescript +import { ComputationContext } from 'cdk-devops' + +const computationContext: ComputationContext = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| environment | string | Environment/stage name. | +| gitInfo | GitInfo | Git information. | +| buildNumber | string | Build number. | +| deploymentTime | string | Deployment timestamp. | +| packageVersion | string | Package version from package.json. | +| pipelineVersion | string | Pipeline version/execution ID. | +| repositoryUrl | string | Repository URL. | + +--- + +##### `environment`Required + +```typescript +public readonly environment: string; +``` + +- *Type:* string + +Environment/stage name. + +--- + +##### `gitInfo`Required + +```typescript +public readonly gitInfo: GitInfo; +``` + +- *Type:* GitInfo + +Git information. + +--- + +##### `buildNumber`Optional + +```typescript +public readonly buildNumber: string; +``` + +- *Type:* string + +Build number. + +--- + +##### `deploymentTime`Optional + +```typescript +public readonly deploymentTime: string; +``` + +- *Type:* string + +Deployment timestamp. + +--- + +##### `packageVersion`Optional + +```typescript +public readonly packageVersion: string; +``` + +- *Type:* string + +Package version from package.json. + +--- + +##### `pipelineVersion`Optional + +```typescript +public readonly pipelineVersion: string; +``` + +- *Type:* string + +Pipeline version/execution ID. + +--- + +##### `repositoryUrl`Optional + +```typescript +public readonly repositoryUrl: string; +``` + +- *Type:* string + +Repository URL. + +--- + +### GitInfo + +Git repository information. + +#### Initializer + +```typescript +import { GitInfo } from 'cdk-devops' + +const gitInfo: GitInfo = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| branch | string | Current branch name. | +| commitCount | number | Total commit count. | +| commitHash | string | Full commit hash. | +| shortCommitHash | string | Short commit hash (typically 8 characters). | +| 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. + +--- + +##### `shortCommitHash`Required + +```typescript +public readonly shortCommitHash: string; +``` + +- *Type:* string + +Short commit hash (typically 8 characters). + +--- + +##### `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). + +--- + +### 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'). + +--- + +### 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. + +--- + +### VersionInfoProps + +Props for creating VersionInfo. + +#### Initializer + +```typescript +import { VersionInfoProps } from 'cdk-devops' + +const versionInfoProps: VersionInfoProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| environment | string | Environment/stage name. | +| gitInfo | GitInfo | Git information. | +| version | string | Computed version string. | +| buildNumber | string | Build number. | +| deploymentTime | string | Deployment timestamp. | +| deploymentUser | string | Deployment username. | +| packageVersion | string | Package version from package.json. | +| pipelineVersion | string | Pipeline version/execution ID. | +| repositoryUrl | string | Repository URL. | + +--- + +##### `environment`Required + +```typescript +public readonly environment: string; +``` + +- *Type:* string + +Environment/stage name. + +--- + +##### `gitInfo`Required + +```typescript +public readonly gitInfo: GitInfo; +``` + +- *Type:* GitInfo + +Git information. + +--- + +##### `version`Required + +```typescript +public readonly version: string; +``` + +- *Type:* string + +Computed version string. + +--- + +##### `buildNumber`Optional + +```typescript +public readonly buildNumber: string; +``` + +- *Type:* string + +Build number. + +--- + +##### `deploymentTime`Optional + +```typescript +public readonly deploymentTime: string; +``` + +- *Type:* string + +Deployment timestamp. + +--- + +##### `deploymentUser`Optional + +```typescript +public readonly deploymentUser: string; +``` + +- *Type:* string + +Deployment username. + +--- + +##### `packageVersion`Optional + +```typescript +public readonly packageVersion: string; +``` + +- *Type:* string + +Package version from package.json. + +--- + +##### `pipelineVersion`Optional + +```typescript +public readonly pipelineVersion: string; +``` + +- *Type:* string + +Pipeline version/execution ID. + +--- + +##### `repositoryUrl`Optional + +```typescript +public readonly repositoryUrl: string; +``` + +- *Type:* string + +Repository URL. + +--- + +### VersioningConfig + +Versioning configuration. + +#### Initializer + +```typescript +import { VersioningConfig } from 'cdk-devops' + +const versioningConfig: VersioningConfig = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| outputs | VersioningOutputsConfig | Output configuration. | +| strategy | IVersioningStrategy | Versioning strategy. | +| enabled | boolean | Whether versioning is enabled. | + +--- + +##### `outputs`Required + +```typescript +public readonly outputs: VersioningOutputsConfig; +``` + +- *Type:* VersioningOutputsConfig + +Output configuration. + +--- + +##### `strategy`Required + +```typescript +public readonly strategy: IVersioningStrategy; +``` + +- *Type:* IVersioningStrategy + +Versioning strategy. + +--- + +##### `enabled`Optional + +```typescript +public readonly enabled: boolean; +``` + +- *Type:* boolean +- *Default:* true + +Whether versioning is enabled. + +--- + +### VersioningOutputsConfig + +Output configuration for version information. + +#### Initializer + +```typescript +import { VersioningOutputsConfig } from 'cdk-devops' + +const versioningOutputsConfig: VersioningOutputsConfig = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| cloudFormation | CloudFormationOutputConfig | CloudFormation output configuration. | +| parameterStore | ParameterStoreOutputConfig | SSM Parameter Store configuration. | + +--- + +##### `cloudFormation`Optional + +```typescript +public readonly cloudFormation: CloudFormationOutputConfig; +``` + +- *Type:* CloudFormationOutputConfig + +CloudFormation output configuration. + +--- + +##### `parameterStore`Optional + +```typescript +public readonly parameterStore: ParameterStoreOutputConfig; +``` + +- *Type:* ParameterStoreOutputConfig + +SSM Parameter Store configuration. + +--- + +### VersioningStrategyComponents + +Components that can be included in a versioning strategy. + +#### Initializer + +```typescript +import { VersioningStrategyComponents } from 'cdk-devops' + +const versioningStrategyComponents: VersioningStrategyComponents = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| buildNumber | BuildNumberConfig | Build number configuration. | +| commitCount | CommitCountConfig | Commit count configuration. | +| gitTag | GitTagConfig | Git tag configuration. | +| packageJson | PackageJsonConfig | Package.json version configuration. | + +--- + +##### `buildNumber`Optional + +```typescript +public readonly buildNumber: BuildNumberConfig; +``` + +- *Type:* BuildNumberConfig + +Build number configuration. + +--- + +##### `commitCount`Optional + +```typescript +public readonly commitCount: CommitCountConfig; +``` + +- *Type:* CommitCountConfig + +Commit count configuration. + +--- + +##### `gitTag`Optional + +```typescript +public readonly gitTag: GitTagConfig; +``` + +- *Type:* GitTagConfig + +Git tag configuration. + +--- + +##### `packageJson`Optional + +```typescript +public readonly packageJson: PackageJsonConfig; +``` + +- *Type:* PackageJsonConfig + +Package.json version configuration. + +--- + +### VersionOutputsProps + +Props for VersionOutputs construct. + +#### Initializer + +```typescript +import { VersionOutputsProps } from 'cdk-devops' + +const versionOutputsProps: VersionOutputsProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| versionInfo | VersionInfo | Version information to output. | +| cloudFormation | CloudFormationOutputConfig | CloudFormation output configuration. | +| metadataKey | string | Metadata key. | +| outputPrefix | string | Prefix for output names. | +| parameterStore | ParameterStoreOutputConfig | SSM Parameter Store configuration. | + +--- + +##### `versionInfo`Required + +```typescript +public readonly versionInfo: VersionInfo; +``` + +- *Type:* VersionInfo + +Version information to output. + +--- + +##### `cloudFormation`Optional + +```typescript +public readonly cloudFormation: CloudFormationOutputConfig; +``` + +- *Type:* CloudFormationOutputConfig +- *Default:* CloudFormation outputs enabled + +CloudFormation output configuration. + +--- + +##### `metadataKey`Optional + +```typescript +public readonly metadataKey: string; +``` + +- *Type:* string +- *Default:* 'Version' + +Metadata key. + +--- + +##### `outputPrefix`Optional + +```typescript +public readonly outputPrefix: string; +``` + +- *Type:* string +- *Default:* 'Version' + +Prefix for output names. + +--- + +##### `parameterStore`Optional + +```typescript +public readonly parameterStore: ParameterStoreOutputConfig; +``` + +- *Type:* ParameterStoreOutputConfig +- *Default:* Parameter Store disabled + +SSM Parameter Store configuration. + +--- + +## Classes + +### CompositeComputation + +Composite computation strategy that replaces template variables. + +#### Initializers + +```typescript +import { CompositeComputation } from 'cdk-devops' + +new CompositeComputation(strategy: IVersioningStrategy) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| strategy | IVersioningStrategy | *No description.* | + +--- + +##### `strategy`Required + +- *Type:* IVersioningStrategy + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| compute | Compute version by replacing template variables in format string. | + +--- + +##### `compute` + +```typescript +public compute(context: ComputationContext): string +``` + +Compute version by replacing template variables in format string. + +###### `context`Required + +- *Type:* ComputationContext + +--- + + + + +### GitInfoHelper + +Helper class for working with Git information. + +#### Initializers + +```typescript +import { GitInfoHelper } from 'cdk-devops' + +new GitInfoHelper() +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | + +--- + + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| create | Create GitInfo from individual components. | +| fromEnvironment | Create GitInfo from environment variables (CI/CD context). | +| isMainBranch | Check if on a main branch. | +| isTaggedRelease | Check if on a tagged release. | +| shortenHash | Shorten a git commit hash to 8 characters. | + +--- + +##### `create` + +```typescript +import { GitInfoHelper } from 'cdk-devops' + +GitInfoHelper.create(props: GitInfoProps) +``` + +Create GitInfo from individual components. + +###### `props`Required + +- *Type:* GitInfoProps + +--- + +##### `fromEnvironment` + +```typescript +import { GitInfoHelper } from 'cdk-devops' + +GitInfoHelper.fromEnvironment() +``` + +Create GitInfo from environment variables (CI/CD context). + +##### `isMainBranch` + +```typescript +import { GitInfoHelper } from 'cdk-devops' + +GitInfoHelper.isMainBranch(branch: string) +``` + +Check if on a main branch. + +###### `branch`Required + +- *Type:* string + +--- + +##### `isTaggedRelease` + +```typescript +import { GitInfoHelper } from 'cdk-devops' + +GitInfoHelper.isTaggedRelease(gitInfo: GitInfo) +``` + +Check if on a tagged release. + +###### `gitInfo`Required + +- *Type:* GitInfo + +--- + +##### `shortenHash` + +```typescript +import { GitInfoHelper } from 'cdk-devops' + +GitInfoHelper.shortenHash(hash: string, length?: number) +``` + +Shorten a git commit hash to 8 characters. + +###### `hash`Required + +- *Type:* string + +--- + +###### `length`Optional + +- *Type:* number + +--- + + + +### VersionComputationStrategy + +Abstract base class for version computation strategies. + +#### Initializers + +```typescript +import { VersionComputationStrategy } from 'cdk-devops' + +new VersionComputationStrategy() +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| compute | Compute version string from context. | + +--- + +##### `compute` + +```typescript +public compute(context: ComputationContext): string +``` + +Compute version string from context. + +###### `context`Required + +- *Type:* ComputationContext + +--- + + + + +### VersionComputer + +Main version computer class. + +#### Initializers + +```typescript +import { VersionComputer } from 'cdk-devops' + +new VersionComputer(strategy: IVersioningStrategy) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| strategy | IVersioningStrategy | *No description.* | + +--- + +##### `strategy`Required + +- *Type:* IVersioningStrategy + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| compute | Compute version from context. | +| computeVersionString | Compute version string only (without creating VersionInfo). | + +--- + +##### `compute` + +```typescript +public compute(context: ComputationContext): VersionInfo +``` + +Compute version from context. + +###### `context`Required + +- *Type:* ComputationContext + +--- + +##### `computeVersionString` + +```typescript +public computeVersionString(context: ComputationContext): string +``` + +Compute version string only (without creating VersionInfo). + +###### `context`Required + +- *Type:* ComputationContext + +--- + + + + +### VersionInfo + +- *Implements:* IVersionInfo + +Version information for deployments. + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| displayVersion | Get display version (prefers tag if available). | +| exportName | Get export name from template. | +| isMainBranch | Check if deployed from main branch. | +| isTaggedRelease | Check if this is a tagged release. | +| parameterName | Get SSM parameter name from template. | +| toJson | Convert to JSON string. | +| toObject | Convert to plain object. | + +--- + +##### `displayVersion` + +```typescript +public displayVersion(): string +``` + +Get display version (prefers tag if available). + +##### `exportName` + +```typescript +public exportName(template: string): string +``` + +Get export name from template. + +###### `template`Required + +- *Type:* string + +--- + +##### `isMainBranch` + +```typescript +public isMainBranch(): boolean +``` + +Check if deployed from main branch. + +##### `isTaggedRelease` + +```typescript +public isTaggedRelease(): boolean +``` + +Check if this is a tagged release. + +##### `parameterName` + +```typescript +public parameterName(template: string): string +``` + +Get SSM parameter name from template. + +###### `template`Required + +- *Type:* string + +--- + +##### `toJson` + +```typescript +public toJson(): string +``` + +Convert to JSON string. + +##### `toObject` + +```typescript +public toObject(): IVersionInfo +``` + +Convert to plain object. + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| compare | Compare two version infos. | +| create | Create VersionInfo from props. | +| fromEnvironment | Create VersionInfo from environment variables. | +| fromJson | Create VersionInfo from JSON string. | + +--- + +##### `compare` + +```typescript +import { VersionInfo } from 'cdk-devops' + +VersionInfo.compare(a: VersionInfo, b: VersionInfo) +``` + +Compare two version infos. + +###### `a`Required + +- *Type:* VersionInfo + +--- + +###### `b`Required + +- *Type:* VersionInfo + +--- + +##### `create` + +```typescript +import { VersionInfo } from 'cdk-devops' + +VersionInfo.create(props: VersionInfoProps) +``` + +Create VersionInfo from props. + +###### `props`Required + +- *Type:* VersionInfoProps + +--- + +##### `fromEnvironment` + +```typescript +import { VersionInfo } from 'cdk-devops' + +VersionInfo.fromEnvironment(version: string, environment: string) +``` + +Create VersionInfo from environment variables. + +###### `version`Required + +- *Type:* string + +--- + +###### `environment`Required + +- *Type:* string + +--- + +##### `fromJson` + +```typescript +import { VersionInfo } from 'cdk-devops' + +VersionInfo.fromJson(json: string) +``` + +Create VersionInfo from JSON string. + +###### `json`Required + +- *Type:* string + +--- + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| branch | string | Git branch name. | +| commitCount | number | Total commit count. | +| commitHash | string | Git commit hash. | +| deploymentTime | string | Deployment timestamp. | +| deploymentUser | string | Deployment username. | +| environment | string | Environment/stage name. | +| shortCommitHash | string | Git commit hash (short form, typically 8 characters). | +| version | string | Computed version string. | +| buildNumber | string | Build number (if available). | +| packageVersion | string | Package version from package.json (if available). | +| pipelineVersion | string | Pipeline version/execution ID. | +| repositoryUrl | string | Repository URL. | +| tag | string | Git tag (if available). | + +--- + +##### `branch`Required + +```typescript +public readonly branch: string; +``` + +- *Type:* string + +Git branch name. + +--- + +##### `commitCount`Required + +```typescript +public readonly commitCount: number; +``` + +- *Type:* number + +Total commit count. + +--- + +##### `commitHash`Required + +```typescript +public readonly commitHash: string; +``` + +- *Type:* string + +Git commit hash. + +--- + +##### `deploymentTime`Required + +```typescript +public readonly deploymentTime: string; +``` + +- *Type:* string + +Deployment timestamp. + +--- + +##### `deploymentUser`Required + +```typescript +public readonly deploymentUser: string; +``` + +- *Type:* string + +Deployment username. + +--- + +##### `environment`Required + +```typescript +public readonly environment: string; +``` + +- *Type:* string + +Environment/stage name. + +--- + +##### `shortCommitHash`Required + +```typescript +public readonly shortCommitHash: string; +``` + +- *Type:* string + +Git commit hash (short form, typically 8 characters). + +--- + +##### `version`Required + +```typescript +public readonly version: string; +``` + +- *Type:* string + +Computed version string. + +--- + +##### `buildNumber`Optional + +```typescript +public readonly buildNumber: string; +``` + +- *Type:* string + +Build number (if available). + +--- + +##### `packageVersion`Optional + +```typescript +public readonly packageVersion: string; +``` + +- *Type:* string + +Package version from package.json (if available). + +--- + +##### `pipelineVersion`Optional + +```typescript +public readonly pipelineVersion: string; +``` + +- *Type:* string + +Pipeline version/execution ID. + +--- + +##### `repositoryUrl`Optional + +```typescript +public readonly repositoryUrl: string; +``` + +- *Type:* string + +Repository URL. + +--- + +##### `tag`Optional + +```typescript +public readonly tag: string; +``` + +- *Type:* string + +Git tag (if available). + +--- + + +### VersionInfoBuilder + +Builder for VersionInfo. + +#### Initializers + +```typescript +import { VersionInfoBuilder } from 'cdk-devops' + +new VersionInfoBuilder() +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| buildVersionInfo | Build the VersionInfo instance. | +| withBuildNumber | Set build number. | +| withDeploymentTime | Set deployment time. | +| withDeploymentUser | Set deployment username. | +| withEnvironment | Set environment. | +| withGitInfo | Set git information. | +| withPackageVersion | Set package version. | +| withPipelineVersion | Set pipeline version. | +| withRepositoryUrl | Set repository URL. | +| withVersion | Set version string. | + +--- + +##### `buildVersionInfo` + +```typescript +public buildVersionInfo(): VersionInfo +``` + +Build the VersionInfo instance. + +##### `withBuildNumber` + +```typescript +public withBuildNumber(buildNumber?: string): VersionInfoBuilder +``` + +Set build number. + +###### `buildNumber`Optional + +- *Type:* string + +--- + +##### `withDeploymentTime` + +```typescript +public withDeploymentTime(deploymentTime?: string): VersionInfoBuilder +``` + +Set deployment time. + +###### `deploymentTime`Optional + +- *Type:* string + +--- + +##### `withDeploymentUser` + +```typescript +public withDeploymentUser(deploymentUser?: string): VersionInfoBuilder +``` + +Set deployment username. + +###### `deploymentUser`Optional + +- *Type:* string + +--- + +##### `withEnvironment` + +```typescript +public withEnvironment(environment: string): VersionInfoBuilder +``` + +Set environment. + +###### `environment`Required + +- *Type:* string + +--- + +##### `withGitInfo` + +```typescript +public withGitInfo(gitInfo: GitInfo): VersionInfoBuilder +``` + +Set git information. + +###### `gitInfo`Required + +- *Type:* GitInfo + +--- + +##### `withPackageVersion` + +```typescript +public withPackageVersion(packageVersion?: string): VersionInfoBuilder +``` + +Set package version. + +###### `packageVersion`Optional + +- *Type:* string + +--- + +##### `withPipelineVersion` + +```typescript +public withPipelineVersion(pipelineVersion?: string): VersionInfoBuilder +``` + +Set pipeline version. + +###### `pipelineVersion`Optional + +- *Type:* string + +--- + +##### `withRepositoryUrl` + +```typescript +public withRepositoryUrl(repositoryUrl?: string): VersionInfoBuilder +``` + +Set repository URL. + +###### `repositoryUrl`Optional + +- *Type:* string + +--- + +##### `withVersion` + +```typescript +public withVersion(version: string): VersionInfoBuilder +``` + +Set version string. + +###### `version`Required + +- *Type:* string + +--- + + + + +### VersioningStrategy + +- *Implements:* IVersioningStrategy + +Versioning strategy implementation. + + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| buildNumber | Strategy using build number and commit information Format: build-{commit-count}-{commit-hash:8}. | +| commitCount | Strategy using commit count Format: 0.0.{commit-count}. | +| commitHash | Strategy using commit hash Format: {commit-hash:8}. | +| create | Create a custom versioning strategy. | +| gitTag | Strategy using git tags as version source Format: {git-tag} or {git-tag}-{commit-count} if not on a tag. | +| gitTagWithDevVersions | Strategy combining git tag with commit count for non-tagged commits Format: {git-tag} or {git-tag}-dev.{commit-count}. | +| packageJson | Strategy using package.json version Format: {package-version}. | +| packageWithBranch | Strategy combining package version with branch and commit info Format: {package-version}-{branch}.{commit-count}. | +| semanticWithPatch | Semantic versioning strategy with automatic patch increment Format: {package-version}.{commit-count}. | + +--- + +##### `buildNumber` + +```typescript +import { VersioningStrategy } from 'cdk-devops' + +VersioningStrategy.buildNumber(config?: BuildNumberConfig) +``` + +Strategy using build number and commit information Format: build-{commit-count}-{commit-hash:8}. + +###### `config`Optional + +- *Type:* BuildNumberConfig + +--- + +##### `commitCount` + +```typescript +import { VersioningStrategy } from 'cdk-devops' + +VersioningStrategy.commitCount(config?: CommitCountConfig) +``` + +Strategy using commit count Format: 0.0.{commit-count}. + +###### `config`Optional + +- *Type:* CommitCountConfig + +--- + +##### `commitHash` + +```typescript +import { VersioningStrategy } from 'cdk-devops' + +VersioningStrategy.commitHash(length?: number) +``` + +Strategy using commit hash Format: {commit-hash:8}. + +###### `length`Optional + +- *Type:* number + +--- + +##### `create` + +```typescript +import { VersioningStrategy } from 'cdk-devops' + +VersioningStrategy.create(format: string, components?: VersioningStrategyComponents) +``` + +Create a custom versioning strategy. + +###### `format`Required + +- *Type:* string + +--- + +###### `components`Optional + +- *Type:* VersioningStrategyComponents + +--- + +##### `gitTag` + +```typescript +import { VersioningStrategy } from 'cdk-devops' + +VersioningStrategy.gitTag(config?: GitTagConfig) +``` + +Strategy using git tags as version source Format: {git-tag} or {git-tag}-{commit-count} if not on a tag. + +###### `config`Optional + +- *Type:* GitTagConfig + +--- + +##### `gitTagWithDevVersions` + +```typescript +import { VersioningStrategy } from 'cdk-devops' + +VersioningStrategy.gitTagWithDevVersions(config?: GitTagConfig) +``` + +Strategy combining git tag with commit count for non-tagged commits Format: {git-tag} or {git-tag}-dev.{commit-count}. + +###### `config`Optional + +- *Type:* GitTagConfig + +--- + +##### `packageJson` + +```typescript +import { VersioningStrategy } from 'cdk-devops' + +VersioningStrategy.packageJson(config?: PackageJsonConfig) +``` + +Strategy using package.json version Format: {package-version}. + +###### `config`Optional + +- *Type:* PackageJsonConfig + +--- + +##### `packageWithBranch` + +```typescript +import { VersioningStrategy } from 'cdk-devops' + +VersioningStrategy.packageWithBranch(config?: PackageJsonConfig) +``` + +Strategy combining package version with branch and commit info Format: {package-version}-{branch}.{commit-count}. + +###### `config`Optional + +- *Type:* PackageJsonConfig + +--- + +##### `semanticWithPatch` + +```typescript +import { VersioningStrategy } from 'cdk-devops' + +VersioningStrategy.semanticWithPatch(config?: PackageJsonConfig) +``` + +Semantic versioning strategy with automatic patch increment Format: {package-version}.{commit-count}. + +###### `config`Optional + +- *Type:* PackageJsonConfig + +--- + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| components | VersioningStrategyComponents | Strategy components configuration. | +| format | string | Format string for version computation Supports placeholders: {git-tag}, {package-version}, {commit-count}, {commit-hash}, {branch}, {build-number}. | + +--- + +##### `components`Required + +```typescript +public readonly components: VersioningStrategyComponents; +``` + +- *Type:* VersioningStrategyComponents + +Strategy components configuration. + +--- + +##### `format`Required + +```typescript +public readonly format: string; +``` + +- *Type:* string + +Format string for version computation Supports placeholders: {git-tag}, {package-version}, {commit-count}, {commit-hash}, {branch}, {build-number}. + +--- + + +## Protocols + +### IVersionInfo + +- *Implemented By:* VersionInfo, IVersionInfo + +Version information interface. + + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| branch | string | Git branch name. | +| commitCount | number | Total commit count. | +| commitHash | string | Git commit hash. | +| deploymentTime | string | Deployment timestamp. | +| deploymentUser | string | Deployment username. | +| environment | string | Environment/stage name. | +| shortCommitHash | string | Git commit hash (short form, typically 8 characters). | +| version | string | Computed version string. | +| buildNumber | string | Build number (if available). | +| packageVersion | string | Package version from package.json (if available). | +| pipelineVersion | string | Pipeline version/execution ID. | +| repositoryUrl | string | Repository URL. | +| tag | string | Git tag (if available). | + +--- + +##### `branch`Required + +```typescript +public readonly branch: string; +``` + +- *Type:* string + +Git branch name. + +--- + +##### `commitCount`Required + +```typescript +public readonly commitCount: number; +``` + +- *Type:* number + +Total commit count. + +--- + +##### `commitHash`Required + +```typescript +public readonly commitHash: string; +``` + +- *Type:* string + +Git commit hash. + +--- + +##### `deploymentTime`Required + +```typescript +public readonly deploymentTime: string; +``` + +- *Type:* string + +Deployment timestamp. + +--- + +##### `deploymentUser`Required + +```typescript +public readonly deploymentUser: string; +``` + +- *Type:* string + +Deployment username. + +--- + +##### `environment`Required + +```typescript +public readonly environment: string; +``` + +- *Type:* string + +Environment/stage name. + +--- + +##### `shortCommitHash`Required + +```typescript +public readonly shortCommitHash: string; +``` + +- *Type:* string + +Git commit hash (short form, typically 8 characters). + +--- + +##### `version`Required + +```typescript +public readonly version: string; +``` + +- *Type:* string + +Computed version string. + +--- + +##### `buildNumber`Optional + +```typescript +public readonly buildNumber: string; +``` + +- *Type:* string + +Build number (if available). + +--- + +##### `packageVersion`Optional + +```typescript +public readonly packageVersion: string; +``` + +- *Type:* string + +Package version from package.json (if available). + +--- + +##### `pipelineVersion`Optional + +```typescript +public readonly pipelineVersion: string; +``` + +- *Type:* string + +Pipeline version/execution ID. + +--- + +##### `repositoryUrl`Optional + +```typescript +public readonly repositoryUrl: string; +``` + +- *Type:* string + +Repository URL. + +--- + +##### `tag`Optional + +```typescript +public readonly tag: string; +``` + +- *Type:* string + +Git tag (if available). + +--- + +### IVersioningStrategy + +- *Implemented By:* VersioningStrategy, IVersioningStrategy + +Versioning strategy interface. + + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| components | VersioningStrategyComponents | Strategy components configuration. | +| format | string | Format string for version computation Supports placeholders: {git-tag}, {package-version}, {commit-count}, {commit-hash}, {branch}, {build-number}. | + +--- + +##### `components`Required + +```typescript +public readonly components: VersioningStrategyComponents; +``` + +- *Type:* VersioningStrategyComponents + +Strategy components configuration. + +--- + +##### `format`Required + +```typescript +public readonly format: string; +``` + +- *Type:* string + +Format string for version computation Supports placeholders: {git-tag}, {package-version}, {commit-count}, {commit-hash}, {branch}, {build-number}. + +--- diff --git a/package-lock.json b/package-lock.json index c747e72..f07f5bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -143,6 +143,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", @@ -757,6 +758,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -780,6 +782,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2477,6 +2480,7 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2548,6 +2552,7 @@ "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", @@ -3026,6 +3031,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3965,6 +3971,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4464,7 +4471,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", @@ -5420,6 +5428,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5590,6 +5599,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7618,6 +7628,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -10069,6 +10080,7 @@ "integrity": "sha512-f8Yxe/QCdZGHuA4k/VSEcAPusZxNDVWGyT3sXZL/MAj8ryDMpMF0w44WH++0Y2kR9hXih7DQ5gkrib173YAMSg==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@jsii/check-node": "^1.118.0", "@jsii/spec": "^1.118.0", @@ -16309,6 +16321,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", @@ -16518,6 +16531,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16583,6 +16597,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, diff --git a/package.json b/package.json index 26b86ce..df34a27 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,10 @@ "tsconfig": "tsconfig.dev.json" } ] - } + }, + "setupFilesAfterEnv": [ + "/test/jest.setup.ts" + ] }, "types": "lib/index.d.ts", "stability": "stable", diff --git a/src/index.ts b/src/index.ts index 92c94b8..e384345 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,2 @@ -export class Hello { - public sayHello() { - return 'hello, world!'; - } -} \ No newline at end of file +// Versioning module +export * from './versioning'; \ No newline at end of file diff --git a/src/versioning/computation.ts b/src/versioning/computation.ts new file mode 100644 index 0000000..12c04f8 --- /dev/null +++ b/src/versioning/computation.ts @@ -0,0 +1,266 @@ +import { GitInfo } from './git-info'; +import { IVersioningStrategy } from './types'; +import { VersionInfo, VersionInfoBuilder } from './version-info'; + +/** + * Context for version computation + */ +export interface ComputationContext { + /** + * Git information + */ + readonly gitInfo: GitInfo; + + /** + * Package version from package.json + */ + readonly packageVersion?: string; + + /** + * Environment/stage name + */ + readonly environment: string; + + /** + * Repository URL + */ + readonly repositoryUrl?: string; + + /** + * Build number + */ + readonly buildNumber?: string; + + /** + * Pipeline version/execution ID + */ + readonly pipelineVersion?: string; + + /** + * Deployment timestamp + */ + readonly deploymentTime?: string; +} + +/** + * Abstract base class for version computation strategies + */ +export abstract class VersionComputationStrategy { + /** + * Compute version string from context + */ + public abstract compute(context: ComputationContext): string; + + /** + * Create VersionInfo from computation result + */ + protected createVersionInfo(version: string, context: ComputationContext): VersionInfo { + return new VersionInfoBuilder() + .withVersion(version) + .withGitInfo(context.gitInfo) + .withPackageVersion(context.packageVersion) + .withEnvironment(context.environment) + .withRepositoryUrl(context.repositoryUrl) + .withBuildNumber(context.buildNumber) + .withPipelineVersion(context.pipelineVersion) + .withDeploymentTime(context.deploymentTime) + .buildVersionInfo(); + } +} + +/** + * Composite computation strategy that replaces template variables + */ +export class CompositeComputation extends VersionComputationStrategy { + constructor(private readonly strategy: IVersioningStrategy) { + super(); + } + + /** + * Compute version by replacing template variables in format string + */ + public compute(context: ComputationContext): string { + let version = this.strategy.format; + + // Replace git tag + version = this.replaceGitTag(version, context); + + // Replace package version + version = this.replacePackageVersion(version, context); + + // Replace commit count + version = this.replaceCommitCount(version, context); + + // Replace commit hash + version = this.replaceCommitHash(version, context); + + // Replace branch + version = this.replaceBranch(version, context); + + // Replace build number + version = this.replaceBuildNumber(version, context); + + return version; + } + + /** + * Replace {git-tag} placeholder + */ + private replaceGitTag(format: string, context: ComputationContext): string { + if (!format.includes('{git-tag}')) { + return format; + } + + const config = this.strategy.components.gitTag; + let tag = context.gitInfo.tag; + + if (tag && config?.prefix) { + // Strip prefix if configured + if (tag.startsWith(config.prefix)) { + tag = tag.substring(config.prefix.length); + } + } + + // If no tag and we're counting commits since tag, create dev version + if (!tag && config?.countCommitsSince && context.gitInfo.commitsSinceTag !== undefined) { + const commitCount = this.strategy.components.commitCount; + const padding = commitCount?.padding ?? 0; + const count = context.gitInfo.commitsSinceTag.toString().padStart(padding, '0'); + return format.replace(/{git-tag}/g, `dev.${count}`); + } + + return format.replace(/{git-tag}/g, tag || 'dev'); + } + + /** + * Replace {package-version} placeholder + */ + private replacePackageVersion(format: string, context: ComputationContext): string { + if (!format.includes('{package-version}')) { + return format; + } + + const version = context.packageVersion || '0.0.0'; + return format.replace(/{package-version}/g, version); + } + + /** + * Replace {commit-count} placeholder with optional padding + */ + private replaceCommitCount(format: string, context: ComputationContext): string { + // Match {commit-count} or {commit-count:5} + const pattern = /{commit-count(?::(\d+))?}/g; + const matches = Array.from(format.matchAll(pattern)); + + if (matches.length === 0) { + return format; + } + + let result = format; + const config = this.strategy.components.commitCount; + + for (const match of matches) { + const paddingStr = match[1]; + const padding = paddingStr ? parseInt(paddingStr, 10) : (config?.padding ?? 0); + + let count: number; + if (config?.mode === 'since-tag' && context.gitInfo.commitsSinceTag !== undefined) { + count = context.gitInfo.commitsSinceTag; + } else { + count = context.gitInfo.commitCount; + } + + const formattedCount = count.toString().padStart(padding, '0'); + result = result.replace(match[0], formattedCount); + } + + return result; + } + + /** + * Replace {commit-hash} placeholder with optional length + */ + private replaceCommitHash(format: string, context: ComputationContext): string { + // Match {commit-hash} or {commit-hash:8} + const pattern = /{commit-hash(?::(\d+))?}/g; + const matches = Array.from(format.matchAll(pattern)); + + if (matches.length === 0) { + return format; + } + + let result = format; + for (const match of matches) { + const lengthStr = match[1]; + const length = lengthStr ? parseInt(lengthStr, 10) : 8; + const hash = context.gitInfo.commitHash.substring(0, length); + result = result.replace(match[0], hash); + } + + return result; + } + + /** + * Replace {branch} placeholder + */ + private replaceBranch(format: string, context: ComputationContext): string { + if (!format.includes('{branch}')) { + return format; + } + + // Sanitize branch name for use in versions + const branch = context.gitInfo.branch + .replace(/[^a-zA-Z0-9-]/g, '-') + .replace(/--+/g, '-') + .replace(/^-|-$/g, ''); + + return format.replace(/{branch}/g, branch); + } + + /** + * Replace {build-number} placeholder + */ + private replaceBuildNumber(format: string, context: ComputationContext): string { + if (!format.includes('{build-number}')) { + return format; + } + + const buildNumber = context.buildNumber || '0'; + return format.replace(/{build-number}/g, buildNumber); + } +} + +/** + * Main version computer class + */ +export class VersionComputer { + private readonly computation: CompositeComputation; + + constructor(strategy: IVersioningStrategy) { + this.computation = new CompositeComputation(strategy); + } + + /** + * Compute version from context + */ + public compute(context: ComputationContext): VersionInfo { + const version = this.computation.compute(context); + return new VersionInfoBuilder() + .withVersion(version) + .withGitInfo(context.gitInfo) + .withPackageVersion(context.packageVersion) + .withEnvironment(context.environment) + .withRepositoryUrl(context.repositoryUrl) + .withBuildNumber(context.buildNumber) + .withPipelineVersion(context.pipelineVersion) + .withDeploymentTime(context.deploymentTime) + .buildVersionInfo(); + } + + /** + * Compute version string only (without creating VersionInfo) + */ + public computeVersionString(context: ComputationContext): string { + return this.computation.compute(context); + } +} diff --git a/src/versioning/git-info.ts b/src/versioning/git-info.ts new file mode 100644 index 0000000..405335b --- /dev/null +++ b/src/versioning/git-info.ts @@ -0,0 +1,190 @@ +/** + * Git repository information + */ +export interface GitInfo { + /** + * Full commit hash + */ + readonly commitHash: string; + + /** + * Short commit hash (typically 8 characters) + */ + readonly shortCommitHash: string; + + /** + * Current branch name + */ + readonly branch: string; + + /** + * Git tag (if on a tagged commit) + */ + readonly tag?: string; + + /** + * Total commit count + */ + readonly commitCount: number; + + /** + * Commit count since last tag + */ + readonly commitsSinceTag?: number; +} + +/** + * Props for creating GitInfo + */ +export interface GitInfoProps { + /** + * Full commit hash + */ + readonly commitHash: string; + + /** + * Current branch name + */ + readonly branch: string; + + /** + * Git tag (if on a tagged commit) + */ + readonly tag?: string; + + /** + * Total commit count + */ + readonly commitCount: number; + + /** + * Commit count since last tag + */ + readonly commitsSinceTag?: number; +} + +/** + * Helper class for working with Git information + */ +export class GitInfoHelper { + /** + * Create GitInfo from individual components + */ + public static create(props: GitInfoProps): GitInfo { + return { + commitHash: props.commitHash, + shortCommitHash: this.shortenHash(props.commitHash), + branch: props.branch, + tag: props.tag, + commitCount: props.commitCount, + commitsSinceTag: props.commitsSinceTag, + }; + } + + /** + * Create GitInfo from environment variables (CI/CD context) + */ + public static fromEnvironment(): GitInfo { + // GitHub Actions + if (process.env.GITHUB_SHA) { + return this.create({ + commitHash: process.env.GITHUB_SHA, + branch: this.extractBranchName( + process.env.GITHUB_REF || '', + process.env.GITHUB_HEAD_REF, + ), + tag: this.extractTagName(process.env.GITHUB_REF), + commitCount: parseInt(process.env.COMMIT_COUNT || '0', 10), + commitsSinceTag: process.env.COMMITS_SINCE_TAG + ? parseInt(process.env.COMMITS_SINCE_TAG, 10) + : undefined, + }); + } + + // GitLab CI + if (process.env.CI_COMMIT_SHA) { + return this.create({ + commitHash: process.env.CI_COMMIT_SHA, + branch: process.env.CI_COMMIT_REF_NAME || 'unknown', + tag: process.env.CI_COMMIT_TAG, + commitCount: parseInt(process.env.COMMIT_COUNT || '0', 10), + commitsSinceTag: process.env.COMMITS_SINCE_TAG + ? parseInt(process.env.COMMITS_SINCE_TAG, 10) + : undefined, + }); + } + + // Generic fallback + return this.create({ + commitHash: process.env.GIT_COMMIT || process.env.COMMIT_SHA || 'unknown', + branch: process.env.GIT_BRANCH || process.env.BRANCH || 'unknown', + tag: process.env.GIT_TAG, + commitCount: parseInt(process.env.COMMIT_COUNT || '0', 10), + commitsSinceTag: process.env.COMMITS_SINCE_TAG + ? parseInt(process.env.COMMITS_SINCE_TAG, 10) + : undefined, + }); + } + + /** + * Shorten a git commit hash to 8 characters + */ + public static shortenHash(hash: string, length: number = 8): string { + return hash.substring(0, length); + } + + /** + * Check if on a main branch + */ + public static isMainBranch(branch: string): boolean { + return branch === 'main' || branch === 'master'; + } + + /** + * Check if on a tagged release + */ + public static isTaggedRelease(gitInfo: GitInfo): boolean { + return gitInfo.tag !== 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/pull/123/merge + if (ref.includes('/pull/')) { + return ref; + } + + return ref || 'unknown'; + } + + /** + * Extract tag name from Git ref + */ + private static extractTagName(ref?: string): string | undefined { + if (!ref) { + return undefined; + } + + // Extract from refs/tags/tag-name + if (ref.startsWith('refs/tags/')) { + return ref.substring('refs/tags/'.length); + } + + return undefined; + } +} diff --git a/src/versioning/index.ts b/src/versioning/index.ts new file mode 100644 index 0000000..9ddfe1c --- /dev/null +++ b/src/versioning/index.ts @@ -0,0 +1,17 @@ +// Types +export * from './types'; + +// Git Information +export * from './git-info'; + +// Version Information +export * from './version-info'; + +// Strategies +export * from './strategy'; + +// Computation +export * from './computation'; + +// CDK Constructs +export * from './version-outputs'; diff --git a/src/versioning/strategy.ts b/src/versioning/strategy.ts new file mode 100644 index 0000000..1980cf8 --- /dev/null +++ b/src/versioning/strategy.ts @@ -0,0 +1,148 @@ +import { + IVersioningStrategy, + VersioningStrategyComponents, + GitTagConfig, + PackageJsonConfig, + CommitCountConfig, + BuildNumberConfig, +} from './types'; + +/** + * Versioning strategy implementation + */ +export class VersioningStrategy implements IVersioningStrategy { + /** + * Create a custom versioning strategy + */ + public static create(format: string, components: VersioningStrategyComponents = {}): VersioningStrategy { + return new VersioningStrategy(format, components); + } + + /** + * Strategy using build number and commit information + * Format: build-{commit-count}-{commit-hash:8} + */ + public static buildNumber(config: BuildNumberConfig = {}): VersioningStrategy { + return new VersioningStrategy( + 'build-{commit-count}-{commit-hash:8}', + { + buildNumber: config, + commitCount: { mode: 'all', padding: 0 }, + }, + ); + } + + /** + * Strategy using git tags as version source + * Format: {git-tag} or {git-tag}-{commit-count} if not on a tag + */ + public static gitTag(config: GitTagConfig = {}): VersioningStrategy { + return new VersioningStrategy( + '{git-tag}', + { + gitTag: { + prefix: config.prefix ?? 'v', + pattern: config.pattern ?? '*.*.*', + countCommitsSince: config.countCommitsSince ?? true, + }, + }, + ); + } + + /** + * Strategy using package.json version + * Format: {package-version} + */ + public static packageJson(config: PackageJsonConfig = {}): VersioningStrategy { + return new VersioningStrategy( + '{package-version}', + { + packageJson: { + includePrerelease: config.includePrerelease ?? true, + }, + }, + ); + } + + /** + * Strategy using commit count + * Format: 0.0.{commit-count} + */ + public static commitCount(config: CommitCountConfig = {}): VersioningStrategy { + return new VersioningStrategy( + '0.0.{commit-count}', + { + commitCount: { + mode: config.mode ?? 'all', + padding: config.padding ?? 0, + }, + }, + ); + } + + /** + * Strategy using commit hash + * Format: {commit-hash:8} + */ + public static commitHash(length: number = 8): VersioningStrategy { + return new VersioningStrategy(`{commit-hash:${length}}`, {}); + } + + /** + * Strategy combining git tag with commit count for non-tagged commits + * Format: {git-tag} or {git-tag}-dev.{commit-count} + */ + public static gitTagWithDevVersions(config: GitTagConfig = {}): VersioningStrategy { + return new VersioningStrategy( + '{git-tag}', + { + gitTag: { + prefix: config.prefix ?? 'v', + pattern: config.pattern ?? '*.*.*', + countCommitsSince: true, + }, + commitCount: { mode: 'since-tag', padding: 0 }, + }, + ); + } + + /** + * Strategy combining package version with branch and commit info + * Format: {package-version}-{branch}.{commit-count} + */ + public static packageWithBranch(config: PackageJsonConfig = {}): VersioningStrategy { + return new VersioningStrategy( + '{package-version}-{branch}.{commit-count}', + { + packageJson: { + includePrerelease: config.includePrerelease ?? true, + }, + commitCount: { mode: 'all', padding: 0 }, + }, + ); + } + + /** + * Semantic versioning strategy with automatic patch increment + * Format: {package-version}.{commit-count} + */ + public static semanticWithPatch(config: PackageJsonConfig = {}): VersioningStrategy { + return new VersioningStrategy( + '{package-version}.{commit-count}', + { + packageJson: { + includePrerelease: config.includePrerelease ?? false, + }, + commitCount: { mode: 'all', padding: 0 }, + }, + ); + } + + public readonly format: string; + public readonly components: VersioningStrategyComponents; + + private constructor(format: string, components: VersioningStrategyComponents = {}) { + this.format = format; + this.components = components; + } +} diff --git a/src/versioning/types.ts b/src/versioning/types.ts new file mode 100644 index 0000000..beb1f53 --- /dev/null +++ b/src/versioning/types.ts @@ -0,0 +1,260 @@ +/** + * Git tag configuration for version extraction + */ +export interface GitTagConfig { + /** + * Prefix to strip from git tags (e.g., 'v' for tags like 'v1.2.3') + * @default 'v' + */ + readonly prefix?: string; + + /** + * Pattern to match git tags + * @default '*.*.*' + */ + readonly pattern?: string; + + /** + * Whether to count commits since the last tag + * @default true + */ + readonly countCommitsSince?: boolean; +} + +/** + * Package.json version configuration + */ +export interface PackageJsonConfig { + /** + * Whether to include prerelease identifiers + * @default true + */ + readonly includePrerelease?: boolean; +} + +/** + * Commit count configuration + */ +export interface CommitCountConfig { + /** + * Commit counting mode + * - 'all': Count all commits + * - 'branch': Count commits on current branch + * - 'since-tag': Count commits since last tag + * @default 'all' + */ + readonly mode?: 'all' | 'branch' | 'since-tag'; + + /** + * Padding for commit count (e.g., 5 means '00042') + * @default 0 + */ + readonly padding?: number; +} + +/** + * Build number configuration + */ +export interface BuildNumberConfig { + /** + * Environment variable to read build number from + * @default 'BUILD_NUMBER' + */ + readonly envVar?: string; +} + +/** + * Components that can be included in a versioning strategy + */ +export interface VersioningStrategyComponents { + /** + * Git tag configuration + */ + readonly gitTag?: GitTagConfig; + + /** + * Package.json version configuration + */ + readonly packageJson?: PackageJsonConfig; + + /** + * Commit count configuration + */ + readonly commitCount?: CommitCountConfig; + + /** + * Build number configuration + */ + readonly buildNumber?: BuildNumberConfig; +} + +/** + * Versioning strategy interface + */ +export interface IVersioningStrategy { + /** + * Format string for version computation + * Supports placeholders: {git-tag}, {package-version}, {commit-count}, {commit-hash}, {branch}, {build-number} + */ + readonly format: string; + + /** + * Strategy components configuration + */ + readonly components: VersioningStrategyComponents; +} + +/** + * CloudFormation output configuration + */ +export interface CloudFormationOutputConfig { + /** + * Whether to create CloudFormation outputs + * @default true + */ + readonly enabled?: boolean; + + /** + * Whether to export the outputs for cross-stack references + * @default false + */ + readonly export?: boolean; + + /** + * Export name template (supports {version}, {environment}, etc.) + */ + readonly exportNameTemplate?: string; +} + +/** + * SSM Parameter Store output configuration + */ +export interface ParameterStoreOutputConfig { + /** + * Whether to create SSM parameters + * @default true + */ + readonly enabled?: boolean; + + /** + * Base path for parameters (e.g., '/myapp/version') + */ + readonly basePath?: string; + + /** + * Whether to split version info into separate parameters + * @default false + */ + readonly splitParameters?: boolean; + + /** + * Description for the parameter + */ + readonly description?: string; +} + +/** + * Output configuration for version information + */ +export interface VersioningOutputsConfig { + /** + * CloudFormation output configuration + */ + readonly cloudFormation?: CloudFormationOutputConfig; + + /** + * SSM Parameter Store configuration + */ + readonly parameterStore?: ParameterStoreOutputConfig; +} + +/** + * Version information interface + */ +export interface IVersionInfo { + /** + * Computed version string + */ + readonly version: string; + + /** + * Git commit hash + */ + readonly commitHash: string; + + /** + * Git commit hash (short form, typically 8 characters) + */ + readonly shortCommitHash: string; + + /** + * Git branch name + */ + readonly branch: string; + + /** + * Git tag (if available) + */ + readonly tag?: string; + + /** + * Total commit count + */ + readonly commitCount: number; + + /** + * Package version from package.json (if available) + */ + readonly packageVersion?: string; + + /** + * Deployment timestamp + */ + readonly deploymentTime: string; + + /** + * Deployment username + */ + readonly deploymentUser: string; + + /** + * Environment/stage name + */ + readonly environment: string; + + /** + * Repository URL + */ + readonly repositoryUrl?: string; + + /** + * Build number (if available) + */ + readonly buildNumber?: string; + + /** + * Pipeline version/execution ID + */ + readonly pipelineVersion?: string; +} + +/** + * Versioning configuration + */ +export interface VersioningConfig { + /** + * Whether versioning is enabled + * @default true + */ + readonly enabled?: boolean; + + /** + * Versioning strategy + */ + readonly strategy: IVersioningStrategy; + + /** + * Output configuration + */ + readonly outputs: VersioningOutputsConfig; +} diff --git a/src/versioning/version-info.ts b/src/versioning/version-info.ts new file mode 100644 index 0000000..40c411e --- /dev/null +++ b/src/versioning/version-info.ts @@ -0,0 +1,342 @@ +import { GitInfo, GitInfoHelper } from './git-info'; +import { IVersionInfo } from './types'; + +/** + * Props for creating VersionInfo + */ +export interface VersionInfoProps { + /** + * Computed version string + */ + readonly version: string; + + /** + * Git information + */ + readonly gitInfo: GitInfo; + + /** + * Package version from package.json + */ + readonly packageVersion?: string; + + /** + * Deployment timestamp + */ + readonly deploymentTime?: string; + + /** + * Deployment username + */ + readonly deploymentUser?: string; + + /** + * Environment/stage name + */ + readonly environment: string; + + /** + * Repository URL + */ + readonly repositoryUrl?: string; + + /** + * Build number + */ + readonly buildNumber?: string; + + /** + * Pipeline version/execution ID + */ + readonly pipelineVersion?: string; +} + +/** + * Version information for deployments + */ +export class VersionInfo implements IVersionInfo { + /** + * Create VersionInfo from props + */ + public static create(props: VersionInfoProps): VersionInfo { + return new VersionInfo(props); + } + + /** + * Create VersionInfo from environment variables + */ + public static fromEnvironment(version: string, environment: string): VersionInfo { + const gitInfo = GitInfoHelper.fromEnvironment(); + + return new VersionInfo({ + version, + gitInfo, + environment, + packageVersion: process.env.PACKAGE_VERSION, + deploymentTime: process.env.DEPLOYMENT_TIME || new Date().toISOString(), + deploymentUser: process.env.GITHUB_ACTOR || process.env.GITLAB_USER_LOGIN || process.env.USER || 'unknown', + repositoryUrl: process.env.REPOSITORY_URL || process.env.GITHUB_REPOSITORY + ? `https://github.com/${process.env.GITHUB_REPOSITORY}` + : undefined, + buildNumber: process.env.BUILD_NUMBER || process.env.GITHUB_RUN_NUMBER, + pipelineVersion: process.env.PIPELINE_VERSION || process.env.CODEBUILD_BUILD_ID, + }); + } + + /** + * Create VersionInfo from JSON string + */ + public static fromJson(json: string): VersionInfo { + const data = JSON.parse(json); + return new VersionInfo({ + version: data.version, + gitInfo: { + commitHash: data.commitHash, + shortCommitHash: data.shortCommitHash, + branch: data.branch, + tag: data.tag, + commitCount: data.commitCount, + }, + packageVersion: data.packageVersion, + deploymentTime: data.deploymentTime, + deploymentUser: data.deploymentUser, + environment: data.environment, + repositoryUrl: data.repositoryUrl, + buildNumber: data.buildNumber, + pipelineVersion: data.pipelineVersion, + }); + } + + /** + * Compare two version infos + */ + public static compare(a: VersionInfo, b: VersionInfo): number { + // First compare by commit count + if (a.commitCount !== b.commitCount) { + return a.commitCount - b.commitCount; + } + + // Then by version string + return a.version.localeCompare(b.version); + } + + public readonly version: string; + public readonly commitHash: string; + public readonly shortCommitHash: string; + public readonly branch: string; + public readonly tag?: string; + public readonly commitCount: number; + public readonly packageVersion?: string; + public readonly deploymentTime: string; + public readonly deploymentUser: string; + public readonly environment: string; + public readonly repositoryUrl?: string; + public readonly buildNumber?: string; + public readonly pipelineVersion?: string; + + private constructor(props: VersionInfoProps) { + this.version = props.version; + this.commitHash = props.gitInfo.commitHash; + this.shortCommitHash = props.gitInfo.shortCommitHash; + this.branch = props.gitInfo.branch; + this.tag = props.gitInfo.tag; + this.commitCount = props.gitInfo.commitCount; + this.packageVersion = props.packageVersion; + this.deploymentTime = props.deploymentTime || new Date().toISOString(); + this.deploymentUser = props.deploymentUser || 'unknown'; + this.environment = props.environment; + this.repositoryUrl = props.repositoryUrl; + this.buildNumber = props.buildNumber; + this.pipelineVersion = props.pipelineVersion; + } + + /** + * Get display version (prefers tag if available) + */ + public displayVersion(): string { + return this.tag || this.version; + } + + /** + * Check if this is a tagged release + */ + public isTaggedRelease(): boolean { + return this.tag !== undefined; + } + + /** + * Check if deployed from main branch + */ + public isMainBranch(): boolean { + return GitInfoHelper.isMainBranch(this.branch); + } + + /** + * Convert to JSON string + */ + public toJson(): string { + return JSON.stringify(this, null, 2); + } + + /** + * Convert to plain object + */ + public toObject(): IVersionInfo { + return { + version: this.version, + commitHash: this.commitHash, + shortCommitHash: this.shortCommitHash, + branch: this.branch, + tag: this.tag, + commitCount: this.commitCount, + packageVersion: this.packageVersion, + deploymentTime: this.deploymentTime, + deploymentUser: this.deploymentUser, + environment: this.environment, + repositoryUrl: this.repositoryUrl, + buildNumber: this.buildNumber, + pipelineVersion: this.pipelineVersion, + }; + } + + /** + * Get SSM parameter name from template + */ + public parameterName(template: string): string { + return this.substituteTemplate(template); + } + + /** + * Get export name from template + */ + public exportName(template: string): string { + return this.substituteTemplate(template); + } + + /** + * Substitute template variables + */ + private substituteTemplate(template: string): string { + return template + .replace(/{version}/g, this.version) + .replace(/{environment}/g, this.environment) + .replace(/{branch}/g, this.branch) + .replace(/{commit-hash}/g, this.shortCommitHash) + .replace(/{tag}/g, this.tag || '') + .replace(/{commit-count}/g, this.commitCount.toString()); + } +} + +/** + * Builder for VersionInfo + */ +export class VersionInfoBuilder { + private version?: string; + private gitInfo?: GitInfo; + private packageVersion?: string; + private deploymentTime?: string; + private deploymentUser?: string; + private environment?: string; + private repositoryUrl?: string; + private buildNumber?: string; + private pipelineVersion?: string; + + /** + * Set version string + */ + public withVersion(version: string): this { + this.version = version; + return this; + } + + /** + * Set git information + */ + public withGitInfo(gitInfo: GitInfo): this { + this.gitInfo = gitInfo; + return this; + } + + /** + * Set package version + */ + public withPackageVersion(packageVersion?: string): this { + this.packageVersion = packageVersion; + return this; + } + + /** + * Set deployment time + */ + public withDeploymentTime(deploymentTime?: string): this { + this.deploymentTime = deploymentTime; + return this; + } + + /** + * Set deployment username + */ + public withDeploymentUser(deploymentUser?: string): this { + this.deploymentUser = deploymentUser; + return this; + } + + /** + * Set environment + */ + public withEnvironment(environment: string): this { + this.environment = environment; + return this; + } + + /** + * Set repository URL + */ + public withRepositoryUrl(repositoryUrl?: string): this { + this.repositoryUrl = repositoryUrl; + return this; + } + + /** + * Set build number + */ + public withBuildNumber(buildNumber?: string): this { + this.buildNumber = buildNumber; + return this; + } + + /** + * Set pipeline version + */ + public withPipelineVersion(pipelineVersion?: string): this { + this.pipelineVersion = pipelineVersion; + return this; + } + + /** + * Build the VersionInfo instance + */ + public buildVersionInfo(): VersionInfo { + if (!this.version) { + throw new Error('Version is required'); + } + if (!this.gitInfo) { + throw new Error('GitInfo is required'); + } + if (!this.environment) { + throw new Error('Environment is required'); + } + + return VersionInfo.create({ + version: this.version, + gitInfo: this.gitInfo, + packageVersion: this.packageVersion, + deploymentTime: this.deploymentTime, + deploymentUser: this.deploymentUser, + environment: this.environment, + repositoryUrl: this.repositoryUrl, + buildNumber: this.buildNumber, + pipelineVersion: this.pipelineVersion, + }); + } +} diff --git a/src/versioning/version-outputs.ts b/src/versioning/version-outputs.ts new file mode 100644 index 0000000..fd0f4bf --- /dev/null +++ b/src/versioning/version-outputs.ts @@ -0,0 +1,274 @@ +import * as cdk from 'aws-cdk-lib'; +import { Stack } from 'aws-cdk-lib'; +import * as ssm from 'aws-cdk-lib/aws-ssm'; +import { Construct } from 'constructs'; +import { CloudFormationOutputConfig, ParameterStoreOutputConfig } from './types'; +import { VersionInfo } from './version-info'; + +/** + * Props for VersionOutputs construct + */ +export interface VersionOutputsProps { + /** + * Version information to output + */ + readonly versionInfo: VersionInfo; + + /** + * CloudFormation output configuration + * @default - CloudFormation outputs enabled + */ + readonly cloudFormation?: CloudFormationOutputConfig; + + /** + * SSM Parameter Store configuration + * @default - Parameter Store disabled + */ + readonly parameterStore?: ParameterStoreOutputConfig; + + /** + * Prefix for output names + * @default 'Version' + */ + readonly outputPrefix?: string; + + /** + * Metadata key + * @default 'Version' + */ + readonly metadataKey?: string; +} + +/** + * Construct for creating version outputs in CloudFormation and SSM Parameter Store + */ +export class VersionOutputs extends Construct { + /** + * The version information + */ + public readonly versionInfo: VersionInfo; + + /** + * CloudFormation outputs (if enabled) + */ + public readonly outputs?: { [key: string]: cdk.CfnOutput }; + + /** + * SSM Parameters (if enabled) + */ + public readonly parameters?: { [key: string]: ssm.StringParameter }; + + constructor(scope: Construct, id: string, props: VersionOutputsProps) { + super(scope, id); + + this.versionInfo = props.versionInfo; + + // Create CloudFormation outputs if enabled + if (props.cloudFormation?.enabled !== false) { + this.outputs = this.createCloudFormationOutputs(props); + } + + // Create SSM parameters if enabled + if (props.parameterStore?.enabled === true) { + this.parameters = this.createParameterStoreOutputs(props); + } + + this.createStackMetadataOutputs(props); + } + + /** + * Create stack metadata outputs + */ + private createStackMetadataOutputs(props: VersionOutputsProps): void { + const metadataKey = props.metadataKey || props.outputPrefix || 'Version'; + + Stack.of(this).addMetadata(metadataKey, this.versionInfo.toObject()); + } + + /** + * Create CloudFormation outputs + */ + private createCloudFormationOutputs(props: VersionOutputsProps): { [key: string]: cdk.CfnOutput } { + const prefix = props.outputPrefix || 'Version'; + const outputs: { [key: string]: cdk.CfnOutput } = {}; + + // Main version output + outputs.version = new cdk.CfnOutput(this, `${prefix}String`, { + value: this.versionInfo.version, + description: 'Deployment version', + exportName: this.getExportName(props.cloudFormation, 'version'), + }); + + // Commit hash + outputs.commitHash = new cdk.CfnOutput(this, `${prefix}CommitHash`, { + value: this.versionInfo.commitHash, + description: 'Git commit hash', + exportName: this.getExportName(props.cloudFormation, 'commit-hash'), + }); + + // Branch + outputs.branch = new cdk.CfnOutput(this, `${prefix}Branch`, { + value: this.versionInfo.branch, + description: 'Git branch', + exportName: this.getExportName(props.cloudFormation, 'branch'), + }); + + // Commit count + outputs.commitCount = new cdk.CfnOutput(this, `${prefix}CommitCount`, { + value: this.versionInfo.commitCount.toString(), + description: 'Git commit count', + exportName: this.getExportName(props.cloudFormation, 'commit-count'), + }); + + // Deployment time + outputs.deploymentTime = new cdk.CfnOutput(this, `${prefix}DeploymentTime`, { + value: this.versionInfo.deploymentTime, + description: 'Deployment timestamp', + exportName: this.getExportName(props.cloudFormation, 'deployment-time'), + }); + + // Environment + outputs.environment = new cdk.CfnOutput(this, `${prefix}Environment`, { + value: this.versionInfo.environment, + description: 'Deployment environment', + exportName: this.getExportName(props.cloudFormation, 'environment'), + }); + + // Optional outputs + if (this.versionInfo.tag) { + outputs.tag = new cdk.CfnOutput(this, `${prefix}Tag`, { + value: this.versionInfo.tag, + description: 'Git tag', + exportName: this.getExportName(props.cloudFormation, 'tag'), + }); + } + + if (this.versionInfo.packageVersion) { + outputs.packageVersion = new cdk.CfnOutput(this, `${prefix}PackageVersion`, { + value: this.versionInfo.packageVersion, + description: 'Package version from package.json', + exportName: this.getExportName(props.cloudFormation, 'package-version'), + }); + } + + return outputs; + } + + /** + * Create SSM Parameter Store outputs + */ + private createParameterStoreOutputs(props: VersionOutputsProps): { [key: string]: ssm.StringParameter } { + const basePath = props.parameterStore?.basePath || '/version'; + const parameters: { [key: string]: ssm.StringParameter } = {}; + + if (props.parameterStore?.splitParameters) { + // Create individual parameters for each field + parameters.version = this.createParameter( + 'Version', + `${basePath}/version`, + this.versionInfo.version, + 'Deployment version', + ); + + parameters.commitHash = this.createParameter( + 'CommitHash', + `${basePath}/commit-hash`, + this.versionInfo.commitHash, + 'Git commit hash', + ); + + parameters.branch = this.createParameter( + 'Branch', + `${basePath}/branch`, + this.versionInfo.branch, + 'Git branch', + ); + + parameters.commitCount = this.createParameter( + 'CommitCount', + `${basePath}/commit-count`, + this.versionInfo.commitCount.toString(), + 'Git commit count', + ); + + parameters.deploymentTime = this.createParameter( + 'DeploymentTime', + `${basePath}/deployment-time`, + this.versionInfo.deploymentTime, + 'Deployment timestamp', + ); + + parameters.environment = this.createParameter( + 'Environment', + `${basePath}/environment`, + this.versionInfo.environment, + 'Deployment environment', + ); + + // Optional parameters + if (this.versionInfo.tag) { + parameters.tag = this.createParameter( + 'Tag', + `${basePath}/tag`, + this.versionInfo.tag, + 'Git tag', + ); + } + + if (this.versionInfo.packageVersion) { + parameters.packageVersion = this.createParameter( + 'PackageVersion', + `${basePath}/package-version`, + this.versionInfo.packageVersion, + 'Package version from package.json', + ); + } + } else { + // Create single parameter with JSON + parameters.versionInfo = this.createParameter( + 'VersionInfo', + basePath, + this.versionInfo.toJson(), + props.parameterStore?.description || 'Version information (JSON)', + ); + } + + return parameters; + } + + /** + * Create SSM parameter + */ + private createParameter( + id: string, + parameterName: string, + value: string, + description: string, + ): ssm.StringParameter { + return new ssm.StringParameter(this, id, { + parameterName, + stringValue: value, + description, + tier: ssm.ParameterTier.STANDARD, + }); + } + + /** + * Get export name for CloudFormation output + */ + private getExportName(config?: CloudFormationOutputConfig, suffix?: string): string | undefined { + if (!config?.export) { + return undefined; + } + + if (config.exportNameTemplate) { + let name = this.versionInfo.exportName(config.exportNameTemplate); + if (suffix) { + name = `${name}-${suffix}`; + } + return name; + } + + return undefined; + } +} diff --git a/test/hello.test.ts b/test/hello.test.ts deleted file mode 100644 index acbacd4..0000000 --- a/test/hello.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Hello } from '../src'; - -test('hello', () => { - expect(new Hello().sayHello()).toBe('hello, world!'); -}); \ No newline at end of file diff --git a/test/jest.setup.ts b/test/jest.setup.ts new file mode 100644 index 0000000..0f93ea1 --- /dev/null +++ b/test/jest.setup.ts @@ -0,0 +1,26 @@ +// Clear CI environment variables before tests to ensure isolation +const ciEnvVars = [ + // GitHub Actions + 'GITHUB_SHA', + 'GITHUB_REF', + 'GITHUB_HEAD_REF', + 'GITHUB_ACTIONS', + // GitLab CI + 'CI_COMMIT_SHA', + 'CI_COMMIT_REF_NAME', + 'CI_COMMIT_TAG', + 'GITLAB_CI', + // Generic CI + 'GIT_COMMIT', + 'GIT_BRANCH', + 'GIT_TAG', + // Common + 'COMMIT_COUNT', + 'COMMITS_SINCE_TAG', +]; + +beforeEach(() => { + for (const envVar of ciEnvVars) { + delete process.env[envVar]; + } +}); diff --git a/test/versioning/computation.test.ts b/test/versioning/computation.test.ts new file mode 100644 index 0000000..3f258e9 --- /dev/null +++ b/test/versioning/computation.test.ts @@ -0,0 +1,263 @@ +import { VersionComputer, ComputationContext } from '../../src/versioning/computation'; +import { GitInfoHelper } from '../../src/versioning/git-info'; +import { VersioningStrategy } from '../../src/versioning/strategy'; + +describe('VersionComputer', () => { + const mockContext: ComputationContext = { + gitInfo: GitInfoHelper.create({ + commitHash: 'abcdef1234567890', + branch: 'feature/test-branch', + tag: 'v1.2.3', + commitCount: 42, + commitsSinceTag: 5, + }), + packageVersion: '1.2.3', + environment: 'production', + buildNumber: '123', + }; + + describe('git tag strategy', () => { + it('should use git tag as version', () => { + const strategy = VersioningStrategy.gitTag(); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('1.2.3'); + }); + + it('should strip prefix from git tag', () => { + const strategy = VersioningStrategy.gitTag({ prefix: 'v' }); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('1.2.3'); + }); + + it('should use dev version when no tag', () => { + const strategy = VersioningStrategy.gitTag(); + const computer = new VersionComputer(strategy); + + const contextNoTag = { + ...mockContext, + gitInfo: GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + commitCount: 42, + }), + }; + + const version = computer.computeVersionString(contextNoTag); + + expect(version).toBe('dev'); + }); + + it('should create dev version with commit count', () => { + const strategy = VersioningStrategy.gitTagWithDevVersions(); + const computer = new VersionComputer(strategy); + + const contextNoTag = { + ...mockContext, + gitInfo: GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + commitCount: 42, + commitsSinceTag: 5, + }), + }; + + const version = computer.computeVersionString(contextNoTag); + + expect(version).toBe('dev.5'); + }); + }); + + describe('package.json strategy', () => { + it('should use package version', () => { + const strategy = VersioningStrategy.packageJson(); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('1.2.3'); + }); + + it('should use default version when package version missing', () => { + const strategy = VersioningStrategy.packageJson(); + const computer = new VersionComputer(strategy); + + const contextNoPackage = { + ...mockContext, + packageVersion: undefined, + }; + + const version = computer.computeVersionString(contextNoPackage); + + expect(version).toBe('0.0.0'); + }); + }); + + describe('commit count strategy', () => { + it('should use commit count', () => { + const strategy = VersioningStrategy.commitCount(); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('0.0.42'); + }); + + it('should pad commit count', () => { + const strategy = VersioningStrategy.commitCount({ padding: 5 }); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('0.0.00042'); + }); + + it('should use commits since tag when mode is since-tag', () => { + const strategy = VersioningStrategy.create('0.0.{commit-count}', { + commitCount: { mode: 'since-tag' }, + }); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('0.0.5'); + }); + }); + + describe('commit hash strategy', () => { + it('should use short commit hash', () => { + const strategy = VersioningStrategy.commitHash(); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('abcdef12'); + }); + + it('should use custom hash length', () => { + const strategy = VersioningStrategy.commitHash(12); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('abcdef123456'); + }); + }); + + describe('build number strategy', () => { + it('should use build number in version', () => { + const strategy = VersioningStrategy.buildNumber(); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('build-42-abcdef12'); + }); + }); + + describe('branch replacement', () => { + it('should sanitize branch name', () => { + const strategy = VersioningStrategy.create('{branch}'); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('feature-test-branch'); + }); + + it('should remove special characters from branch', () => { + const strategy = VersioningStrategy.create('{branch}'); + const computer = new VersionComputer(strategy); + + const contextSpecialBranch = { + ...mockContext, + gitInfo: GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'feature/test@branch#123', + commitCount: 42, + }), + }; + + const version = computer.computeVersionString(contextSpecialBranch); + + expect(version).toBe('feature-test-branch-123'); + }); + }); + + describe('composite strategy', () => { + it('should combine multiple components', () => { + const strategy = VersioningStrategy.packageWithBranch(); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('1.2.3-feature-test-branch.42'); + }); + + it('should handle semantic with patch', () => { + const strategy = VersioningStrategy.semanticWithPatch(); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('1.2.3.42'); + }); + }); + + describe('compute (VersionInfo)', () => { + it('should create complete VersionInfo', () => { + const strategy = VersioningStrategy.gitTag(); + const computer = new VersionComputer(strategy); + + const versionInfo = computer.compute(mockContext); + + expect(versionInfo.version).toBe('1.2.3'); + expect(versionInfo.commitHash).toBe('abcdef1234567890'); + expect(versionInfo.branch).toBe('feature/test-branch'); + expect(versionInfo.tag).toBe('v1.2.3'); + expect(versionInfo.commitCount).toBe(42); + expect(versionInfo.environment).toBe('production'); + expect(versionInfo.buildNumber).toBe('123'); + expect(versionInfo.packageVersion).toBe('1.2.3'); + }); + + it('should include repository URL when provided', () => { + const strategy = VersioningStrategy.gitTag(); + const computer = new VersionComputer(strategy); + + const contextWithRepo = { + ...mockContext, + repositoryUrl: 'https://github.com/test/repo', + }; + + const versionInfo = computer.compute(contextWithRepo); + + expect(versionInfo.repositoryUrl).toBe('https://github.com/test/repo'); + }); + }); + + describe('inline padding specification', () => { + it('should support inline padding in commit count', () => { + const strategy = VersioningStrategy.create('v{commit-count:5}'); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('v00042'); + }); + + it('should support multiple commit count placeholders with different padding', () => { + const strategy = VersioningStrategy.create('{commit-count:3}-{commit-count:5}'); + const computer = new VersionComputer(strategy); + + const version = computer.computeVersionString(mockContext); + + expect(version).toBe('042-00042'); + }); + }); +}); diff --git a/test/versioning/git-info.test.ts b/test/versioning/git-info.test.ts new file mode 100644 index 0000000..275d19b --- /dev/null +++ b/test/versioning/git-info.test.ts @@ -0,0 +1,165 @@ +import { GitInfoHelper } from '../../src/versioning/git-info'; + +describe('GitInfoHelper', () => { + describe('create', () => { + it('should create GitInfo with short hash', () => { + const gitInfo = GitInfoHelper.create({ + commitHash: 'abcdef1234567890', + branch: 'main', + commitCount: 42, + }); + + expect(gitInfo.commitHash).toBe('abcdef1234567890'); + expect(gitInfo.shortCommitHash).toBe('abcdef12'); + expect(gitInfo.branch).toBe('main'); + expect(gitInfo.commitCount).toBe(42); + expect(gitInfo.tag).toBeUndefined(); + }); + + it('should include tag when provided', () => { + const gitInfo = GitInfoHelper.create({ + commitHash: 'abcdef1234567890', + branch: 'main', + tag: 'v1.2.3', + commitCount: 42, + }); + + expect(gitInfo.tag).toBe('v1.2.3'); + }); + + it('should include commits since tag when provided', () => { + const gitInfo = GitInfoHelper.create({ + commitHash: 'abcdef1234567890', + branch: 'main', + commitCount: 42, + commitsSinceTag: 5, + }); + + expect(gitInfo.commitsSinceTag).toBe(5); + }); + }); + + describe('shortenHash', () => { + it('should shorten hash to 8 characters by default', () => { + const short = GitInfoHelper.shortenHash('abcdef1234567890'); + expect(short).toBe('abcdef12'); + }); + + it('should shorten hash to custom length', () => { + const short = GitInfoHelper.shortenHash('abcdef1234567890', 12); + expect(short).toBe('abcdef123456'); + }); + + it('should handle short hashes', () => { + const short = GitInfoHelper.shortenHash('abc', 8); + expect(short).toBe('abc'); + }); + }); + + describe('fromEnvironment', () => { + it('should extract from GitHub Actions environment', () => { + process.env.GITHUB_SHA = 'abcdef1234567890'; + process.env.GITHUB_REF = 'refs/heads/feature-branch'; + process.env.COMMIT_COUNT = '42'; + + const gitInfo = GitInfoHelper.fromEnvironment(); + + expect(gitInfo.commitHash).toBe('abcdef1234567890'); + expect(gitInfo.branch).toBe('feature-branch'); + expect(gitInfo.commitCount).toBe(42); + }); + + it('should extract tag from GitHub Actions', () => { + process.env.GITHUB_SHA = 'abcdef1234567890'; + process.env.GITHUB_REF = 'refs/tags/v1.2.3'; + process.env.COMMIT_COUNT = '42'; + + const gitInfo = GitInfoHelper.fromEnvironment(); + + expect(gitInfo.tag).toBe('v1.2.3'); + }); + + it('should use head ref for pull requests', () => { + process.env.GITHUB_SHA = 'abcdef1234567890'; + process.env.GITHUB_REF = 'refs/pull/123/merge'; + process.env.GITHUB_HEAD_REF = 'feature-branch'; + process.env.COMMIT_COUNT = '42'; + + const gitInfo = GitInfoHelper.fromEnvironment(); + + expect(gitInfo.branch).toBe('feature-branch'); + }); + + it('should extract from GitLab CI environment', () => { + process.env.CI_COMMIT_SHA = 'abcdef1234567890'; + process.env.CI_COMMIT_REF_NAME = 'feature-branch'; + process.env.CI_COMMIT_TAG = 'v1.2.3'; + process.env.COMMIT_COUNT = '42'; + + const gitInfo = GitInfoHelper.fromEnvironment(); + + expect(gitInfo.commitHash).toBe('abcdef1234567890'); + expect(gitInfo.branch).toBe('feature-branch'); + expect(gitInfo.tag).toBe('v1.2.3'); + expect(gitInfo.commitCount).toBe(42); + }); + + it('should use generic fallback', () => { + process.env.GIT_COMMIT = 'abcdef1234567890'; + process.env.GIT_BRANCH = 'main'; + process.env.COMMIT_COUNT = '42'; + + const gitInfo = GitInfoHelper.fromEnvironment(); + + expect(gitInfo.commitHash).toBe('abcdef1234567890'); + expect(gitInfo.branch).toBe('main'); + expect(gitInfo.commitCount).toBe(42); + }); + + it('should handle missing environment variables', () => { + const gitInfo = GitInfoHelper.fromEnvironment(); + + expect(gitInfo.commitHash).toBe('unknown'); + expect(gitInfo.branch).toBe('unknown'); + expect(gitInfo.commitCount).toBe(0); + }); + }); + + describe('isMainBranch', () => { + it('should return true for main branch', () => { + expect(GitInfoHelper.isMainBranch('main')).toBe(true); + }); + + it('should return true for master branch', () => { + expect(GitInfoHelper.isMainBranch('master')).toBe(true); + }); + + it('should return false for other branches', () => { + expect(GitInfoHelper.isMainBranch('develop')).toBe(false); + expect(GitInfoHelper.isMainBranch('feature/test')).toBe(false); + }); + }); + + describe('isTaggedRelease', () => { + it('should return true when tag is present', () => { + const gitInfo = GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + tag: 'v1.0.0', + commitCount: 42, + }); + + expect(GitInfoHelper.isTaggedRelease(gitInfo)).toBe(true); + }); + + it('should return false when tag is missing', () => { + const gitInfo = GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + commitCount: 42, + }); + + expect(GitInfoHelper.isTaggedRelease(gitInfo)).toBe(false); + }); + }); +}); diff --git a/test/versioning/strategy.test.ts b/test/versioning/strategy.test.ts new file mode 100644 index 0000000..923853a --- /dev/null +++ b/test/versioning/strategy.test.ts @@ -0,0 +1,141 @@ +import { VersioningStrategy } from '../../src/versioning/strategy'; + +describe('VersioningStrategy', () => { + describe('create', () => { + it('should create custom strategy', () => { + const strategy = VersioningStrategy.create('{version}-{branch}', { + commitCount: { mode: 'all', padding: 0 }, + }); + + expect(strategy.format).toBe('{version}-{branch}'); + expect(strategy.components.commitCount?.mode).toBe('all'); + }); + + it('should create strategy without components', () => { + const strategy = VersioningStrategy.create('{commit-hash}'); + + expect(strategy.format).toBe('{commit-hash}'); + expect(strategy.components).toEqual({}); + }); + }); + + describe('buildNumber', () => { + it('should create build number strategy with defaults', () => { + const strategy = VersioningStrategy.buildNumber(); + + expect(strategy.format).toBe('build-{commit-count}-{commit-hash:8}'); + expect(strategy.components.commitCount?.mode).toBe('all'); + expect(strategy.components.commitCount?.padding).toBe(0); + }); + + it('should accept custom config', () => { + const strategy = VersioningStrategy.buildNumber({ envVar: 'MY_BUILD_NUMBER' }); + + expect(strategy.components.buildNumber?.envVar).toBe('MY_BUILD_NUMBER'); + }); + }); + + describe('gitTag', () => { + it('should create git tag strategy with defaults', () => { + const strategy = VersioningStrategy.gitTag(); + + expect(strategy.format).toBe('{git-tag}'); + expect(strategy.components.gitTag?.prefix).toBe('v'); + expect(strategy.components.gitTag?.pattern).toBe('*.*.*'); + expect(strategy.components.gitTag?.countCommitsSince).toBe(true); + }); + + it('should accept custom prefix', () => { + const strategy = VersioningStrategy.gitTag({ prefix: 'release-' }); + + expect(strategy.components.gitTag?.prefix).toBe('release-'); + }); + + it('should accept custom pattern', () => { + const strategy = VersioningStrategy.gitTag({ pattern: 'v*' }); + + expect(strategy.components.gitTag?.pattern).toBe('v*'); + }); + }); + + describe('packageJson', () => { + it('should create package.json strategy with defaults', () => { + const strategy = VersioningStrategy.packageJson(); + + expect(strategy.format).toBe('{package-version}'); + expect(strategy.components.packageJson?.includePrerelease).toBe(true); + }); + + it('should accept custom config', () => { + const strategy = VersioningStrategy.packageJson({ includePrerelease: false }); + + expect(strategy.components.packageJson?.includePrerelease).toBe(false); + }); + }); + + describe('commitCount', () => { + it('should create commit count strategy with defaults', () => { + const strategy = VersioningStrategy.commitCount(); + + expect(strategy.format).toBe('0.0.{commit-count}'); + expect(strategy.components.commitCount?.mode).toBe('all'); + expect(strategy.components.commitCount?.padding).toBe(0); + }); + + it('should accept custom mode', () => { + const strategy = VersioningStrategy.commitCount({ mode: 'since-tag' }); + + expect(strategy.components.commitCount?.mode).toBe('since-tag'); + }); + + it('should accept custom padding', () => { + const strategy = VersioningStrategy.commitCount({ padding: 5 }); + + expect(strategy.components.commitCount?.padding).toBe(5); + }); + }); + + describe('commitHash', () => { + it('should create commit hash strategy with default length', () => { + const strategy = VersioningStrategy.commitHash(); + + expect(strategy.format).toBe('{commit-hash:8}'); + }); + + it('should accept custom length', () => { + const strategy = VersioningStrategy.commitHash(12); + + expect(strategy.format).toBe('{commit-hash:12}'); + }); + }); + + describe('gitTagWithDevVersions', () => { + it('should create strategy with dev version support', () => { + const strategy = VersioningStrategy.gitTagWithDevVersions(); + + expect(strategy.format).toBe('{git-tag}'); + expect(strategy.components.gitTag?.countCommitsSince).toBe(true); + expect(strategy.components.commitCount?.mode).toBe('since-tag'); + }); + }); + + describe('packageWithBranch', () => { + it('should create package with branch strategy', () => { + const strategy = VersioningStrategy.packageWithBranch(); + + expect(strategy.format).toBe('{package-version}-{branch}.{commit-count}'); + expect(strategy.components.packageJson?.includePrerelease).toBe(true); + expect(strategy.components.commitCount?.mode).toBe('all'); + }); + }); + + describe('semanticWithPatch', () => { + it('should create semantic versioning with patch strategy', () => { + const strategy = VersioningStrategy.semanticWithPatch(); + + expect(strategy.format).toBe('{package-version}.{commit-count}'); + expect(strategy.components.packageJson?.includePrerelease).toBe(false); + expect(strategy.components.commitCount?.mode).toBe('all'); + }); + }); +}); diff --git a/test/versioning/version-info.test.ts b/test/versioning/version-info.test.ts new file mode 100644 index 0000000..0e84d67 --- /dev/null +++ b/test/versioning/version-info.test.ts @@ -0,0 +1,364 @@ +import { GitInfoHelper } from '../../src/versioning/git-info'; +import { VersionInfo, VersionInfoBuilder } from '../../src/versioning/version-info'; + +describe('VersionInfo', () => { + const mockGitInfo = GitInfoHelper.create({ + commitHash: 'abcdef1234567890', + branch: 'main', + tag: 'v1.2.3', + commitCount: 42, + }); + + describe('create', () => { + it('should create VersionInfo with all properties', () => { + const versionInfo = VersionInfo.create({ + version: '1.2.3', + gitInfo: mockGitInfo, + environment: 'production', + packageVersion: '1.2.3', + deploymentTime: '2024-01-15T10:00:00Z', + repositoryUrl: 'https://github.com/test/repo', + buildNumber: '123', + pipelineVersion: 'pipeline-v1', + }); + + expect(versionInfo.version).toBe('1.2.3'); + expect(versionInfo.commitHash).toBe('abcdef1234567890'); + expect(versionInfo.shortCommitHash).toBe('abcdef12'); + expect(versionInfo.branch).toBe('main'); + expect(versionInfo.tag).toBe('v1.2.3'); + expect(versionInfo.commitCount).toBe(42); + expect(versionInfo.environment).toBe('production'); + expect(versionInfo.packageVersion).toBe('1.2.3'); + expect(versionInfo.deploymentTime).toBe('2024-01-15T10:00:00Z'); + expect(versionInfo.repositoryUrl).toBe('https://github.com/test/repo'); + expect(versionInfo.buildNumber).toBe('123'); + expect(versionInfo.pipelineVersion).toBe('pipeline-v1'); + }); + + it('should use current time if deploymentTime not provided', () => { + const before = Date.now(); + const versionInfo = VersionInfo.create({ + version: '1.2.3', + gitInfo: mockGitInfo, + environment: 'production', + }); + const after = Date.now(); + + const deploymentTime = new Date(versionInfo.deploymentTime).getTime(); + expect(deploymentTime).toBeGreaterThanOrEqual(before); + expect(deploymentTime).toBeLessThanOrEqual(after); + }); + }); + + describe('fromEnvironment', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('should create from GitHub Actions environment', () => { + process.env.GITHUB_SHA = 'abcdef1234567890'; + process.env.GITHUB_REF = 'refs/heads/main'; + process.env.GITHUB_REPOSITORY = 'test/repo'; + process.env.GITHUB_RUN_NUMBER = '123'; + process.env.COMMIT_COUNT = '42'; + process.env.PACKAGE_VERSION = '1.2.3'; + + const versionInfo = VersionInfo.fromEnvironment('1.2.3', 'production'); + + expect(versionInfo.version).toBe('1.2.3'); + expect(versionInfo.commitHash).toBe('abcdef1234567890'); + expect(versionInfo.branch).toBe('main'); + expect(versionInfo.environment).toBe('production'); + expect(versionInfo.repositoryUrl).toBe('https://github.com/test/repo'); + expect(versionInfo.buildNumber).toBe('123'); + expect(versionInfo.packageVersion).toBe('1.2.3'); + }); + + it('should handle CodeBuild environment', () => { + process.env.GIT_COMMIT = 'abcdef1234567890'; + process.env.GIT_BRANCH = 'main'; + process.env.COMMIT_COUNT = '42'; + process.env.CODEBUILD_BUILD_ID = 'build-123'; + + const versionInfo = VersionInfo.fromEnvironment('1.2.3', 'production'); + + expect(versionInfo.pipelineVersion).toBe('build-123'); + }); + }); + + describe('fromJson', () => { + it('should parse JSON correctly', () => { + const json = JSON.stringify({ + version: '1.2.3', + commitHash: 'abcdef1234567890', + shortCommitHash: 'abcdef12', + branch: 'main', + tag: 'v1.2.3', + commitCount: 42, + environment: 'production', + deploymentTime: '2024-01-15T10:00:00Z', + packageVersion: '1.2.3', + repositoryUrl: 'https://github.com/test/repo', + buildNumber: '123', + pipelineVersion: 'pipeline-v1', + }); + + const versionInfo = VersionInfo.fromJson(json); + + expect(versionInfo.version).toBe('1.2.3'); + expect(versionInfo.commitHash).toBe('abcdef1234567890'); + expect(versionInfo.tag).toBe('v1.2.3'); + }); + }); + + describe('displayVersion', () => { + it('should return tag when available', () => { + const versionInfo = VersionInfo.create({ + version: '1.2.3-dev', + gitInfo: mockGitInfo, + environment: 'production', + }); + + expect(versionInfo.displayVersion()).toBe('v1.2.3'); + }); + + it('should return version when tag is not available', () => { + const gitInfo = GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + commitCount: 42, + }); + + const versionInfo = VersionInfo.create({ + version: '1.2.3-dev', + gitInfo, + environment: 'production', + }); + + expect(versionInfo.displayVersion()).toBe('1.2.3-dev'); + }); + }); + + describe('isTaggedRelease', () => { + it('should return true when tag exists', () => { + const versionInfo = VersionInfo.create({ + version: '1.2.3', + gitInfo: mockGitInfo, + environment: 'production', + }); + + expect(versionInfo.isTaggedRelease()).toBe(true); + }); + + it('should return false when tag is missing', () => { + const gitInfo = GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + commitCount: 42, + }); + + const versionInfo = VersionInfo.create({ + version: '1.2.3', + gitInfo, + environment: 'production', + }); + + expect(versionInfo.isTaggedRelease()).toBe(false); + }); + }); + + describe('isMainBranch', () => { + it('should return true for main branch', () => { + const versionInfo = VersionInfo.create({ + version: '1.2.3', + gitInfo: mockGitInfo, + environment: 'production', + }); + + expect(versionInfo.isMainBranch()).toBe(true); + }); + + it('should return false for other branches', () => { + const gitInfo = GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'develop', + commitCount: 42, + }); + + const versionInfo = VersionInfo.create({ + version: '1.2.3', + gitInfo, + environment: 'production', + }); + + expect(versionInfo.isMainBranch()).toBe(false); + }); + }); + + describe('toJson and toObject', () => { + it('should convert to JSON string', () => { + const versionInfo = VersionInfo.create({ + version: '1.2.3', + gitInfo: mockGitInfo, + environment: 'production', + }); + + const json = versionInfo.toJson(); + const parsed = JSON.parse(json); + + expect(parsed.version).toBe('1.2.3'); + expect(parsed.commitHash).toBe('abcdef1234567890'); + }); + + it('should convert to object', () => { + const versionInfo = VersionInfo.create({ + version: '1.2.3', + gitInfo: mockGitInfo, + environment: 'production', + }); + + const obj = versionInfo.toObject(); + + expect(obj.version).toBe('1.2.3'); + expect(obj.commitHash).toBe('abcdef1234567890'); + }); + }); + + describe('template substitution', () => { + it('should substitute version in parameterName', () => { + const versionInfo = VersionInfo.create({ + version: '1.2.3', + gitInfo: mockGitInfo, + environment: 'production', + }); + + const name = versionInfo.parameterName('/app/{environment}/{version}'); + expect(name).toBe('/app/production/1.2.3'); + }); + + it('should substitute multiple variables', () => { + const versionInfo = VersionInfo.create({ + version: '1.2.3', + gitInfo: mockGitInfo, + environment: 'prod', + }); + + const name = versionInfo.exportName('{environment}-{version}-{commit-hash}'); + expect(name).toBe('prod-1.2.3-abcdef12'); + }); + }); + + describe('compare', () => { + it('should compare by commit count', () => { + const v1 = VersionInfo.create({ + version: '1.2.3', + gitInfo: GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + commitCount: 10, + }), + environment: 'production', + }); + + const v2 = VersionInfo.create({ + version: '1.2.4', + gitInfo: GitInfoHelper.create({ + commitHash: 'def456', + branch: 'main', + commitCount: 20, + }), + environment: 'production', + }); + + expect(VersionInfo.compare(v1, v2)).toBeLessThan(0); + expect(VersionInfo.compare(v2, v1)).toBeGreaterThan(0); + }); + + it('should compare by version string when commit counts are equal', () => { + const v1 = VersionInfo.create({ + version: '1.2.3', + gitInfo: GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + commitCount: 10, + }), + environment: 'production', + }); + + const v2 = VersionInfo.create({ + version: '1.2.4', + gitInfo: GitInfoHelper.create({ + commitHash: 'def456', + branch: 'main', + commitCount: 10, + }), + environment: 'production', + }); + + expect(VersionInfo.compare(v1, v2)).toBeLessThan(0); + }); + }); +}); + +describe('VersionInfoBuilder', () => { + it('should build VersionInfo with fluent API', () => { + const gitInfo = GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + commitCount: 42, + }); + + const versionInfo = new VersionInfoBuilder() + .withVersion('1.2.3') + .withGitInfo(gitInfo) + .withEnvironment('production') + .withPackageVersion('1.2.3') + .withRepositoryUrl('https://github.com/test/repo') + .withBuildNumber('123') + .buildVersionInfo(); + + expect(versionInfo.version).toBe('1.2.3'); + expect(versionInfo.environment).toBe('production'); + expect(versionInfo.packageVersion).toBe('1.2.3'); + expect(versionInfo.buildNumber).toBe('123'); + }); + + it('should throw error if version is missing', () => { + const builder = new VersionInfoBuilder() + .withGitInfo(GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + commitCount: 42, + })) + .withEnvironment('production'); + + expect(() => builder.buildVersionInfo()).toThrow('Version is required'); + }); + + it('should throw error if gitInfo is missing', () => { + const builder = new VersionInfoBuilder() + .withVersion('1.2.3') + .withEnvironment('production'); + + expect(() => builder.buildVersionInfo()).toThrow('GitInfo is required'); + }); + + it('should throw error if environment is missing', () => { + const builder = new VersionInfoBuilder() + .withVersion('1.2.3') + .withGitInfo(GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + commitCount: 42, + })); + + expect(() => builder.buildVersionInfo()).toThrow('Environment is required'); + }); +}); diff --git a/test/versioning/version-outputs.test.ts b/test/versioning/version-outputs.test.ts new file mode 100644 index 0000000..a36f53a --- /dev/null +++ b/test/versioning/version-outputs.test.ts @@ -0,0 +1,546 @@ +import * as cdk from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { GitInfoHelper } from '../../src/versioning/git-info'; +import { VersionInfo } from '../../src/versioning/version-info'; +import { VersionOutputs } from '../../src/versioning/version-outputs'; + +describe('VersionOutputs', () => { + let stack: cdk.Stack; + let versionInfo: VersionInfo; + + beforeEach(() => { + stack = new cdk.Stack(); + versionInfo = VersionInfo.create({ + version: '1.2.3', + gitInfo: GitInfoHelper.create({ + commitHash: 'abcdef1234567890', + branch: 'main', + tag: 'v1.2.3', + commitCount: 42, + }), + environment: 'production', + packageVersion: '1.2.3', + deploymentTime: '2024-01-15T10:00:00Z', + repositoryUrl: 'https://github.com/test/repo', + }); + }); + + describe('CloudFormation outputs', () => { + it('should create CloudFormation outputs by default', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + }); + + const template = Template.fromStack(stack); + const outputs = template.findOutputs('*'); + + // Check that we have the expected number of outputs + expect(Object.keys(outputs).length).toBe(8); + + // Check each output exists with correct properties + const outputValues = Object.values(outputs); + + expect(outputValues).toContainEqual( + expect.objectContaining({ + Value: '1.2.3', + Description: 'Deployment version', + }), + ); + + expect(outputValues).toContainEqual( + expect.objectContaining({ + Value: 'abcdef1234567890', + Description: 'Git commit hash', + }), + ); + + expect(outputValues).toContainEqual( + expect.objectContaining({ + Value: 'main', + Description: 'Git branch', + }), + ); + + expect(outputValues).toContainEqual( + expect.objectContaining({ + Value: '42', + Description: 'Git commit count', + }), + ); + + expect(outputValues).toContainEqual( + expect.objectContaining({ + Value: '2024-01-15T10:00:00Z', + Description: 'Deployment timestamp', + }), + ); + + expect(outputValues).toContainEqual( + expect.objectContaining({ + Value: 'production', + Description: 'Deployment environment', + }), + ); + }); + + it('should create tag output when tag is present', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + }); + + const template = Template.fromStack(stack); + const outputs = Object.values(template.findOutputs('*')); + + expect(outputs).toContainEqual( + expect.objectContaining({ + Value: 'v1.2.3', + Description: 'Git tag', + }), + ); + }); + + it('should create package version output when present', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + }); + + const template = Template.fromStack(stack); + const outputs = Object.values(template.findOutputs('*')); + + expect(outputs).toContainEqual( + expect.objectContaining({ + Value: '1.2.3', + Description: 'Package version from package.json', + }), + ); + }); + + it('should not create outputs when disabled', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + cloudFormation: { enabled: false }, + }); + + const template = Template.fromStack(stack); + const outputs = template.toJSON().Outputs || {}; + + expect(Object.keys(outputs).length).toBe(0); + }); + + it('should create exports when enabled', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + cloudFormation: { + enabled: true, + export: true, + exportNameTemplate: '{environment}-version', + }, + }); + + const template = Template.fromStack(stack); + const outputs = Object.values(template.findOutputs('*')); + + expect(outputs).toContainEqual( + expect.objectContaining({ + Value: '1.2.3', + Description: 'Deployment version', + Export: { + Name: 'production-version-version', + }, + }), + ); + }); + + it('should use custom output prefix', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + outputPrefix: 'App', + }); + + const template = Template.fromStack(stack); + const outputs = Object.values(template.findOutputs('*')); + + // Just verify that the version output exists + expect(outputs).toContainEqual( + expect.objectContaining({ + Value: '1.2.3', + }), + ); + }); + }); + + describe('SSM Parameter Store outputs', () => { + it('should not create parameters by default', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + }); + + const template = Template.fromStack(stack); + const parameters = template.findResources('AWS::SSM::Parameter'); + + expect(Object.keys(parameters).length).toBe(0); + }); + + it('should create single JSON parameter when not split', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + parameterStore: { + enabled: true, + basePath: '/myapp/version', + }, + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::SSM::Parameter', { + Name: '/myapp/version', + Type: 'String', + Description: 'Version information (JSON)', + }); + + const parameters = template.findResources('AWS::SSM::Parameter'); + expect(Object.keys(parameters).length).toBe(1); + }); + + it('should create split parameters when enabled', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + parameterStore: { + enabled: true, + basePath: '/myapp/version', + splitParameters: true, + }, + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::SSM::Parameter', { + Name: '/myapp/version/version', + Value: '1.2.3', + Description: 'Deployment version', + }); + + template.hasResourceProperties('AWS::SSM::Parameter', { + Name: '/myapp/version/commit-hash', + Value: 'abcdef1234567890', + Description: 'Git commit hash', + }); + + template.hasResourceProperties('AWS::SSM::Parameter', { + Name: '/myapp/version/branch', + Value: 'main', + Description: 'Git branch', + }); + + template.hasResourceProperties('AWS::SSM::Parameter', { + Name: '/myapp/version/commit-count', + Value: '42', + Description: 'Git commit count', + }); + + template.hasResourceProperties('AWS::SSM::Parameter', { + Name: '/myapp/version/deployment-time', + Value: '2024-01-15T10:00:00Z', + Description: 'Deployment timestamp', + }); + + template.hasResourceProperties('AWS::SSM::Parameter', { + Name: '/myapp/version/environment', + Value: 'production', + Description: 'Deployment environment', + }); + }); + + it('should create tag parameter when tag is present and split', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + parameterStore: { + enabled: true, + basePath: '/myapp/version', + splitParameters: true, + }, + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::SSM::Parameter', { + Name: '/myapp/version/tag', + Value: 'v1.2.3', + Description: 'Git tag', + }); + }); + + it('should use default base path', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + parameterStore: { + enabled: true, + }, + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::SSM::Parameter', { + Name: '/version', + }); + }); + + it('should use custom description', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + parameterStore: { + enabled: true, + description: 'Custom description', + }, + }); + + const template = Template.fromStack(stack); + + template.hasResourceProperties('AWS::SSM::Parameter', { + Description: 'Custom description', + }); + }); + }); + + describe('combined outputs', () => { + it('should create both CloudFormation and SSM outputs', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + cloudFormation: { enabled: true }, + parameterStore: { + enabled: true, + basePath: '/app/version', + }, + }); + + const template = Template.fromStack(stack); + + // Check CloudFormation outputs + const outputs = Object.values(template.findOutputs('*')); + expect(outputs).toContainEqual( + expect.objectContaining({ + Value: '1.2.3', + }), + ); + + // Check SSM parameter + template.hasResourceProperties('AWS::SSM::Parameter', { + Name: '/app/version', + }); + }); + }); + + describe('version info without optional fields', () => { + it('should handle version info without tag', () => { + const versionInfoNoTag = VersionInfo.create({ + version: '1.2.3-dev', + gitInfo: GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'develop', + commitCount: 42, + }), + environment: 'development', + }); + + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo: versionInfoNoTag, + }); + + const template = Template.fromStack(stack); + const outputs = template.toJSON().Outputs || {}; + + expect(outputs.VersionTag).toBeUndefined(); + }); + + it('should handle version info without package version', () => { + const versionInfoNoPackage = VersionInfo.create({ + version: '1.2.3', + gitInfo: GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + commitCount: 42, + }), + environment: 'production', + }); + + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo: versionInfoNoPackage, + }); + + const template = Template.fromStack(stack); + const outputs = template.toJSON().Outputs || {}; + + expect(outputs.VersionPackageVersion).toBeUndefined(); + }); + }); + + describe('stack metadata', () => { + it('should add version info as stack metadata with default key', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + }); + + const template = Template.fromStack(stack); + const metadata = template.toJSON().Metadata; + + expect(metadata).toBeDefined(); + expect(metadata.Version).toBeDefined(); + expect(metadata.Version).toMatchObject({ + version: '1.2.3', + commitHash: 'abcdef1234567890', + branch: 'main', + tag: 'v1.2.3', + commitCount: 42, + environment: 'production', + packageVersion: '1.2.3', + deploymentTime: '2024-01-15T10:00:00Z', + }); + }); + + it('should use custom metadataKey when provided', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + metadataKey: 'CustomVersion', + }); + + const template = Template.fromStack(stack); + const metadata = template.toJSON().Metadata; + + expect(metadata).toBeDefined(); + expect(metadata.CustomVersion).toBeDefined(); + expect(metadata.CustomVersion.version).toBe('1.2.3'); + expect(metadata.Version).toBeUndefined(); + }); + + it('should fallback to outputPrefix when metadataKey is not provided', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + outputPrefix: 'App', + }); + + const template = Template.fromStack(stack); + const metadata = template.toJSON().Metadata; + + expect(metadata).toBeDefined(); + expect(metadata.App).toBeDefined(); + expect(metadata.App.version).toBe('1.2.3'); + }); + + it('should prefer metadataKey over outputPrefix', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + metadataKey: 'SpecificKey', + outputPrefix: 'App', + }); + + const template = Template.fromStack(stack); + const metadata = template.toJSON().Metadata; + + expect(metadata).toBeDefined(); + expect(metadata.SpecificKey).toBeDefined(); + expect(metadata.App).toBeUndefined(); + expect(metadata.Version).toBeUndefined(); + }); + + it('should include all version info fields in metadata', () => { + const fullVersionInfo = VersionInfo.create({ + version: '2.0.0', + gitInfo: GitInfoHelper.create({ + commitHash: 'def4567890123456', + branch: 'release/2.0', + tag: 'v2.0.0', + commitCount: 100, + }), + environment: 'staging', + packageVersion: '2.0.0', + deploymentTime: '2024-02-01T15:30:00Z', + repositoryUrl: 'https://github.com/example/repo', + buildNumber: '123', + pipelineVersion: 'pipeline-v1', + }); + + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo: fullVersionInfo, + }); + + const template = Template.fromStack(stack); + const metadata = template.toJSON().Metadata; + + expect(metadata.Version).toMatchObject({ + version: '2.0.0', + commitHash: 'def4567890123456', + shortCommitHash: 'def45678', + branch: 'release/2.0', + tag: 'v2.0.0', + commitCount: 100, + environment: 'staging', + packageVersion: '2.0.0', + deploymentTime: '2024-02-01T15:30:00Z', + repositoryUrl: 'https://github.com/example/repo', + buildNumber: '123', + pipelineVersion: 'pipeline-v1', + }); + }); + + it('should handle version info without optional fields in metadata', () => { + const minimalVersionInfo = VersionInfo.create({ + version: '1.0.0', + gitInfo: GitInfoHelper.create({ + commitHash: 'abc123', + branch: 'main', + commitCount: 10, + }), + environment: 'dev', + }); + + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo: minimalVersionInfo, + }); + + const template = Template.fromStack(stack); + const metadata = template.toJSON().Metadata; + + expect(metadata.Version).toBeDefined(); + expect(metadata.Version.version).toBe('1.0.0'); + expect(metadata.Version.tag).toBeUndefined(); + expect(metadata.Version.packageVersion).toBeUndefined(); + expect(metadata.Version.repositoryUrl).toBeUndefined(); + }); + + it('should add metadata even when CloudFormation outputs are disabled', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + cloudFormation: { enabled: false }, + }); + + const template = Template.fromStack(stack); + const metadata = template.toJSON().Metadata; + const outputs = template.toJSON().Outputs || {}; + + // No CloudFormation outputs + expect(Object.keys(outputs).length).toBe(0); + + // But metadata should still exist + expect(metadata.Version).toBeDefined(); + expect(metadata.Version.version).toBe('1.2.3'); + }); + + it('should add metadata even when only SSM parameters are enabled', () => { + new VersionOutputs(stack, 'VersionOutputs', { + versionInfo, + cloudFormation: { enabled: false }, + parameterStore: { + enabled: true, + basePath: '/app/version', + }, + }); + + const template = Template.fromStack(stack); + const metadata = template.toJSON().Metadata; + + // Metadata should exist regardless of output configuration + expect(metadata.Version).toBeDefined(); + expect(metadata.Version.version).toBe('1.2.3'); + }); + }); +});