diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2de2364c..f032d79a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,16 +51,3 @@ jobs: uses: crate-ci/typos@v1.17.2 - name: Lint run: bun lint - - name: Check version - shell: bash - run: | - # check for version changes - ./update-version.sh - # Check if any changes were made in README.md files - if [[ -n "$(git status --porcelain -- '**/README.md')" ]]; then - echo "Version mismatch detected. Please run ./update-version.sh and commit the updated README.md files." - git diff -- '**/README.md' - exit 1 - else - echo "No version mismatch detected. All versions are up to date." - fi diff --git a/.github/workflows/update-readme-version.yaml b/.github/workflows/update-readme-version.yaml new file mode 100644 index 00000000..83a2c410 --- /dev/null +++ b/.github/workflows/update-readme-version.yaml @@ -0,0 +1,98 @@ +name: Auto Update README Version + +on: + push: + tags: + - "release/*/v*" # Matches tags like release/module-name/v1.0.0 + +jobs: + update-readme: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Extract tag information + id: tag + run: | + # Parse the tag name (e.g., "release/code-server/v1.2.3") + TAG_NAME="${GITHUB_REF#refs/tags/}" + # Extract module name (the middle part) + MODULE_NAME=$(echo $TAG_NAME | cut -d'/' -f2) + # Extract version (the last part, without the 'v' prefix) + VERSION=$(echo $TAG_NAME | cut -d'/' -f3 | sed 's/^v//') + + # Make these values available to other steps + echo "MODULE_NAME=$MODULE_NAME" >> $GITHUB_OUTPUT + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + echo "TAG_NAME=$TAG_NAME" >> $GITHUB_OUTPUT + + - name: Check if README version needs updating + id: check + env: + MODULE_NAME: ${{steps.tag.outputs.MODULE_NAME}} + VERSION: ${{steps.tag.outputs.VERSION}} + run: | + if ./update_version.sh --check $MODULE_NAME $VERSION + echo "NEEDS_UPDATE=false" >> $GITHUB_OUTPUT + echo "README version already matches tag version - no update needed" + else + echo "NEEDS_UPDATE=true" >> $GITHUB_OUTPUT + echo "README version doesn't match tag version - update needed" + fi + + # Update README with new version + - name: Update README version + if: steps.check.outputs.NEEDS_UPDATE == 'true' + env: + MODULE_NAME: ${{steps.tag.outputs.MODULE_NAME}} + VERSION: ${{steps.tag.outputs.VERSION}} + run: | + # Set git identity for commits + git config user.name "cdrci" + git config user.email "78873720+cdrci@users.noreply.github.com" + + # Update the README with the new version + ./update_version.sh $MODULE_NAME $VERSION + echo "Updated README version of $MODULE_NAME to $VERSION" + + - name: Create PR for version update + if: steps.check.outputs.NEEDS_UPDATE == 'true' && github.repository_owner == 'coder' + id: create-pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MODULE_NAME: ${{steps.tag.outputs.MODULE_NAME}} + VERSION: ${{steps.tag.outputs.VERSION}} + TAG: ${{steps.tag.outputs.TAG}} + run: | + BRANCH="auto-update-$MODULE_NAME-$VERSION" + PR_TITLE="chore($MODULE_NAME): update version to $VERSION" + git checkout -b "$BRANCH" + git add "$MODULE_NAME/README.md" + git commit -m "$PR_TITLE" + git push origin "$BRANCH" + + PR_URL=$(gh pr create \ + --title "$PR_TITLE" \ + --body "Updates module version to match the latest tag $TAG" \ + --base main \ + --head "$BRANCH") + + gh pr merge "$PR_URL" --auto --squash + echo "Enabled auto-merge for $PR_URL" + + # TODO: Add Autoapprove PR diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60b1260b..940c840e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,8 @@ Follow the instructions to ensure that Bun is available globally. Once Bun has b ## Testing a Module -> **Note:** It is the responsibility of the module author to implement tests for their module. The author must test the module locally before submitting a PR. +> [!NOTE] +> It is the responsibility of the module author to implement tests for their module. The author must test the module locally before submitting a PR. A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers. @@ -53,23 +54,52 @@ module "example" { ## Releases -> [!WARNING] -> When creating a new release, make sure that your new version number is fully accurate. If a version number is incorrect or does not exist, we may end up serving incorrect/old data for our various tools and providers. +The release process is automated with these steps: + +## 1. Create and Merge PR + +- Create a PR with your module changes +- Get your PR reviewed, approved, and merged to `main` + +## 2. Prepare Release (Maintainer Task) + +After merging to main, a maintainer will: + +- View all modules and their current versions: + + ```shell + ./release.sh --list + ``` + +- Determine the next version number based on changes: + + - **Patch version** (1.2.3 → 1.2.4): Bug fixes + - **Minor version** (1.2.3 → 1.3.0): New features, adding inputs, deprecating inputs + - **Major version** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing input types) + +- Create and push an annotated tag: + + ```shell + # Fetch latest changes + git fetch origin + + # Create and push tag + ./release.sh module-name 1.2.3 --push + ``` + + The tag format will be: `release/module-name/v1.0.0` -Much of our release process is automated. To cut a new release: +## 3. Automated Version Update -1. Navigate to [GitHub's Releases page](https://github.com/coder/modules/releases) -2. Click "Draft a new release" -3. Click the "Choose a tag" button and type a new release number in the format `v..` (e.g., `v1.18.0`). Then click "Create new tag". -4. Click the "Generate release notes" button, and clean up the resulting README. Be sure to remove any notes that would not be relevant to end-users (e.g., bumping dependencies). -5. Once everything looks good, click the "Publish release" button. +When the tag is pushed, a GitHub Action automatically: -Once the release has been cut, a script will run to check whether there are any modules that will require that the new release number be published to Terraform. If there are any, a new pull request will automatically be generated. Be sure to approve this PR and merge it into the `main` branch. +- Checks if the module's `README.md` version matches the tag version +- Updates the `README.md` version if needed +- Creates a PR with the version update -Following that, our automated processes will handle publishing new data for [`registry.coder.com`](https://github.com/coder/registry.coder.com/): +## 4. Publishing to Registry -1. Publishing new versions to Coder's [Terraform Registry](https://registry.terraform.io/providers/coder/coder/latest) -2. Publishing new data to the [Coder Registry](https://registry.coder.com) +Our automated processes will handle publishing new data to [registry.coder.com](https://registry.coder.com). > [!NOTE] -> Some data in `registry.coder.com` is fetched on demand from the Module repo's main branch. This data should be updated almost immediately after a new release, but other changes will take some time to propagate. +> Some data in registry.coder.com is fetched on demand from the [coder/modules](https://github.com/coder/modules) repo's `main` branch. This data should update almost immediately after a release, while other changes will take some time to propagate. diff --git a/package.json b/package.json index eea421d8..a122f4f2 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,9 @@ "name": "modules", "scripts": { "test": "bun test", - "fmt": "bun x prettier -w **/*.sh .sample/run.sh new.sh **/*.ts **/*.md *.md && terraform fmt **/*.tf .sample/main.tf", + "fmt": "bun x prettier -w **/*.sh .sample/run.sh new.sh terraform_validate.sh release.sh update_version.sh **/*.ts **/*.md *.md && terraform fmt **/*.tf .sample/main.tf", "fmt:ci": "bun x prettier --check **/*.sh .sample/run.sh new.sh **/*.ts **/*.md *.md && terraform fmt -check **/*.tf .sample/main.tf", - "lint": "bun run lint.ts && ./terraform_validate.sh", - "update-version": "./update-version.sh" + "lint": "bun run lint.ts && ./terraform_validate.sh" }, "devDependencies": { "bun-types": "^1.1.23", diff --git a/release.sh b/release.sh new file mode 100755 index 00000000..a5a5b11d --- /dev/null +++ b/release.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# +# release.sh - Creates annotated tags for module releases +# +# This script is used by maintainers to create annotated tags for module releases. +# It supports three main modes: +# 1. Creating a new tag for a module: ./release.sh module-name X.Y.Z +# 2. Creating and pushing a new tag for a module: ./release.sh module-name X.Y.Z --push +# 3. Listing all modules with their versions: ./release.sh --list +# +# When a tag is pushed, it triggers a GitHub workflow that updates README versions. + +set -euo pipefail + +# Function to extract version from README +extract_version() { + grep -o 'version *= *"[0-9]\+\.[0-9]\+\.[0-9]\+"' "$1" | head -1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "0.0.0" +} + +# Parse command line options +LIST=false +DRY_RUN=false +PUSH=false +TEMP=$(getopt -o 'ldp' --long 'list,dry-run,push' -n 'release.sh' -- "$@") +eval set -- "$TEMP" + +while true; do + case "$1" in + -l | --list) + LIST=true + shift + ;; + -d | --dry-run) + DRY_RUN=true + shift + ;; + -p | --push) + PUSH=true + shift + ;; + --) + shift + break + ;; + *) + echo "Internal error!" + exit 1 + ;; + esac +done + +# Handle listing all modules and their versions +if [[ "$LIST" == "true" ]]; then + # Display header for module listing + echo "Listing all modules and their latest versions:" + echo "----------------------------------------------------------" + printf "%-30s %-15s %s\n" "MODULE" "README VERSION" "LATEST TAG" + echo "----------------------------------------------------------" + + # Loop through all module directories + for dir in */; do + if [[ -d "$dir" && -f "${dir}README.md" && "$dir" != ".git/" ]]; then + module_name="${dir%/}" + + # Get README version + readme_version=$(extract_version "${dir}README.md") + + # Get latest tag for this module + latest_tag=$(git tag -l "release/${module_name}/v*" | sort -V | tail -n 1) + + # Set tag version with parameter expansion and default value + tag_version=${latest_tag:+${latest_tag#release/${module_name}/v}} + tag_version=${tag_version:-none} + + # Display module info + printf "%-30s %-15s %s\n" "$module_name" "$readme_version" "$tag_version" + fi + done + + echo "----------------------------------------------------------" + exit 0 +fi + +# Validate arguments for module release +if [[ "$#" -ne 2 ]]; then + echo "Usage: ./release.sh [--dry-run] module-name X.Y.Z" + echo " or: ./release.sh --list" + exit 1 +fi + +MODULE_NAME="$1" +VERSION="$2" + +# Validate version format +if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must be in format X.Y.Z" + exit 1 +fi + +# Check if module exists +if [[ ! -d "$MODULE_NAME" || ! -f "$MODULE_NAME/README.md" ]]; then + echo "Error: Module directory not found or missing README.md" + exit 1 +fi + +# Get current README version and construct tag name +README_VERSION=$(extract_version "$MODULE_NAME/README.md") +TAG_NAME="release/$MODULE_NAME/v$VERSION" + +# Check if tag already exists +if git rev-parse -q --verify "refs/tags/$TAG_NAME" > /dev/null; then + echo "Error: Tag $TAG_NAME already exists" + exit 1 +fi + +# Display release information +echo "Module: $MODULE_NAME" +echo "Current README version: $README_VERSION" +echo "New tag version: $VERSION" +echo "Tag name: $TAG_NAME" + +# Create the tag (or simulate in dry-run mode) +if [[ "$DRY_RUN" == "false" ]]; then + # Create annotated tag + git tag -a "$TAG_NAME" -m "Release $MODULE_NAME v$VERSION" + echo "Tag '$TAG_NAME' created." + if [[ "$PUSH" == "true" ]]; then + git push origin "$TAG_NAME" + echo "Success! Tag '$TAG_NAME' pushed to remote." + fi +else + echo "[DRY RUN] Would create tag: $TAG_NAME" +fi + +exit 0 diff --git a/update-version.sh b/update-version.sh deleted file mode 100755 index 09547f9c..00000000 --- a/update-version.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash - -# This script increments the version number in the README.md files of all modules -# by 1 patch version. It is intended to be run from the root -# of the repository or by using the `bun update-version` command. - -set -euo pipefail - -current_tag=$(git describe --tags --abbrev=0) - -# Increment the patch version -LATEST_TAG=$(echo "$current_tag" | sed 's/^v//' | awk -F. '{print $1"."$2"."$3+1}') || exit $? - -# List directories with changes that are not README.md or test files -mapfile -t changed_dirs < <(git diff --name-only "$current_tag" -- ':!**/README.md' ':!**/*.test.ts' | xargs dirname | grep -v '^\.' | sort -u) - -echo "Directories with changes: ${changed_dirs[*]}" - -# Iterate over directories and update version in README.md -for dir in "${changed_dirs[@]}"; do - if [[ -f "$dir/README.md" ]]; then - file="$dir/README.md" - tmpfile=$(mktemp /tmp/tempfile.XXXXXX) - awk -v tag="$LATEST_TAG" ' - BEGIN { in_code_block = 0; in_nested_block = 0 } - { - # Detect the start and end of Markdown code blocks. - if ($0 ~ /^```/) { - in_code_block = !in_code_block - # Reset nested block tracking when exiting a code block. - if (!in_code_block) { - in_nested_block = 0 - } - } - - # Handle nested blocks within a code block. - if (in_code_block) { - # Detect the start of a nested block (skipping "module" blocks). - if ($0 ~ /{/ && !($1 == "module" || $1 ~ /^[a-zA-Z0-9_]+$/)) { - in_nested_block++ - } - - # Detect the end of a nested block. - if ($0 ~ /}/ && in_nested_block > 0) { - in_nested_block-- - } - - # Update "version" only if not in a nested block. - if (!in_nested_block && $1 == "version" && $2 == "=") { - sub(/"[^"]*"/, "\"" tag "\"") - } - } - - print - } - ' "$file" > "$tmpfile" && mv "$tmpfile" "$file" - - # Check if the README.md file has changed - if ! git diff --quiet -- "$dir/README.md"; then - echo "Bumping version in $dir/README.md from $current_tag to $LATEST_TAG (incremented)" - else - echo "Version in $dir/README.md is already up to date" - fi - fi -done diff --git a/update_version.sh b/update_version.sh new file mode 100755 index 00000000..50271a46 --- /dev/null +++ b/update_version.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# +# update-version.sh - Updates or checks README.md version in module documentation + +set -eo pipefail + +# Display help message +show_help() { + echo "Usage: ./update-version.sh [--check|--help] MODULE_NAME VERSION" + echo + echo "Options:" + echo " --check Check if README.md version matches VERSION without updating" + echo " --help Display this help message and exit" + echo + echo "Examples:" + echo " ./update-version.sh code-server 1.2.3 # Update version in code-server/README.md" + echo " ./update-version.sh --check code-server 1.2.3 # Check if version matches 1.2.3" + echo +} + +# Handle help request +if [[ $# -eq 0 || "$1" == "--help" ]]; then + show_help + exit 0 +fi + +# Check if we're in check-only mode +CHECK_ONLY=false +if [[ "$1" == "--check" ]]; then + CHECK_ONLY=true + shift +fi + +# Validate we have the right number of arguments +if [[ "$#" -ne 2 ]]; then + echo "Error: Incorrect number of arguments" + echo "Expected exactly 2 arguments (MODULE_NAME VERSION)" + echo + show_help + exit 1 +fi + +MODULE_NAME="$1" +VERSION="$2" + +# Validate version format (X.Y.Z) +if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must be in format X.Y.Z (e.g., 1.2.3)" + exit 1 +fi + +# Check if module directory exists +if [[ ! -d "$MODULE_NAME" ]]; then + echo "Error: Module directory '$MODULE_NAME' not found" + echo "Available modules:" + find . -type d -mindepth 1 -maxdepth 1 -not -path "*/\.*" | sed 's|^./||' | sort + exit 1 +fi + +# Check if README.md exists +if [[ ! -f "$MODULE_NAME/README.md" ]]; then + echo "Error: README.md not found in '$MODULE_NAME' directory" + exit 1 +fi + +# Extract version from README.md file +extract_version() { + grep -o 'version *= *"[0-9]\+\.[0-9]\+\.[0-9]\+"' "$1" | head -1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "0.0.0" +} + +# Update version in README.md file +update_version() { + local file="$1" latest_tag=$2 tmpfile + tmpfile=$(mktemp) + echo "Updating version in $file from $(extract_version "$file") to $latest_tag..." + + awk -v tag="$2" ' + BEGIN { in_code_block = 0; in_nested_block = 0 } + { + # Track code blocks (```...```) + if ($0 ~ /^```/) { + in_code_block = !in_code_block + if (!in_code_block) { in_nested_block = 0 } + } + + # Inside code blocks, track nested {} blocks + if (in_code_block) { + if ($0 ~ /{/ && !($1 == "module" || $1 ~ /^[a-zA-Z0-9_]+$/)) { + in_nested_block++ + } + if ($0 ~ /}/ && in_nested_block > 0) { + in_nested_block-- + } + + # Only update version if not in a nested block + if (!in_nested_block && $1 == "version" && $2 == "=") { + sub(/"[^"]*"/, "\"" tag "\"") + } + } + print + } + ' "$1" > "$tmpfile" && mv "$tmpfile" "$1" +} + +README_PATH="$MODULE_NAME/README.md" +README_VERSION=$(extract_version "$README_PATH") + +# In check mode, just return success/failure based on version match +if [[ "$CHECK_ONLY" == "true" ]]; then + if [[ "$README_VERSION" == "$VERSION" ]]; then + echo "✅ Success: Version in $README_PATH matches $VERSION" + exit 0 + else + echo "❌ Error: Version mismatch in $README_PATH" + echo "Expected: $VERSION" + echo "Found: $README_VERSION" + exit 1 + fi +fi + +# Update the version if needed +if [[ "$README_VERSION" != "$VERSION" ]]; then + update_version "$README_PATH" "$VERSION" + echo "✅ Version updated successfully to $VERSION" +else + echo "ℹ️ Version in $README_PATH already set to $VERSION, no update needed" +fi + +exit 0