diff --git a/.changes/1.1.0.json b/.changes/1.1.0.json deleted file mode 100644 index 5c5198f..0000000 --- a/.changes/1.1.0.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "type": "api-change", - "category": "vks", - "description": "Align node group commands with latest API: remove --image-id (no longer in the API), add --os (ubuntu/linux, default ubuntu) for create-cluster/create-nodegroup; update-nodegroup no longer requires/sends image" - }, - { - "type": "feature", - "category": "vserver", - "description": "Add vServer CLI: vpc, subnet, secgroup, volume, volume-type, flavor, image, and server commands" - } -] diff --git a/.changes/1.2.0.json b/.changes/1.2.0.json deleted file mode 100644 index 02799cc..0000000 --- a/.changes/1.2.0.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - { - "type": "api-change", - "category": "vks", - "description": "Remove get-nodegroup-events; network-type CALICO->TIGERA; --os adds rocky; replace boolean toggle flags with --private-cluster/--private-nodes/--load-balancer-plugin/--block-store-csi-plugin " - }, - { - "type": "bugfix", - "category": "configure", - "description": "Fix panic (nil pointer) when running 'grn configure --profile ' for a profile that does not exist yet; start from empty defaults so the profile can be created" - }, - { - "type": "bugfix", - "category": "vks", - "description": "Return a non-zero exit code and 'unknown command' error for invalid subcommands of grn vks/vserver (and their groups) instead of silently printing help with exit 0" - } -] diff --git a/.changes/1.3.0.json b/.changes/1.3.0.json deleted file mode 100644 index 5d2a574..0000000 --- a/.changes/1.3.0.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "type": "api-change", - "category": "vks", - "description": "update-nodegroup: drop deprecated --labels/--taints (use update-nodegroup-metadata); replace --auto-scale-min/max and --upgrade-strategy/max-surge/max-unavailable with struct flags --auto-scale and --upgrade-config" - }, - { - "type": "feature", - "category": "vks", - "description": "Add struct-valued flags (shorthand or JSON) to create-nodegroup: --tags, --secondary-subnets, --auto-scale, --placement-group, --upgrade-config" - } -] diff --git a/.changes/1.3.1.json b/.changes/1.3.1.json deleted file mode 100644 index 9f3370b..0000000 --- a/.changes/1.3.1.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "type": "bugfix", - "category": "configure", - "description": "Fix panic (nil pointer) when running 'grn configure set/list --profile ' for a profile that does not exist yet. LoadConfig now reads the credentials and config files independently, so a profile created via 'configure set' (config file only) loads its region/output/project_id correctly, and 'configure get'/'configure list' on a truly missing profile report a clear 'profile does not exist' error (exit 1, like 'aws configure') instead of crashing" - }, - { - "type": "bugfix", - "category": "core", - "description": "Honor --cli-connect-timeout: the TCP connect and TLS handshake are now bounded by the flag (previously it was accepted but ignored, so a slow/unreachable endpoint hung for ~127s regardless). Wired via the HTTP transport's dialer in both VKS and vServer clients" - }, - { - "type": "bugfix", - "category": "output", - "description": "Reject an invalid --output or --color value (e.g. a typo like 'tabel') with a clear error, a 'maybe you meant' suggestion, and a non-zero exit, instead of silently falling back" - }, - { - "type": "enhancement", - "category": "core", - "description": "On error, print a single concise 'Error: ...' line instead of cobra's duplicated error plus a full usage dump (use 'grn --help' for usage)" - }, - { - "type": "enhancement", - "category": "vks", - "description": "Document the --unhealthy-range format in config-auto-healing help: expects \"[min-max]\" (e.g. \"[2-5]\")" - } -] diff --git a/.changes/1.4.0.json b/.changes/1.4.0.json deleted file mode 100644 index ae288a5..0000000 --- a/.changes/1.4.0.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "type": "api-change", - "category": "vks", - "description": "Rename 'set-auto-upgrade-config' to 'config-auto-upgrade'; the old name still works as a deprecated alias" - }, - { - "type": "bugfix", - "category": "configure", - "description": "Mask credential values in 'configure set' output so client_id/client_secret are no longer echoed in plaintext to stdout (consistent with 'configure list'); non-sensitive values are still shown" - }, - { - "type": "bugfix", - "category": "vks", - "description": "grn vks wait now aborts immediately on a permanent error (HTTP 403/401/400, or 404 for an active waiter) instead of polling until timeout; transient errors (5xx, network) still retry" - }, - { - "type": "enhancement", - "category": "core", - "description": "Add --dry-run to destructive commands that lacked it (vServer server stop/reboot/delete, volume/vpc/subnet/secgroup/secgroup-rule delete, and vks delete-auto-upgrade-config) and unify the preview + confirmation flow across delete commands via shared helpers" - }, - { - "type": "feature", - "category": "output", - "description": "Implement the --color flag (on/off/auto, like aws): color status values (ACTIVE=green, ERROR/FAILED=red, CREATING/PENDING=yellow) in text and table output. auto colors only when stdout is a terminal and NO_COLOR is unset; JSON output is never colored" - } -] diff --git a/.changes/1.4.1.json b/.changes/1.4.1.json deleted file mode 100644 index 5ba962a..0000000 --- a/.changes/1.4.1.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "type": "enhancement", - "category": "vks", - "description": "Add --dry-run to the remaining mutating VKS commands (config-auto-healing, config-auto-upgrade, update-nodegroup-metadata, upgrade-nodegroup-version, generate-kubeconfig); it previews the request payload and exits without calling the API (works offline)" - } -] diff --git a/.changes/1.5.0.json b/.changes/1.5.0.json deleted file mode 100644 index 979568e..0000000 --- a/.changes/1.5.0.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - { - "type": "api-change", - "category": "vks", - "description": "create-cluster now provisions the control plane only and no longer creates a default node group: removed --node-group-name/--flavor-id/--os/--disk-type/--ssh-key-id/--disk-size/--num-nodes/--private-nodes/--security-groups/--labels/--taints. Create workers separately with create-nodegroup" - }, - { - "type": "bugfix", - "category": "core", - "description": "Redact credential fields (embedded kubeconfig, tokens, client certs/keys, secrets) in --debug request/response logging so 'update-kubeconfig --debug' and similar no longer print long-lived credentials to stderr (SEC-07)" - }, - { - "type": "enhancement", - "category": "core", - "description": "Guard --endpoint-url against sending the IAM bearer token to untrusted hosts (SEC-08): hosts outside vngcloud.vn/greenode.ai are warned over TLS, and blocked when there is no TLS protection (plain http or --no-verify-ssl) unless --allow-untrusted-endpoint is set" - } -] diff --git a/.changes/1.6.0.json b/.changes/1.6.0.json deleted file mode 100644 index 354341c..0000000 --- a/.changes/1.6.0.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "type": "enhancement", - "category": "core", - "description": "cli.ParseStructFlag gains bool-field coercion (ParseStructFlagTyped) so struct flags can carry boolean values in shorthand" - }, - { - "type": "feature", - "category": "vks", - "description": "create-cluster gains the remaining optional CreateClusterDto fields: --secondary-subnets, --list-subnet-ids, --node-netmask-size, --service-endpoint, --az-strategy, and struct-valued --auto-upgrade-config/--auto-healing-config (shorthand or JSON, matching create-nodegroup). --subnet-id is now optional (per spec; pass --subnet-id or --list-subnet-ids or neither, the server validates)" - } -] diff --git a/.changes/next-release/.gitkeep b/.changes/next-release/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6326241..1b0518a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,8 +7,9 @@ /go/cmd/configure/ @vngcloud/platform /go/main.go @vngcloud/platform -# Shared docs, changelog, and CI — additive but cross-team; platform reviews. -/.changes/ @vngcloud/platform +# Shared release/CI config and docs — cross-team; platform reviews. +/release-please-config.json @vngcloud/platform +/.release-please-manifest.json @vngcloud/platform /.github/ @vngcloud/platform /mkdocs.yml @vngcloud/platform /docs/development/ @vngcloud/platform diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4cd7165..1f7b4f4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,8 +12,8 @@ Brief description of what this PR does. ## Checklist -- [ ] Tests added/updated and passing (`python -m pytest tests/ -v`) -- [ ] Changelog entry added (`./scripts/new-change`) +- [ ] Tests added/updated and passing (`cd go && go test ./...`) +- [ ] PR title is a Conventional Commit (`feat:`/`fix:`/`feat!:`) — drives the release - [ ] Documentation updated (if applicable) - [ ] Code follows existing patterns diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..266c862 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,20 @@ +name: Conventional Commits title + +# release-please derives the version bump and CHANGELOG from commit messages. +# We squash-merge, so the PR title becomes the commit message — it must be a +# valid Conventional Commit (feat:, fix:, docs:, chore:, feat!:, ...). + +on: + pull_request: + types: [opened, edited, synchronize] + +permissions: + pull-requests: read + +jobs: + title: + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..c25cdc1 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,40 @@ +name: Release Please + +on: + push: + branches: [main] + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + outputs: + release_created: ${{ steps.rp.outputs.release_created }} + tag_name: ${{ steps.rp.outputs.tag_name }} + steps: + # Optional fine-grained PAT (contents: write, pull-requests: write). With a + # PAT the release PR's required checks start automatically; with the default + # GITHUB_TOKEN you must close & reopen the release PR once to kick checks. + - uses: googleapis/release-please-action@v4 + id: rp + with: + token: ${{ secrets.RELEASE_PLEASE_TOKEN || github.token }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + # When release-please cuts a release (release PR merged), build the binaries and + # attach them to the just-created GitHub Release. Done in-run so we don't depend + # on the release tag triggering the tag-push workflow (GITHUB_TOKEN-created tags + # do not trigger push workflows). + build-release: + needs: release-please + if: ${{ needs.release-please.outputs.release_created == 'true' }} + uses: ./.github/workflows/release.yml + with: + tag: ${{ needs.release-please.outputs.tag_name }} + permissions: + contents: write + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d3fe77..217a72b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,12 @@ on: version: description: 'Version to release (e.g. 0.2.0)' required: true + workflow_call: + inputs: + tag: + description: 'Release tag to build and attach binaries to (e.g. v1.7.0)' + required: true + type: string permissions: contents: write @@ -70,9 +76,18 @@ jobs: path: dist/ merge-multiple: true - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.ref_name }} - generate_release_notes: true - files: dist/grn-* + - name: Attach binaries to the release + env: + GH_TOKEN: ${{ github.token }} + # tag comes from the workflow_call input; otherwise the pushed tag ref. + TAG: ${{ inputs.tag || github.ref_name }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then + # Release already exists (e.g. created by release-please) — attach binaries. + gh release upload "$TAG" dist/grn-* --clobber --repo "$REPO" + else + # Manual tag push / dispatch — create the release with generated notes. + gh release create "$TAG" dist/grn-* --generate-notes --repo "$REPO" + fi diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..0d1bebe --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.6.0" +} diff --git a/CLAUDE.md b/CLAUDE.md index f042ed7..547187b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -139,8 +139,11 @@ GOOS=windows GOARCH=amd64 go build -o grn-windows-amd64.exe . - **Do not auto commit/push** — only change source code, user will ask for commit/push - **Main branch is protected** — must use PR -- **Changelog**: `./scripts/new-change` for every change -- **Release**: `./scripts/bump-version minor` → `git push && git push --tags` +- **Versioning/CHANGELOG are automated by release-please** — never edit the version + (`go/cmd/root.go`) or `CHANGELOG.md` by hand +- **PR titles are Conventional Commits** (`feat:`/`fix:`/`feat!:`) — PRs are squash-merged, + so the title becomes the release commit release-please reads +- **Release**: merge the `chore: release main` PR → tags `vX.Y.Z` + GitHub Release + binaries ## Documentation update rule @@ -157,7 +160,6 @@ GOOS=windows GOARCH=amd64 go build -o grn-windows-amd64.exe . - `CLAUDE.md` - `CONTRIBUTING.md` - `docs/development/contributing.md` and `docs/development/architecture.md` (adding services/commands) -- `./scripts/new-change` — changelog fragment > Note: `docs/superpowers/` (design specs/plans) is local-only and git-ignored — > never commit it to this public repo. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92954f1..143259d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,31 +41,37 @@ CGO_ENABLED=0 go build -o grn . ./grn vks --help ``` -### 3. Add a changelog entry +### 3. Versioning & changelog are automated (release-please) -Every PR should include a changelog fragment: +**Do not edit the version or CHANGELOG by hand.** [release-please](https://github.com/googleapis/release-please) +derives the version bump and `CHANGELOG.md` from commit messages. Because PRs are +**squash-merged**, the **PR title becomes the commit message**, so it must be a +valid [Conventional Commit](https://www.conventionalcommits.org/) — this is +enforced by the `Conventional Commits title` check. -```bash -./scripts/new-change -t feature -c vks -d "Add your feature description" +``` +feat(vks): add describe-events command # minor bump +fix(auth): fix token refresh race condition # patch bump +feat!: drop deprecated --foo flag # breaking (see note) +docs(readme): update installation # no release ``` -Change types: `feature`, `bugfix`, `enhancement`, `api-change` - -### 4. Commit and push +Bump rules: `fix:` → patch, `feat:` → minor, `feat!:`/`BREAKING CHANGE:` → major. +Getting a wrong bump? Fix the PR title — never touch the version file. -Follow [Conventional Commits](https://www.conventionalcommits.org/): +### 4. Create a Pull Request -``` -feat(vks): add describe-events command -fix(auth): fix token refresh race condition -docs(readme): update installation instructions -``` +- PR to `main`; use a Conventional Commit title (the `Conventional Commits title` + check gates it). +- CI (`Run Tests`) must pass before merge. +- **Squash-merge** so the PR title lands as the release commit. -### 5. Create a Pull Request +### 5. Releasing -- PR to `main` when release-ready -- CI must pass before merge -- At least 1 approval required +release-please opens/refreshes a `chore: release main` PR that bumps the version +(in `go/cmd/root.go`) and updates `CHANGELOG.md`. Merge that PR to publish: it +tags `vX.Y.Z`, creates the GitHub Release, and the release workflow attaches the +built binaries. ## Adding a New Command diff --git a/docs/development/contributing.md b/docs/development/contributing.md index afb6266..f1f0724 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -24,7 +24,9 @@ go build -o grn . 7. (Optional) Register flag value completion — see [Architecture](architecture.md#shell-completion) 8. Document it: create `docs/commands//.md`, add it to the command index table and to the `mkdocs.yml` nav -9. Add a changelog fragment: `./scripts/new-change -t feature -c -d "..."` + +The changelog/version are automated by release-please from your Conventional +Commit PR title — no manual changelog step. ## Adding a new service diff --git a/docs/development/release.md b/docs/development/release.md index fb3bf10..21e2ad8 100644 --- a/docs/development/release.md +++ b/docs/development/release.md @@ -1,41 +1,46 @@ # Release Process -## Adding changelog entries +Versioning and the changelog are automated by [release-please](https://github.com/googleapis/release-please). +**Never edit the version (`go/cmd/root.go`) or `CHANGELOG.md` by hand**, and never +create tags manually — release-please derives everything from commit messages. -```bash -./scripts/new-change # Interactive -./scripts/new-change -t feature -c vks -d "Add new command" # CLI args -``` +## During development -Change types: `feature`, `bugfix`, `enhancement`, `api-change` +- Branch from `main`, open a PR with a **Conventional Commits** title + (`feat:`, `fix:`, `docs:`, `feat!:`, …). The `Conventional Commits title` check + enforces this. +- PRs are **squash-merged**, so the PR title becomes the commit message that + release-please reads. -## Creating a release +Bump rules: `fix:` → patch, `feat:` → minor, `feat!:` / `BREAKING CHANGE:` → major. +Wrong bump? Fix the PR title — do not touch the version file. -```bash -./scripts/bump-version patch # 0.1.0 → 0.1.1 -./scripts/bump-version minor # 0.1.0 → 0.2.0 -./scripts/bump-version major # 0.1.0 → 1.0.0 -git push && git push --tags # Triggers GitHub Actions release -``` +## Cutting a release -## Release flow +1. As `feat`/`fix` PRs merge to `main`, release-please opens/refreshes a + **`chore: release main`** PR that bumps the version in `go/cmd/root.go` and + updates `CHANGELOG.md`. +2. **Squash-merge that release PR.** release-please then tags `vX.Y.Z` and creates + the GitHub Release; the release workflow builds the multi-platform binaries and + attaches them. -``` -Developer workflow: -1. During development: ./scripts/new-change (add fragments per PR) -2. Ready to release: ./scripts/bump-version minor -3. Push: git push && git push --tags +> If the release PR's checks don't start (a limitation of the default +> `GITHUB_TOKEN`), close & reopen it once — or configure a `RELEASE_PLEASE_TOKEN` +> PAT (contents: write, pull-requests: write) so checks run automatically. -GitHub Actions (automatic): -4. Build Go binaries for Linux/macOS/Windows (amd64 + arm64) -5. Create GitHub Release with binaries attached -``` +## Manual / emergency release + +`release.yml` still accepts a tag push (`git tag vX.Y.Z && git push --tags`) or a +`workflow_dispatch`, which builds the binaries and creates the release directly. +Prefer the release-please flow for normal releases. ## CI/CD workflows | Workflow | Trigger | Purpose | |----------|---------|---------| -| `run-tests.yml` | PR to main/develop | Build + test Go binary | -| `release.yml` | Tag push `v*`, manual dispatch | Build multi-platform binaries + GitHub Release | +| `run-tests.yml` | PR + push | Build + test Go binary | +| `pr-title.yml` | PR opened/edited | Enforce Conventional Commits PR title | +| `release-please.yml` | Push to `main` | Maintain the release PR; on merge, tag + release + build binaries | +| `release.yml` | Tag push `v*`, dispatch, or called by release-please | Build multi-platform binaries + attach to the release | | `deploy-docs.yml` | Push to main (docs/) | Deploy documentation to GitHub Pages | | `stale.yml` | Daily schedule | Auto-close stale issues | diff --git a/go/cmd/root.go b/go/cmd/root.go index 423b602..739dd79 100644 --- a/go/cmd/root.go +++ b/go/cmd/root.go @@ -11,7 +11,7 @@ import ( "github.com/vngcloud/greennode-cli/internal/config" ) -const cliVersion = "1.6.0" +const cliVersion = "1.6.0" // x-release-please-version // Global flags var ( diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..94c42e1 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "simple", + "include-component-in-tag": false, + "include-v-in-tag": true, + "bump-minor-pre-major": false, + "packages": { + ".": { + "package-name": "greennode-cli", + "extra-files": [ + "go/cmd/root.go" + ] + } + }, + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance" }, + { "type": "refactor", "section": "Refactors" }, + { "type": "docs", "section": "Documentation" }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "ci", "section": "CI", "hidden": true }, + { "type": "chore", "section": "Chores", "hidden": true } + ] +} diff --git a/scripts/bump-version b/scripts/bump-version deleted file mode 100755 index 21f6023..0000000 --- a/scripts/bump-version +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -# Bump version, merge changelog, commit, and tag. -# Usage: ./scripts/bump-version - -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -VERSION_FILE="${PROJECT_DIR}/go/cmd/root.go" - -if [ -z "$1" ]; then - echo "Usage: $0 " - exit 1 -fi - -BUMP_TYPE="$1" - -# Extract current version from Go source -CURRENT_VERSION=$(grep 'const cliVersion' "$VERSION_FILE" | sed 's/.*"\(.*\)".*/\1/') - -if [ -z "$CURRENT_VERSION" ]; then - echo "Error: Could not read version from $VERSION_FILE" - exit 1 -fi - -echo "Current version: ${CURRENT_VERSION}" - -# Parse semver -IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" - -# Bump -case "$BUMP_TYPE" in - major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;; - minor) MINOR=$((MINOR + 1)); PATCH=0 ;; - patch) PATCH=$((PATCH + 1)) ;; - *) - echo "Error: Invalid bump type '$BUMP_TYPE'. Use: major, minor, patch" - exit 1 - ;; -esac - -NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" -echo "New version: ${NEW_VERSION}" - -# Update Go source -if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s/const cliVersion = \"${CURRENT_VERSION}\"/const cliVersion = \"${NEW_VERSION}\"/" "$VERSION_FILE" -else - sed -i "s/const cliVersion = \"${CURRENT_VERSION}\"/const cliVersion = \"${NEW_VERSION}\"/" "$VERSION_FILE" -fi - -echo "Updated ${VERSION_FILE}" - -# Merge changelog fragments -echo "" -python3 "${SCRIPT_DIR}/render-changelog" --merge "${NEW_VERSION}" - -# Commit and tag -cd "$PROJECT_DIR" -git add go/cmd/root.go .changes/ CHANGELOG.md -git commit -m "release: v${NEW_VERSION}" -git tag "v${NEW_VERSION}" - -echo "" -echo "Release v${NEW_VERSION} ready!" -echo "Run: git push && git push --tags" diff --git a/scripts/new-change b/scripts/new-change deleted file mode 100755 index ca7d46e..0000000 --- a/scripts/new-change +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -"""Create a changelog fragment in .changes/next-release/.""" - -import argparse -import json -import os -import random -import string -import sys - -VALID_TYPES = ['feature', 'bugfix', 'enhancement', 'api-change'] -CHANGES_DIR = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - '.changes', 'next-release', -) - - -def random_id(length=8): - return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) - - -def prompt(message, valid_options=None): - while True: - response = input(f'{message}: ').strip() - if valid_options and response not in valid_options: - print(f' Valid options: {", ".join(valid_options)}') - continue - if response: - return response - print(' Value is required.') - - -def main(): - parser = argparse.ArgumentParser(description='Create a changelog entry') - parser.add_argument('-t', '--type', choices=VALID_TYPES, help='Change type') - parser.add_argument('-c', '--category', help='Category (e.g. vks, auth, configure)') - parser.add_argument('-d', '--description', help='Description of the change') - args = parser.parse_args() - - change_type = args.type or prompt(f'Type ({"/".join(VALID_TYPES)})', VALID_TYPES) - category = args.category or prompt('Category') - description = args.description or prompt('Description') - - entry = { - 'type': change_type, - 'category': category, - 'description': description, - } - - os.makedirs(CHANGES_DIR, exist_ok=True) - filename = f'{change_type}-{category}-{random_id()}.json' - filepath = os.path.join(CHANGES_DIR, filename) - - with open(filepath, 'w') as f: - json.dump(entry, f, indent=2) - f.write('\n') - - print(f'Created: .changes/next-release/{filename}') - return 0 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/scripts/render-changelog b/scripts/render-changelog deleted file mode 100755 index 736fad1..0000000 --- a/scripts/render-changelog +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 -"""Render CHANGELOG.md from .changes/ fragment files.""" - -import glob -import json -import os -import sys -from datetime import date - -CHANGES_DIR = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - '.changes', -) -CHANGELOG_PATH = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - 'CHANGELOG.md', -) - -TYPE_HEADERS = { - 'feature': 'Features', - 'bugfix': 'Bug Fixes', - 'enhancement': 'Enhancements', - 'api-change': 'API Changes', -} - -TYPE_ORDER = ['feature', 'enhancement', 'bugfix', 'api-change'] - - -def load_fragments(directory): - """Load all JSON fragments from a directory.""" - entries = [] - pattern = os.path.join(directory, '*.json') - for filepath in sorted(glob.glob(pattern)): - if os.path.basename(filepath) == '.gitkeep': - continue - with open(filepath) as f: - data = json.load(f) - if isinstance(data, list): - entries.extend(data) - else: - entries.append(data) - return entries - - -def merge_next_release(version): - """Merge next-release fragments into a version file.""" - next_dir = os.path.join(CHANGES_DIR, 'next-release') - entries = load_fragments(next_dir) - if not entries: - print('No unreleased changes found in .changes/next-release/') - return False - - version_file = os.path.join(CHANGES_DIR, f'{version}.json') - with open(version_file, 'w') as f: - json.dump(entries, f, indent=2) - f.write('\n') - - for filepath in glob.glob(os.path.join(next_dir, '*.json')): - os.remove(filepath) - - print(f'Merged {len(entries)} entries into .changes/{version}.json') - return True - - -def render_changelog(): - """Generate CHANGELOG.md from all version files.""" - versions = [] - for filepath in glob.glob(os.path.join(CHANGES_DIR, '*.json')): - basename = os.path.basename(filepath) - if basename == '.gitkeep': - continue - version = basename.replace('.json', '') - with open(filepath) as f: - entries = json.load(f) - if not isinstance(entries, list): - entries = [entries] - versions.append((version, entries)) - - def version_key(v): - parts = v[0].split('.') - return tuple(int(p) for p in parts if p.isdigit()) - - versions.sort(key=version_key, reverse=True) - - lines = ['# Changelog\n'] - for version, entries in versions: - lines.append(f'\n## {version}\n') - - by_type = {} - for entry in entries: - t = entry.get('type', 'enhancement') - by_type.setdefault(t, []).append(entry) - - for change_type in TYPE_ORDER: - if change_type not in by_type: - continue - header = TYPE_HEADERS.get(change_type, change_type) - lines.append(f'\n### {header}\n') - for entry in by_type[change_type]: - category = entry.get('category', '') - desc = entry.get('description', '') - if category: - lines.append(f'* **{category}**: {desc}\n') - else: - lines.append(f'* {desc}\n') - - with open(CHANGELOG_PATH, 'w') as f: - f.writelines(lines) - - print(f'Generated {CHANGELOG_PATH}') - - -def main(): - import argparse - parser = argparse.ArgumentParser(description='Render CHANGELOG.md') - parser.add_argument('--merge', metavar='VERSION', - help='Merge next-release fragments into VERSION') - args = parser.parse_args() - - if args.merge: - if not merge_next_release(args.merge): - return 1 - - render_changelog() - return 0 - - -if __name__ == '__main__': - sys.exit(main())