Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .npmignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ const project = new awscdk.AwsCdkConstructLibrary({
// packageName: undefined, /* The "name" in package.json. */
});

// Ignore the .tmp/ directory used by compute-version for version artifacts
project.gitignore.exclude('.tmp/');
project.npmignore?.exclude('.tmp/');

project.jest?.addSetupFileAfterEnv('<rootDir>/test/jest.setup.ts');

// Add CLI bin entry for compute-version
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,25 @@ For detailed documentation, see [Metadata Guide](docs/METADATA.md).
A CLI utility for computing versions based on git information:

```bash
npx compute-version --strategy git-tag --environment production
# Basic usage (writes to .tmp/version.json)
npx compute-version '{"format":"{commit-count}","components":{}}'

# Custom output path
npx compute-version --output build/version.json '{"format":"{git-tag}","components":{}}'
```

The version artifact is written to `.tmp/version.json` by default. This path is
gitignored and safe for all shell emulators (Yarn Berry, pnpm, Bun).

You can override the output path with the `--output` (or `-o`) flag, or via the
`VERSION_OUTPUT_PATH` environment variable.

> **Migration note:** Previous versions wrote to `~version.json`. That path
> caused tilde-expansion failures in non-bash shell emulators. The CLI still
> reads `~version.json` as a fallback (with a deprecation warning), but no
> longer writes to it. Update any scripts that reference `~version.json` to
> use `.tmp/version.json` instead.

## API Reference

For complete API documentation, see [API.md](docs/API.md).
Expand Down
54 changes: 47 additions & 7 deletions docs/VERSIONING.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,19 +256,59 @@ const config = VersioningOutputsFactory.minimal();

## CLI Usage

The `compute-version` CLI computes version information from git:
The `compute-version` CLI computes version information from git and writes it to
a JSON artifact file.

```bash
# Basic usage
npx compute-version --environment production
# Basic usage β€” writes to .tmp/version.json (default)
npx compute-version '{"format":"{commit-count}","components":{}}'

# With strategy
npx compute-version --strategy git-tag --environment staging
# Custom strategy
npx compute-version '{"format":"{git-tag}","components":{"commitCount":{"mode":"all"}}}'

# Output as JSON
npx compute-version --format json
# Override output path
npx compute-version --output build/version.json '{"format":"{commit-count}","components":{}}'

# Or via environment variable
VERSION_OUTPUT_PATH=build/version.json npx compute-version '{"format":"{commit-count}","components":{}}'
```

### Version Artifact

| Setting | Value |
|---------|-------|
| Default path | `.tmp/version.json` |
| Override flag | `--output <path>` / `-o <path>` |
| Environment variable | `VERSION_OUTPUT_PATH` |
| Gitignored | Yes (`.tmp/` is excluded) |

The artifact is a JSON file containing all computed version fields (version,
commitHash, shortCommitHash, branch, tag, commitCount, environment, etc.).

**Path validation:** The CLI rejects output paths whose filename starts with
`~`, `-`, or `#` β€” characters that cause shell-parsing hazards (tilde expansion,
option-flag interpretation, comment stripping).

### Reading the Version File (Programmatic)

```typescript
import { readVersionFile } from 'cdk-devops';

// Reads from .tmp/version.json, falls back to ~version.json (deprecated)
const json = readVersionFile();
```

### Migration from `~version.json`

Previous versions wrote the artifact to `~version.json`. A leading `~` in a
filename triggers tilde expansion in shell emulators used by Yarn Berry, pnpm,
and Bun, causing the build step to abort with "Unsupported tilde expansion".

The new default is `.tmp/version.json`. The `readVersionFile()` helper still
reads `~version.json` as a fallback and emits a deprecation warning. This
fallback will be removed in the next minor release. Update any CI scripts or
projen tasks that reference `~version.json` to use `.tmp/version.json`.

## Environment Variables

The module automatically extracts information from these environment variables:
Expand Down
15 changes: 1 addition & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

113 changes: 106 additions & 7 deletions src/versioning/compute-version.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,55 @@
#!/usr/bin/env node
import * as cp from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { VersionComputer } from './computation';
import { GitInfo } from './git-info';
import { DEFAULT_VERSION_OUTPUT_PATH, LEGACY_VERSION_OUTPUT_PATH, shellSafePath, validateOutputPath } from './output-path';
import { VersioningStrategy } from './strategy';

/**
* Options for the compute-version CLI
*/
export interface ComputeVersionOptions {
/**
* Strategy configuration with format string and components
*/
readonly strategyConfig: { format: string; components: any };

/**
* Output file path for the version JSON artifact.
* @default '.tmp/version.json'
*/
readonly outputPath?: string;
}

/**
* Resolve the output path from CLI args, environment, or default.
*/
function resolveOutputPath(cliOutput: string | undefined): string {
const outputPath = cliOutput || process.env.VERSION_OUTPUT_PATH || DEFAULT_VERSION_OUTPUT_PATH;
validateOutputPath(outputPath);
return outputPath;
}

/**
* Ensure the parent directory of a file path exists.
*/
function ensureDirectory(filePath: string): void {
const dir = path.dirname(filePath);
if (dir && dir !== '.' && !fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}

/**
* Compute version information and write to file
*
* @param options - Configuration options
*/
export async function computeVersion(strategyConfig: { format: string; components: any }): Promise<void> {
export async function computeVersion(options: ComputeVersionOptions): Promise<void> {
const outputPath = resolveOutputPath(options.outputPath);

try {
// Gather git information
const commitHash = cp.execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
Expand Down Expand Up @@ -60,12 +101,14 @@ export async function computeVersion(strategyConfig: { format: string; component
};

// Create strategy and compute version
const strategy = VersioningStrategy.create(strategyConfig.format, strategyConfig.components);
const strategy = VersioningStrategy.create(options.strategyConfig.format, options.strategyConfig.components);
const computer = new VersionComputer(strategy);
const versionInfo = computer.compute(context);

fs.writeFileSync('~version.json', versionInfo.toJson());
console.log('Version computed:', versionInfo.version, '(commit:', versionInfo.shortCommitHash + ')');
ensureDirectory(outputPath);
fs.writeFileSync(outputPath, versionInfo.toJson());
console.log(`Version computed: ${versionInfo.version} (commit: ${versionInfo.shortCommitHash})`);
console.log(`Written to: ${shellSafePath(outputPath)}`);
} catch (error: any) {
console.error('Error computing version:', error.message);
const fallback = {
Expand All @@ -79,12 +122,68 @@ export async function computeVersion(strategyConfig: { format: string; component
deploymentUser: 'unknown',
environment: 'unknown',
};
fs.writeFileSync('~version.json', JSON.stringify(fallback, null, 2));
ensureDirectory(outputPath);
fs.writeFileSync(outputPath, JSON.stringify(fallback, null, 2));
}
}

/**
* Read version information from the output file.
*
* Checks the configured path first, then falls back to the legacy
* `~version.json` path with a deprecation warning.
*
* @param outputPath - Primary path to read from
* @returns The file contents as a string, or undefined if not found
*/
export function readVersionFile(outputPath?: string): string | undefined {
const primary = outputPath || DEFAULT_VERSION_OUTPUT_PATH;

if (fs.existsSync(primary)) {
return fs.readFileSync(primary, 'utf8');
}

// Backwards-compat fallback: read from legacy path with deprecation warning
if (fs.existsSync(LEGACY_VERSION_OUTPUT_PATH)) {
console.warn(
`[DEPRECATED] Reading version from "${LEGACY_VERSION_OUTPUT_PATH}". ` +
'This fallback will be removed in the next minor version. ' +
`Please update your workflow to use "${shellSafePath(DEFAULT_VERSION_OUTPUT_PATH)}" instead.`,
);
return fs.readFileSync(LEGACY_VERSION_OUTPUT_PATH, 'utf8');
}

return undefined;
}

/**
* Parse CLI arguments.
*
* Supports:
* compute-version [strategyJson]
* compute-version --output <path> [strategyJson]
*/
function parseCLIArgs(argv: string[]): { strategyConfig: string; outputPath?: string } {
const args = argv.slice(2);
let outputPath: string | undefined;
let strategyConfig = '{"format":"{commit-count}","components":{}}';

for (let i = 0; i < args.length; i++) {
if (args[i] === '--output' || args[i] === '-o') {
outputPath = args[++i];
} else if (!args[i].startsWith('-')) {
strategyConfig = args[i];
}
}

return { strategyConfig, outputPath };
}

// CLI entry point
if (require.main === module) {
const strategyConfig = JSON.parse(process.argv[2] || '{"format":"{commit-count}","components":{}}');
computeVersion(strategyConfig).catch(console.error);
const { strategyConfig, outputPath } = parseCLIArgs(process.argv);
computeVersion({
strategyConfig: JSON.parse(strategyConfig),
outputPath,
}).catch(console.error);
}
3 changes: 3 additions & 0 deletions src/versioning/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@ export * from './computation';
// CDK Constructs
export * from './version-outputs';

// Output path utilities
export { DEFAULT_VERSION_OUTPUT_PATH, LEGACY_VERSION_OUTPUT_PATH, validateOutputPath, shellSafePath } from './output-path';

// CLI utilities
export { computeVersion } from './compute-version';
Loading
Loading