diff --git a/.github/workflows/validate-version-bump.yaml b/.github/workflows/validate-version-bump.yaml new file mode 100644 index 0000000..48ae0b7 --- /dev/null +++ b/.github/workflows/validate-version-bump.yaml @@ -0,0 +1,109 @@ +--- +name: "Validate Version Bump" +on: + pull_request: + paths: + - 'src/**/devcontainer-feature.json' + +jobs: + validate-version-bump: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate version changes + run: | + set -e + + echo "Checking for version changes..." + + # Get changed files + CHANGED_FILES=$(git diff --name-only \ + origin/${{ github.base_ref }}...HEAD | \ + grep 'devcontainer-feature.json$' || true) + + if [ -z "$CHANGED_FILES" ]; then + echo "No devcontainer-feature.json files changed" + exit 0 + fi + + HAS_ERROR=0 + + for FILE in $CHANGED_FILES; do + echo "" + echo "Checking $FILE..." + + # Get old version + OLD_VERSION=$(git show \ + origin/${{ github.base_ref }}:$FILE | \ + jq -r '.version' 2>/dev/null || echo "") + NEW_VERSION=$(jq -r '.version' $FILE 2>/dev/null || echo "") + FEATURE_ID=$(jq -r '.id' $FILE 2>/dev/null || echo "unknown") + + if [ -z "$OLD_VERSION" ]; then + echo " New feature: $FEATURE_ID (version: $NEW_VERSION)" + continue + fi + + if [ -z "$NEW_VERSION" ]; then + echo " Error: Could not read version from $FILE" + HAS_ERROR=1 + continue + fi + + # Validate semantic versioning format + if ! echo "$NEW_VERSION" | \ + grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo " Error: Version does not follow semver" + HAS_ERROR=1 + continue + fi + + if [ "$OLD_VERSION" = "$NEW_VERSION" ]; then + echo " Warning: Version unchanged ($OLD_VERSION)" + else + echo " Version bumped: $OLD_VERSION -> $NEW_VERSION" + + # Parse versions + OLD_MAJOR=$(echo $OLD_VERSION | cut -d. -f1) + OLD_MINOR=$(echo $OLD_VERSION | cut -d. -f2) + OLD_PATCH=$(echo $OLD_VERSION | cut -d. -f3) + + NEW_MAJOR=$(echo $NEW_VERSION | cut -d. -f1) + NEW_MINOR=$(echo $NEW_VERSION | cut -d. -f2) + NEW_PATCH=$(echo $NEW_VERSION | cut -d. -f3) + + # Check if version increased + if [ $NEW_MAJOR -lt $OLD_MAJOR ]; then + echo " Error: Major version decreased" + HAS_ERROR=1 + elif [ $NEW_MAJOR -eq $OLD_MAJOR ]; then + if [ $NEW_MINOR -lt $OLD_MINOR ]; then + echo " Error: Minor version decreased" + HAS_ERROR=1 + elif [ $NEW_MINOR -eq $OLD_MINOR ]; then + if [ $NEW_PATCH -lt $OLD_PATCH ]; then + echo " Error: Patch version decreased" + HAS_ERROR=1 + fi + fi + fi + fi + done + + if [ $HAS_ERROR -eq 1 ]; then + echo "" + echo "Version validation failed" + exit 1 + fi + + echo "" + echo "All version changes are valid" + diff --git a/.gitignore b/.gitignore index 1298790..19e9b76 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,11 @@ dist # Finder (MacOS) folder config .DS_Store .direnv + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + diff --git a/README.md b/README.md index 6821ae8..c65d894 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,9 @@ A collection of devcontainer features. | [powerlevel10k](./src/powerlevel10k) | PowerLevel10k is a theme for Zsh. | | [rye](./src/rye) | A Hassle-Free Python Experience. | | [starship](./src/starship) | The minimal, blazing-fast, and infinitely customizable prompt for any shell! | + +## Development + +### Version Bumping + +See [docs/VERSION_BUMPING.md](./docs/VERSION_BUMPING.md) for information on bumping feature versions. diff --git a/docs/NUSHELL_INC_COMMAND.md b/docs/NUSHELL_INC_COMMAND.md new file mode 100644 index 0000000..e059fd5 --- /dev/null +++ b/docs/NUSHELL_INC_COMMAND.md @@ -0,0 +1,120 @@ +# Note on Nushell's `inc` Command + +## Background + +The Nushell documentation references an `inc` command that was designed to increment values, including semantic version strings: +- Documentation: https://www.nushell.sh/commands/docs/inc.html +- Supported flags: `--major`, `--minor`, `--patch` + +## Current Status + +The `inc` command was available in Nushell versions prior to 0.80 but has been **moved to a plugin** in current versions (0.107.0+). + +### nu_plugin_inc + +The `inc` functionality is now available as an official Nushell plugin: `nu_plugin_inc` + +## Installation + +### Step 1: Install the Plugin + +```bash +cargo install nu_plugin_inc +``` + +### Step 2: Register the Plugin with Nushell + +```bash +# Start nushell +nu + +# Register the plugin +plugin add ~/.cargo/bin/nu_plugin_inc + +# Or in one command: +nu -c "plugin add ~/.cargo/bin/nu_plugin_inc" +``` + +### Step 3: Verify Installation + +```bash +nu -c '"1.2.3" | inc --patch' +# Should output: 1.2.4 +``` + +## Implementation in bump-version.nu + +The `scripts/bump-version.nu` script now uses a **hybrid approach**: + +1. **First, try to use the `inc` plugin** if it's installed and registered +2. **Fallback to custom implementation** if the plugin is not available + +This ensures the script works whether or not the plugin is installed, while preferring the official plugin when available. + +### Code Implementation: + +```nushell +def bump_semver [ + version: string + bump_type: string +]: nothing -> string { + # Try to use inc plugin if available + let inc_result = try { + match $bump_type { + "major" => { $version | inc --major } + "minor" => { $version | inc --minor } + "patch" => { $version | inc --patch } + } + } catch { + null + } + + # If inc plugin worked, return the result + if $inc_result != null { + return $inc_result + } + + # Fallback to custom implementation + # [custom implementation code] +} +``` + +## Benefits + +1. **Plugin-first approach**: Uses official nu_plugin_inc when available +2. **Graceful fallback**: Works without the plugin for compatibility +3. **No manual configuration**: Script detects and uses the plugin automatically +4. **Best of both worlds**: Official plugin + guaranteed functionality + +## Advantages of Using the Plugin + +When the plugin is installed: +- ✅ Uses official, maintained code from the Nushell team +- ✅ Consistent with Nushell ecosystem +- ✅ Automatically updated with plugin updates +- ✅ Better integration with Nushell's type system + +## Compatibility + +- **With plugin**: Uses `nu_plugin_inc` (recommended) +- **Without plugin**: Uses custom implementation (automatic fallback) +- **Both approaches**: Produce identical results + +## Setup in CI/CD + +To use the plugin in CI/CD environments, add to your setup: + +```yaml +- name: Install nu_plugin_inc + run: | + cargo install nu_plugin_inc + nu -c "plugin add ~/.cargo/bin/nu_plugin_inc" +``` + +## Conclusion + +The script now follows best practices by: +1. Preferring the official `nu_plugin_inc` when available +2. Providing a fallback for environments without the plugin +3. Automatically detecting and using the appropriate method +4. Requiring no changes to the command-line interface diff --git a/docs/NUSHELL_RATIONALE.md b/docs/NUSHELL_RATIONALE.md new file mode 100644 index 0000000..8ec00b4 --- /dev/null +++ b/docs/NUSHELL_RATIONALE.md @@ -0,0 +1,112 @@ +# Nushell vs Python: Version Bumping Implementation + +This document explains why Nushell was chosen as the primary scripting language for version bumping. + +## Comparison + +### Native JSON Support + +**Python:** +```python +import json + +with open(json_path, 'r') as f: + data = json.load(f) + +# Modify data +data['version'] = new_version + +with open(json_path, 'w') as f: + json.dump(data, f, indent=4) +``` + +**Nushell:** +```nushell +let data = open $json_path +let updated_data = ($data | upsert version $new_version) +$updated_data | to json --indent 4 | save --force $json_path +``` + +### Structured Data Handling + +Nushell treats JSON, CSV, and other structured data as first-class citizens, making data manipulation more natural and less error-prone. + +### Type Safety + +**Python:** +- Dynamic typing with potential runtime errors +- Manual type checking needed +- Error handling via try/except + +**Nushell:** +- Strongly typed with better compile-time checks +- Built-in error handling with `error make` +- Type coercion with `into int`, `into string`, etc. + +### Error Messages + +**Python:** +``` +Traceback (most recent call last): + File "bump-version.py", line 59 + data = json.load(f) +json.decoder.JSONDecodeError: ... +``` + +**Nushell:** +``` +Error: nu::shell::error + × Error: Invalid JSON in file + ╭─[bump-version.nu:50:13] + 50 │ let data = try { + · ─┬─ + · ╰── originates from here +``` + +Nushell provides clearer, more actionable error messages with line numbers and context. + +## Benefits of Nushell for This Project + +1. **Native JSON Support**: No external libraries needed for JSON parsing +2. **Concise Syntax**: Less boilerplate code (155 lines in Python vs ~150 in Nushell) +3. **Better Error Handling**: More informative error messages +4. **Type Safety**: Fewer runtime errors +5. **Modern Shell**: Better integration with command-line workflows +6. **Performance**: Efficient structured data processing +7. **Maintainability**: Cleaner, more readable code + +## Note on `inc` Command + +Nushell's `inc` functionality for incrementing semantic version strings is now available as an official plugin: `nu_plugin_inc`. + +**Our implementation uses a hybrid approach:** +1. First attempts to use `nu_plugin_inc` if installed +2. Falls back to custom implementation if plugin is not available + +This provides the best of both worlds: +- Uses official plugin when available (recommended) +- Guarantees functionality even without the plugin +- No manual configuration needed + +**To install the plugin:** +```bash +cargo install nu_plugin_inc +nu -c "plugin add ~/.cargo/bin/nu_plugin_inc" +``` + +See [NUSHELL_INC_COMMAND.md](./NUSHELL_INC_COMMAND.md) for complete details on plugin installation and usage. + +## Installation + +Nushell can be installed via: +- Snap: `sudo snap install nushell --classic` +- Cargo: `cargo install nu` +- Package managers: See https://www.nushell.sh/book/installation.html + +## Compatibility + +Both Python and Nushell versions are maintained: +- **Nushell** (`bump-version.nu`): Primary, recommended version +- **Python** (`bump-version.py`): Legacy version for compatibility + +Both produce identical results and follow the same API. diff --git a/docs/VERSION_BUMPING.md b/docs/VERSION_BUMPING.md new file mode 100644 index 0000000..852d342 --- /dev/null +++ b/docs/VERSION_BUMPING.md @@ -0,0 +1,256 @@ +# Version Bumping Infrastructure + +This document describes the infrastructure for bumping versions of devcontainer features in this repository. + +## Overview + +All devcontainer features in this repository follow [Semantic Versioning](https://semver.org/) (MAJOR.MINOR.PATCH): +- **MAJOR**: Incompatible API changes +- **MINOR**: New functionality in a backward-compatible manner +- **PATCH**: Backward-compatible bug fixes + +## Tools + +### Bump Version Script (Nushell - Recommended) + +The `scripts/bump-version.nu` script is the primary tool for version bumping, written in [Nushell](https://www.nushell.sh/) for better structured data handling and modern shell features. + +#### Why Nushell? + +- **Native JSON Support**: Built-in commands for working with JSON without external dependencies +- **Type Safety**: Better error handling and data validation +- **Modern Syntax**: Clean, readable code that's easier to maintain +- **Performance**: Efficient data processing and manipulation +- **Plugin Support**: Can use official `nu_plugin_inc` for semantic versioning + +#### Prerequisites + +Install nushell: +```bash +# Using cargo (Rust package manager) +cargo install nu + +# Or download from https://github.com/nushell/nushell/releases +``` + +**Optional but recommended**: Install the `inc` plugin for enhanced functionality: +```bash +# Install the plugin +cargo install nu_plugin_inc + +# Register it with nushell +nu -c "plugin add ~/.cargo/bin/nu_plugin_inc" +``` + +The script works with or without the plugin - it will automatically use the plugin if available and fall back to a custom implementation otherwise. + +#### Usage + +```bash +# Bump patch version for a single feature +nu scripts/bump-version.nu --feature just --type patch + +# Bump minor version for a feature (dry run to preview) +nu scripts/bump-version.nu --feature playwright --type minor --dry-run + +# Bump major version for all features +nu scripts/bump-version.nu --all --type major + +# Bump patch version for all features (dry run) +nu scripts/bump-version.nu --all --type patch --dry-run +``` + +#### Options + +- `--feature FEATURE` or `-f FEATURE`: Specify a single feature to bump +- `--all` or `-a`: Bump all features +- `--type {major,minor,patch}` or `-t {major,minor,patch}`: Type of version bump (required) +- `--dry-run` or `-n`: Preview changes without modifying files + +### Python Version (Legacy) + +A Python version (`scripts/bump-version.py`) is maintained for compatibility but the Nushell version is recommended. + +```bash +# Same usage as nushell version +python3 scripts/bump-version.py --feature just --type patch +``` + +### Just Recipes + +If you have [just](https://github.com/casey/just) installed, you can use convenient recipes: + +```bash +# Bump a single feature (uses nushell) +just bump-version just patch +just bump-version playwright minor --dry-run + +# Bump all features +just bump-all-versions patch +just bump-all-versions minor --dry-run +``` + +## Workflow + +### When to Bump Versions + +- **Patch**: Bug fixes, documentation updates, minor improvements +- **Minor**: New features, new options, backward-compatible changes +- **Major**: Breaking changes, removed options, significant behavior changes + +### Process + +1. **Make your changes** to the feature (install scripts, documentation, etc.) + +2. **Bump the version** using one of the tools above: + ```bash + # Using nushell (recommended) + nu scripts/bump-version.nu --feature YOUR_FEATURE --type patch + + # Or using just + just bump-version YOUR_FEATURE patch + + # Or using python (legacy) + python3 scripts/bump-version.py --feature YOUR_FEATURE --type patch + ``` + +3. **Review the change**: + ```bash + git diff src/YOUR_FEATURE/devcontainer-feature.json + ``` + +4. **Commit your changes**: + ```bash + git add . + git commit -m "Bump YOUR_FEATURE version to X.Y.Z" + ``` + +5. **Create a pull request** + +### Automated Validation + +When you create a pull request that modifies `devcontainer-feature.json` files, the "Validate Version Bump" workflow automatically: + +- ✅ Checks that versions follow semantic versioning format +- ✅ Ensures versions don't decrease +- ✅ Reports version changes clearly +- ⚠️ Warns if versions are unchanged (but doesn't fail the build) + +### Publishing + +After your PR is merged to main, the "CI - Test Features" workflow: + +1. Validates all features +2. Runs tests +3. Publishes features to GitHub Container Registry (GHCR) +4. Generates updated documentation +5. Creates a PR with documentation updates + +## Examples + +### Example 1: Fix a Bug in a Feature + +```bash +# 1. Make your fix +vim src/just/install.sh + +# 2. Bump patch version (using nushell) +nu scripts/bump-version.nu --feature just --type patch + +# 3. Commit and push +git add . +git commit -m "Fix just installation on ARM architecture + +Bump version to 0.1.1" +git push origin my-bugfix-branch +``` + +### Example 2: Add a New Option + +```bash +# 1. Add the new option +vim src/playwright/devcontainer-feature.json +vim src/playwright/install.sh + +# 2. Bump minor version (using just recipe) +just bump-version playwright minor + +# 3. Commit and push +git add . +git commit -m "Add headless option to playwright feature + +Bump version to 0.2.0" +git push origin my-feature-branch +``` + +### Example 3: Bulk Patch Update + +```bash +# 1. Make changes to multiple features +vim src/*/install.sh + +# 2. Bump all features (preview first with nushell) +nu scripts/bump-version.nu --all --type patch --dry-run + +# 3. Apply the changes +just bump-all-versions patch + +# 4. Review and commit +git diff +git add . +git commit -m "Update all features to use updated base image + +Bump all feature versions" +git push origin bulk-update-branch +``` + +## Integration with devcontainer CLI + +This infrastructure works seamlessly with the devcontainer CLI: + +```bash +# Validate features +devcontainer features test -p . + +# Test a specific feature +devcontainer features test -f just -i debian:bookworm + +# Publish features (done automatically in CI) +devcontainer features publish ./src --namespace YOUR_NAMESPACE +``` + +## Troubleshooting + +### Script shows "File not found" error + +Make sure you're running the script from the repository root: +```bash +cd /path/to/devcontainer-features +python3 scripts/bump-version.py --feature just --type patch +``` + +### Version validation fails in CI + +The validation workflow checks that: +- Versions follow the X.Y.Z format +- Versions don't decrease +- JSON files are valid + +Review the error message in the workflow logs and fix the issue. + +### Just command not found + +Install just using the package manager or from [releases](https://github.com/casey/just/releases): +```bash +# Using cargo +cargo install just + +# Or download binary +curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/bin +``` + +## See Also + +- [Semantic Versioning](https://semver.org/) +- [devcontainer CLI Documentation](https://containers.dev/guide/cli) +- [devcontainers/action](https://github.com/devcontainers/action) diff --git a/justfile b/justfile index bb872a8..36835bb 100644 --- a/justfile +++ b/justfile @@ -1,2 +1,18 @@ test *args: - devcontainer features test -p . {{ args }} \ No newline at end of file + devcontainer features test -p . {{ args }} + +# Bump version for a specific feature (nushell version) +bump-version feature type="patch" dry_run="": + nu scripts/bump-version.nu --feature {{ feature }} --type {{ type }} {{ dry_run }} + +# Bump version for all features (nushell version) +bump-all-versions type="patch" dry_run="": + nu scripts/bump-version.nu --all --type {{ type }} {{ dry_run }} + +# Bump version for a specific feature (python version - legacy) +bump-version-py feature type="patch" dry_run="": + python3 scripts/bump-version.py --feature {{ feature }} --type {{ type }} {{ dry_run }} + +# Bump version for all features (python version - legacy) +bump-all-versions-py type="patch" dry_run="": + python3 scripts/bump-version.py --all --type {{ type }} {{ dry_run }} \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..198e152 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,63 @@ +# Scripts + +This directory contains scripts for maintaining devcontainer features. + +## bump-version.nu (Recommended) + +Nushell script to bump version numbers in `devcontainer-feature.json` files. This is the primary version bumping tool. + +### Why Nushell? + +- Native JSON support without external dependencies +- Modern, type-safe shell with better error handling +- Clean, readable syntax +- Efficient structured data processing +- Optional plugin support for enhanced functionality + +### Plugin Support + +The script automatically uses the `nu_plugin_inc` plugin if installed, providing official Nushell support for semantic versioning: + +```bash +# Install the plugin (optional but recommended) +cargo install nu_plugin_inc +nu -c "plugin add ~/.cargo/bin/nu_plugin_inc" +``` + +The script works with or without the plugin - it automatically detects and uses it if available. + +See [../docs/VERSION_BUMPING.md](../docs/VERSION_BUMPING.md) for complete documentation. + +### Quick Usage + +```bash +# Bump a single feature +nu scripts/bump-version.nu --feature just --type patch + +# Bump all features +nu scripts/bump-version.nu --all --type minor + +# Dry run (preview changes) +nu scripts/bump-version.nu --feature playwright --type patch --dry-run +``` + +## bump-version.py (Legacy) + +Python script for version bumping, maintained for compatibility. The Nushell version is recommended for new usage. + +```bash +# Same usage as Nushell version +python3 scripts/bump-version.py --feature just --type patch +``` + +## test_bump_version.py + +Unit tests for the Python bump-version script. + +### Running Tests + +```bash +python3 scripts/test_bump_version.py +``` + +All tests should pass before committing changes to the bump-version script. diff --git a/scripts/bump-version.nu b/scripts/bump-version.nu new file mode 100755 index 0000000..f8788c8 --- /dev/null +++ b/scripts/bump-version.nu @@ -0,0 +1,167 @@ +#!/usr/bin/env nu +# Bump version script for devcontainer features +# This script bumps the version of a devcontainer feature in its devcontainer-feature.json file. +# It follows semantic versioning (MAJOR.MINOR.PATCH). + +def main [ + --feature (-f): string # Feature name (directory name in src/) + --all (-a) # Bump version for all features + --type (-t): string # Type of version bump (major, minor, patch) + --dry-run (-n) # Show what would be changed without making changes +] { + # Validate that either --feature or --all is provided + if ($feature == null and not $all) { + error make {msg: "Error: Either --feature or --all must be specified"} + } + + if ($feature != null and $all) { + error make {msg: "Error: Cannot specify both --feature and --all"} + } + + # Validate bump type + if ($type not-in ["major", "minor", "patch"]) { + error make {msg: "Error: --type must be one of: major, minor, patch"} + } + + # Find the src directory + let repo_root = ($env.FILE_PWD | path dirname) + let src_dir = ($repo_root | path join "src") + + if not ($src_dir | path exists) { + error make {msg: $"Error: src directory not found at ($src_dir)"} + } + + # Process features + if $all { + # Get all feature directories + let features = (ls $src_dir + | where type == dir + | get name + | each { |dir| + let json_file = ($dir | path join "devcontainer-feature.json") + if ($json_file | path exists) { + $dir + } else { + null + } + } + | where $it != null + ) + + if ($features | is-empty) { + error make {msg: "Error: No features found in src/"} + } + + print $"Bumping ($type) version for ($features | length) features...\n" + + $features | each { |feature_dir| + bump_feature_version $feature_dir $type $dry_run + print "" + } + } else { + # Single feature + let feature_dir = ($src_dir | path join $feature) + + if not ($feature_dir | path exists) { + error make {msg: $"Error: Feature directory not found: ($feature_dir)"} + } + + bump_feature_version $feature_dir $type $dry_run + } +} + +# Bump version for a single feature +def bump_feature_version [ + feature_path: string + bump_type: string + dry_run: bool +] { + let json_path = ($feature_path | path join "devcontainer-feature.json") + + if not ($json_path | path exists) { + error make {msg: $"Error: File not found: ($json_path)"} + } + + # Read and parse JSON + let data = try { + open $json_path + } catch { + error make {msg: $"Error: Invalid JSON in ($json_path)"} + } + + # Check if version field exists + if not ("version" in ($data | columns)) { + error make {msg: $"Error: No 'version' field found in ($json_path)"} + } + + let old_version = $data.version + let new_version = (bump_semver $old_version $bump_type) + + # Print info + print $"Feature: ($data.id? | default 'unknown')" + print $" ($old_version) → ($new_version)" + + if $dry_run { + print " (dry run - no changes made)" + return + } + + # Update version and write back + let updated_data = ($data | upsert version $new_version) + $updated_data | to json --indent 4 | save --force $json_path + + # Add newline at end of file (to match Python behavior) + "\n" | save --append $json_path + + print $" ✓ Updated ($json_path)" +} + +# Bump a semantic version string +# Uses the nu_plugin_inc if available, otherwise falls back to custom implementation +def bump_semver [ + version: string + bump_type: string +]: nothing -> string { + # Try to use inc plugin if available + let inc_result = try { + match $bump_type { + "major" => { $version | inc --major } + "minor" => { $version | inc --minor } + "patch" => { $version | inc --patch } + _ => { error make {msg: $"Error: Invalid bump type: ($bump_type)"} } + } + } catch { + null + } + + # If inc plugin worked, return the result + if $inc_result != null { + return $inc_result + } + + # Fallback to custom implementation if inc plugin is not available + let parts = ($version | split row ".") + + if ($parts | length) != 3 { + error make {msg: $"Error: Version must have exactly 3 parts: ($version)"} + } + + let major = ($parts | get 0 | into int) + let minor = ($parts | get 1 | into int) + let patch = ($parts | get 2 | into int) + + match $bump_type { + "major" => { + $"($major + 1).0.0" + } + "minor" => { + $"($major).($minor + 1).0" + } + "patch" => { + $"($major).($minor).($patch + 1)" + } + _ => { + error make {msg: $"Error: Invalid bump type: ($bump_type)"} + } + } +} diff --git a/scripts/bump-version.py b/scripts/bump-version.py new file mode 100755 index 0000000..5a8f566 --- /dev/null +++ b/scripts/bump-version.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +""" +Bump version script for devcontainer features. + +This script bumps the version of a devcontainer feature in its devcontainer-feature.json file. +It follows semantic versioning (MAJOR.MINOR.PATCH). +""" + +import argparse +import json +import sys +from pathlib import Path + + +def parse_version(version_str): + """Parse a semantic version string into major, minor, patch integers.""" + try: + parts = version_str.split('.') + if len(parts) != 3: + raise ValueError(f"Version must have exactly 3 parts: {version_str}") + return tuple(int(p) for p in parts) + except (ValueError, AttributeError) as e: + print(f"Error: Invalid version format: {version_str}", file=sys.stderr) + print(f"Version must be in format MAJOR.MINOR.PATCH (e.g., 1.2.3)", file=sys.stderr) + sys.exit(1) + + +def bump_version(version_str, bump_type): + """Bump a version string according to the bump type.""" + major, minor, patch = parse_version(version_str) + + if bump_type == 'major': + major += 1 + minor = 0 + patch = 0 + elif bump_type == 'minor': + minor += 1 + patch = 0 + elif bump_type == 'patch': + patch += 1 + else: + raise ValueError(f"Invalid bump type: {bump_type}") + + return f"{major}.{minor}.{patch}" + + +def update_feature_version(feature_path, bump_type, dry_run=False): + """Update the version in a devcontainer-feature.json file.""" + json_path = Path(feature_path) / "devcontainer-feature.json" + + if not json_path.exists(): + print(f"Error: File not found: {json_path}", file=sys.stderr) + sys.exit(1) + + try: + with open(json_path, 'r') as f: + data = json.load(f) + except json.JSONDecodeError as e: + print(f"Error: Invalid JSON in {json_path}: {e}", file=sys.stderr) + sys.exit(1) + + if 'version' not in data: + print(f"Error: No 'version' field found in {json_path}", file=sys.stderr) + sys.exit(1) + + old_version = data['version'] + new_version = bump_version(old_version, bump_type) + + print(f"Feature: {data.get('id', 'unknown')}") + print(f" {old_version} → {new_version}") + + if dry_run: + print(" (dry run - no changes made)") + return old_version, new_version + + data['version'] = new_version + + with open(json_path, 'w') as f: + json.dump(data, f, indent=4) + f.write('\n') + + print(f" ✓ Updated {json_path}") + + return old_version, new_version + + +def main(): + parser = argparse.ArgumentParser( + description='Bump version for devcontainer features', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Bump patch version for a single feature + python3 scripts/bump-version.py --feature just --type patch + + # Bump minor version for a feature (dry run) + python3 scripts/bump-version.py --feature playwright --type minor --dry-run + + # Bump major version for all features + python3 scripts/bump-version.py --all --type major + """ + ) + + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--feature', '-f', help='Feature name (directory name in src/)') + group.add_argument('--all', '-a', action='store_true', help='Bump version for all features') + + parser.add_argument( + '--type', '-t', + required=True, + choices=['major', 'minor', 'patch'], + help='Type of version bump' + ) + + parser.add_argument( + '--dry-run', '-n', + action='store_true', + help='Show what would be changed without making changes' + ) + + args = parser.parse_args() + + # Find the src directory + script_dir = Path(__file__).parent + repo_root = script_dir.parent + src_dir = repo_root / "src" + + if not src_dir.exists(): + print(f"Error: src directory not found at {src_dir}", file=sys.stderr) + sys.exit(1) + + if args.all: + # Get all feature directories + features = [d for d in src_dir.iterdir() if d.is_dir() and (d / "devcontainer-feature.json").exists()] + if not features: + print("Error: No features found in src/", file=sys.stderr) + sys.exit(1) + + print(f"Bumping {args.type} version for {len(features)} features...\n") + + for feature_dir in sorted(features): + update_feature_version(feature_dir, args.type, args.dry_run) + print() + else: + # Single feature + feature_dir = src_dir / args.feature + if not feature_dir.exists(): + print(f"Error: Feature directory not found: {feature_dir}", file=sys.stderr) + sys.exit(1) + + update_feature_version(feature_dir, args.type, args.dry_run) + + +if __name__ == '__main__': + main() diff --git a/scripts/test_bump_version.nu b/scripts/test_bump_version.nu new file mode 100755 index 0000000..c613984 --- /dev/null +++ b/scripts/test_bump_version.nu @@ -0,0 +1,75 @@ +#!/usr/bin/env nu +# Test script for bump-version.nu + +def main [] { + print "=== Testing bump-version.nu ===" + print "" + + # Test 1: Check script exists + print "1. Checking if bump-version.nu exists..." + let script_path = "scripts/bump-version.nu" + if not ($script_path | path exists) { + print $" ❌ Error: ($script_path) not found" + exit 1 + } + print " ✓ Script exists" + print "" + + # Test 2: Test dry-run on single feature + print "2. Testing dry-run on single feature (just)..." + let result = (nu scripts/bump-version.nu --feature just --type patch --dry-run + | complete) + + if $result.exit_code != 0 { + print $" ❌ Error: Script failed with exit code ($result.exit_code)" + print $result.stderr + exit 1 + } + + if ($result.stdout | str contains "0.1.0 → 0.1.1") { + print " ✓ Dry-run works correctly" + } else { + print " ❌ Error: Unexpected output" + print $result.stdout + exit 1 + } + print "" + + # Test 3: Test dry-run with all features + print "3. Testing dry-run with all features..." + let result = (nu scripts/bump-version.nu --all --type patch --dry-run + | complete) + + if $result.exit_code != 0 { + print $" ❌ Error: Script failed with exit code ($result.exit_code)" + print $result.stderr + exit 1 + } + + if ($result.stdout | str contains "Bumping patch version") { + print " ✓ All features dry-run works" + } else { + print " ❌ Error: Unexpected output" + exit 1 + } + print "" + + # Test 4: Test different bump types + print "4. Testing different bump types..." + + for bump_type in ["major", "minor", "patch"] { + let result = (nu scripts/bump-version.nu --feature cypress --type $bump_type --dry-run + | complete) + + if $result.exit_code != 0 { + print $" ❌ Error: ($bump_type) bump failed" + exit 1 + } + } + print " ✓ All bump types work (major, minor, patch)" + print "" + + print "===================================" + print "✅ ALL TESTS PASSED!" + print "===================================" +} diff --git a/scripts/test_bump_version.py b/scripts/test_bump_version.py new file mode 100755 index 0000000..24cf0f9 --- /dev/null +++ b/scripts/test_bump_version.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +""" +Tests for the bump-version script. +""" + +import json +import tempfile +import shutil +from pathlib import Path +import sys +import os + +# Add scripts directory to path +script_dir = Path(__file__).parent +sys.path.insert(0, str(script_dir)) + +# Import from the bump-version script +import importlib.util +spec = importlib.util.spec_from_file_location("bump_version", script_dir / "bump-version.py") +bump_version_module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(bump_version_module) + +bump_version = bump_version_module.bump_version +parse_version = bump_version_module.parse_version +update_feature_version = bump_version_module.update_feature_version + + +def test_parse_version(): + """Test version parsing.""" + assert parse_version("1.2.3") == (1, 2, 3) + assert parse_version("0.0.1") == (0, 0, 1) + assert parse_version("10.20.30") == (10, 20, 30) + print("✓ test_parse_version passed") + + +def test_bump_version(): + """Test version bumping logic.""" + # Patch + assert bump_version("1.2.3", "patch") == "1.2.4" + assert bump_version("0.0.0", "patch") == "0.0.1" + + # Minor + assert bump_version("1.2.3", "minor") == "1.3.0" + assert bump_version("0.0.5", "minor") == "0.1.0" + + # Major + assert bump_version("1.2.3", "major") == "2.0.0" + assert bump_version("0.5.9", "major") == "1.0.0" + + print("✓ test_bump_version passed") + + +def test_update_feature_version(): + """Test updating a feature version in a JSON file.""" + + # Create a temporary directory with a test feature + with tempfile.TemporaryDirectory() as tmpdir: + feature_dir = Path(tmpdir) / "test-feature" + feature_dir.mkdir() + + json_path = feature_dir / "devcontainer-feature.json" + test_data = { + "id": "test-feature", + "version": "1.0.0", + "name": "Test Feature" + } + + with open(json_path, 'w') as f: + json.dump(test_data, f) + + # Test patch bump + old_ver, new_ver = update_feature_version(feature_dir, "patch") + assert old_ver == "1.0.0" + assert new_ver == "1.0.1" + + # Verify the file was updated + with open(json_path, 'r') as f: + updated_data = json.load(f) + assert updated_data["version"] == "1.0.1" + + print("✓ test_update_feature_version passed") + + +if __name__ == '__main__': + print("Running bump-version tests...") + test_parse_version() + test_bump_version() + test_update_feature_version() + print("\n✅ All tests passed!")