diff --git a/CHANGELOG.md b/CHANGELOG.md index faca89c..72f793c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- Self-release detection now recognizes releasegen running from the repository + root. `RELEASEGEN_SELF_MODULE` defaults to the root module (empty path) and the + feature is gated on `RELEASEGEN_SELF_REPO`, so the released version is printed + to stdout and downstream steps (e.g. the Docker build/push) are no longer skipped. +### Changed +- **BREAKING CHANGE** - No change, just bumping to v1.0.0. ## [[v0.1.0](https://github.com/C2FO/releasegen/releases/tag/v0.1.0)] - 2026-06-16 ### Added diff --git a/README.md b/README.md index 67ba6dc..4c592a2 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,44 @@ -![releasegen-logo.png](docs/images/releasegen-logo.png) +![ReleaseGen — changelog-driven releases, automated](docs/images/releasegen-logo.png) # ReleaseGen ---- +> Turn the `CHANGELOG.md` you already maintain into versioned Git tags and GitHub Releases — automatically. -`ReleaseGen` is a Go application designed to automate versioning and release creation based on the content of `CHANGELOG.md` files. The application adheres to Semantic Versioning (SemVer) principles, ensuring that versions are incremented correctly based on the types of changes documented in your changelog. +ReleaseGen is a small Go tool that reads your changelog and does the mechanical part of cutting a release for you. You write a normal [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) entry under `## [Unreleased]`; when you merge to your release branch, ReleaseGen: -You write a normal, human-readable [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) entry under `## [Unreleased]`; when you merge to your release branch, ReleaseGen decides the next version, promotes those notes into a numbered section, commits and tags it, and publishes a matching GitHub Release. That's the whole job — no plugins, no runtime, no DSL. +1. decides the next [SemVer](https://semver.org/spec/v2.0.0.html) version from the kinds of changes you listed, +2. promotes those notes into a new numbered section and commits it, +3. creates the matching Git tag, and +4. publishes a GitHub Release using your changelog notes as the body. + +That's the whole job — no plugins, no runtime, no DSL, and it never inspects your source code. + +## Table of Contents + +- [Why ReleaseGen?](#why-releasegen) +- [Features](#features) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Writing Your `CHANGELOG.md`](#writing-your-changelogmd) +- [GitHub Actions Integration](#github-actions-integration) +- [Configuration](#configuration) +- [Building From Source](#building-from-source) +- [FAQ](#faq) +- [Contributing](#contributing) +- [License](#license) ## Why ReleaseGen? -Most release automation derives the version and notes from **commit messages** (Conventional Commits) or from special **intent files**. ReleaseGen takes the position that the changelog you already maintain *is* the source of truth, and that the only thing standing between a merged PR and a published release is mechanical work a tool should do for you. +Most release automation derives the version and notes from **commit messages** (Conventional Commits) or from special **intent files**. ReleaseGen takes the position that the changelog you already curate *is* the source of truth, and that the only thing standing between a merged PR and a published release is mechanical work a tool should do for you. -It is a good fit when you want: +It's a good fit when you want: - **Changelog-driven, not commit-driven.** Your `CHANGELOG.md` is the contract. You don't have to enforce Conventional Commits, squash policies, or commit linting across every contributor to get correct versions. -- **Language-agnostic monorepo releases.** A "module" is just any directory containing a `CHANGELOG.md`. Each gets its own independent version line and tag (e.g. `services/api/v1.2.3`). It works equally well for Go, Node, Python, or polyglot repos — it never inspects your source. -- **One small, auditable step.** A single static binary (or container image) that does exactly one thing: turn curated changelog intent into a tag + GitHub Release. It composes with whatever builds and publishes your artifacts, rather than replacing them. -- **Safe by default.** `--dry-run` previews every decision, runs fail atomically (a bad module aborts the run rather than half-releasing), bearer tokens are scrubbed from error output, and structured exit codes let CI branch on the failure class instead of grepping logs. - -![releasegen-features.png](docs/images/releasegen-features.png) +- **Language-agnostic monorepo releases.** A "module" is just any directory containing a `CHANGELOG.md`. Each gets its own independent version line and tag (e.g. `services/api/v1.2.3`). It works equally well for Go, Node, Python, or polyglot repos. +- **One small, auditable step.** A single static binary (or container image) that does exactly one thing and composes with whatever already builds and publishes your artifacts, rather than replacing it. +- **Safety by default.** `--dry-run` previews every decision, a bad module aborts the run instead of half-releasing, tokens are scrubbed from error output, and structured exit codes let CI branch on the failure class instead of grepping logs. -### How it compares +### How It Compares | Tool | Decides version from | Monorepo model | Scope | | ---- | -------------------- | -------------- | ----- | @@ -29,10 +46,27 @@ It is a good fit when you want: | semantic-release | Conventional Commit messages | Plugins / extra config | Versioning + publishing (Node-centric) | | release-please | Conventional Commit messages | Release PRs per package | Release PR + tag + release | | Changesets | Hand-written intent files (`.changeset/`) | First-class (JS/TS workspaces) | Versioning + publishing (JS-centric) | -| GoReleaser | Existing git tags | N/A (builds artifacts) | Build + package + publish | +| GoReleaser | Existing Git tags | N/A (builds artifacts) | Build + package + publish | ReleaseGen deliberately does **not** build artifacts, publish to package registries, open PRs, or write your changelog for you. If you need those, run ReleaseGen for the version/tag/release step and pair it with your existing build tooling. +## Features + +![ReleaseGen feature overview](docs/images/releasegen-features.png) + +- **Automatic SemVer versioning.** The kinds of changes under `## [Unreleased]` decide whether the next version is a major, minor, or patch bump. +- **Monorepo support.** Discovers every `CHANGELOG.md` in the repo and releases each module independently with its own version line and tag. +- **GitHub Releases.** Creates the tag and a GitHub Release whose notes come straight from your changelog. +- **Dry-run previews.** `--dry-run` prints the next version and bump type without rewriting files, committing, pushing, tagging, or publishing. +- **Atomic, fail-fast runs.** A failing module aborts the run rather than leaving you half-released. +- **Structured exit codes.** Distinct codes for config, changelog, Git, and API failures so CI can branch on the failure class. See [Exit Codes](#exit-codes). +- **Machine-readable summaries.** `--summary-file` writes a JSON summary of the run for downstream steps to consume instead of scraping logs. +- **Config via flags or env.** Every environment variable has an equivalent CLI flag; flags take precedence over env, which takes precedence over built-in defaults. +- **Custom change types.** Map your own changelog headings (e.g. `Documentation`) to a specific bump level. +- **Debug logging.** `--debug` traces tag discovery and module-name extraction for troubleshooting. +- **Secure by default.** Bearer tokens are scrubbed from Git push errors before they reach the logs. +- **Structured logging.** `log/slog`-based output; under GitHub Actions it emits `::group::` / `::endgroup::` / `::error::` markers, and plain text locally. + ## Quick Start Install the CLI with Go: @@ -41,137 +75,67 @@ Install the CLI with Go: go install github.com/c2fo/releasegen/cmd/releasegen@latest ``` -Or pull the container image from GitHub Container Registry: +…or pull the container image from GitHub Container Registry: ```bash docker pull ghcr.io/c2fo/releasegen:latest ``` -Preview what would happen for your repo without changing anything: +Preview what a release would do for your repo, without changing anything: ```bash -releasegen --dry-run --repo-root . --repository your-org/your-repo --branch main --actor "$USER" --token "$GH_TOKEN" +releasegen --dry-run \ + --repo-root . \ + --repository your-org/your-repo \ + --branch main \ + --actor "$USER" \ + --token "$GH_TOKEN" ``` When you're ready to automate it, drop the [example GitHub Actions workflow](#workflow-example) into `.github/workflows/`. -## Features - ---- - -- **Automatic Versioning**: Detects changes in `CHANGELOG.md` and increments your project’s version following SemVer. -- **Monorepo Support**: Discovers changes to `CHANGELOG.md` files across different directories and generates appropriate release tags for each module. -- **Release Tagging**: Creates a Git tag for the new version, optionally prefixed by the directory path if the `CHANGELOG.md` file is located outside the repo’s root. -- **GitHub Releases**: Automatically creates a GitHub release, pulling release notes directly from your changelog. - ## How It Works ---- - ### Versioning Logic -ReleaseGen inspects the `CHANGELOG.md` file for notable changes and applies version bumps according to SemVer: +ReleaseGen reads the entries under `## [Unreleased]` and applies the highest applicable bump: -- **Major Version Bump**: If the words "BREAKING CHANGE" appear under any “Changed” or “Removed” sections, indicating backward-incompatible changes. -- **Minor Version Bump**: If there are new features, non-breaking changes, deprecations, or security updates. -- **Patch Version Bump**: If only bug fixes are found. -- **Manual Version Override**: If the MANUAL_VERSION environment variable is set, it overrides the calculated version. +| Bump | Triggered by | +| ---- | ------------ | +| **Major** | The exact phrase `BREAKING CHANGE` appears under a `### Changed` or `### Removed` section. | +| **Minor** | New features (`### Added`), `### Deprecated`, or `### Security` entries. | +| **Patch** | Only bug fixes (`### Fixed`). | -### Monorepo Support & Release Tag Naming +If `MANUAL_VERSION` (or `--manual-version`) is set, that value is used instead of the computed bump. When no tags exist yet, the repository is treated as starting from `v0.0.0`. -If you maintain multiple modules in a single repository (a monorepo), ReleaseGen will: -1. Detect all `CHANGELOG.md` files in different subdirectories. -2. Assign separate release tags to each directory that has new changes under ## [Unreleased]. +### Monorepo Support & Tag Naming -![releasegen-mono.png](docs/images/releasegen-mono.png) +![ReleaseGen monorepo tag naming](docs/images/releasegen-mono.png) -#### Single Module (Root) +Any directory containing a `CHANGELOG.md` is treated as a module. ReleaseGen processes each one independently and only releases the modules whose `## [Unreleased]` section has new entries. -- The tag is simply `vX.Y.Z` (e.g., `v1.2.3`). +- **Root module** → the tag is `vX.Y.Z` (e.g. `v1.2.3`). +- **Nested module** → the tag is prefixed with the module's path (e.g. `worker/v2.3.4` or `services/api/v0.2.0`). -#### Multiple Modules (Monorepo) - -- The tag is prefixed by the path to the module, (e.g. `worker/v2.3.4` or `services/api/v0.2.0`). - -This convention keeps releases organized in larger repositories. A future enhancement may allow “flat” tag naming if you prefer to omit directory prefixes. +Prefixing tags with the directory path keeps releases organized and prevents collisions in larger repositories. ### Custom Change Types -You can define custom change types and their corresponding bump types using the `CUSTOM_CHANGE_TYPES` environment variable. For example: +Map additional changelog headings to a bump level with `CUSTOM_CHANGE_TYPES` (or `--custom-change-types`) using newline-separated `:` pairs, where `` is `major`, `minor`, or `patch`: ```yaml CUSTOM_CHANGE_TYPES: | - documentation:patch - performance:minor + Documentation:minor + Performance:patch ``` ### Debug Mode -For troubleshooting tag detection issues, enable detailed logging with the `DEBUG` environment variable or the `--debug` flag: +Set `DEBUG=true` (or pass `--debug`) to trace which tags are processed, the module names extracted from them, and which tags are added versus skipped — useful when tags aren't being detected as expected in a multi-module repo. -```yaml -DEBUG: true -``` - -When enabled, ReleaseGen will output detailed information about: - -- Which tags are being processed -- Module names extracted from tags -- Which tags are successfully added vs skipped - -This is particularly useful for diagnosing issues in multi-module repositories or when tags aren't being detected as expected. - -### v2 highlights - -Releasegen v2 ships several quality-of-life and safety improvements while -keeping the v1 contract intact for end users (CHANGELOG format, env vars, -GitHub Actions integration). The breaking changes are mostly internal / -distribution-side: - -- **New module path.** When consumed as a library, the import is - `github.com/c2fo/releasegen/...`. The CLI binary path is - unchanged inside the docker image. -- **`c2fo/vfs/v7` and `golang.org/x/oauth2` are gone.** The binary uses the - standard library + `google/go-github/v68` only. -- **CLI flags for every env var.** Every documented env var has a matching - `--flag`. Flags > env > built-in defaults. -- **`--dry-run`.** Prints what would happen (next version, bump type) without - rewriting files, committing, pushing, tagging, or publishing. Safe to run - locally against your real repo. -- **`--summary-file`** / **`SUMMARY_FILE`.** Writes a JSON summary of the - run that downstream workflow steps can read instead of screen-scraping - logs. -- **`--repo-root`** / **`REPO_ROOT`.** Run releasegen against a worktree - that isn't `.`. -- **`--version`.** Prints the build-time version. -- **Structured exit codes.** `0` (success / nothing to do), `1` (config), - `2` (changelog validation), `3` (git), `4` (GitHub API), `10` (internal). - CI scripts can branch on these instead of grepping logs. -- **Structured logging.** `log/slog`-based; in GitHub Actions - (`GITHUB_ACTIONS=true`) it still emits `::group::`, `::endgroup::`, and - `::error::` markers. Locally, output is plain text. -- **Validated `MANUAL_VERSION`.** Must be a valid semver string before it is - used. -- **Token scrubbing.** Bearer tokens are stripped from go-git push errors - before they reach the logs. -- **Configurable self-release.** The "releasegen releasing itself" detection - (`RELEASEGEN_SELF_MODULE` / `RELEASEGEN_SELF_REPO`) is now overridable; - defaults are unchanged for c2fo/releasegen. - -### Building locally - -```bash -go build -ldflags "-X main.version=$(git describe --tags --always)" -o release-gen ./cmd/releasegen -./release-gen --help -# Preview what a release would do against another checkout, without writing anything: -./release-gen --dry-run --repo-root /path/to/your/repo --repository your-org/your-repo --branch main --actor you --token "$GH_TOKEN" -``` - -## Example `CHANGELOG.md` +## Writing Your `CHANGELOG.md` ---- - -Your CHANGELOG.md should follow the `CHANGELOG.md` files following the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. For example: +Your changelog must follow the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. ReleaseGen reads everything under `## [Unreleased]` to compute and cut the next release. ```markdown # Changelog @@ -187,84 +151,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New feature X. ### Changed -- Modified behavior of Y. - **BREAKING CHANGE**: Changed API behavior in module Z. -### Removed -- Deprecated feature W removed. -- **BREAKING CHANGE**: Removed support for legacy API. - ### Deprecated - Feature V is now deprecated. +### Removed +- **BREAKING CHANGE**: Removed support for the legacy API. + ### Security - Updated dependencies for security patches. ### Fixed - Fixed bug related to issue #123. -## [my-project/v1.2.3] - 2024-08-09 +## [v1.2.3] - 2024-08-09 + ### Added -- Another new feature. +- A previously released feature. +``` -### Fixed -- Fixed a minor bug. +A few conventions keep parsing reliable: -``` +1. **Use the standard headings** — `### Added`, `### Changed`, `### Removed`, `### Deprecated`, `### Security`, `### Fixed` (case-insensitive). Add your own via [custom change types](#custom-change-types). +2. **Mark breaking changes** with the exact phrase `BREAKING CHANGE`. This is the only thing that triggers a major bump, which guards against accidental major releases. +3. **Don't hand-edit version numbers.** Keep new entries under `## [Unreleased]` and let ReleaseGen promote them when you merge. -Note that while we require adhering to the Keep a Changelog format, `ReleaseGen` allows for custom change type headings when used with the env var `CUSTOM_CHANGE_TYPES`. - -## Developer Expectations - ---- - -When using `ReleaseGen`, developers should follow these guidelines to ensure the application can parse the `CHANGELOG.md` file correctly: - -1. **Use Section Headings**: (`### Added`, `### Changed`, `### Removed`, `### Deprecated`, `### Security`, `### Fixed`) — these are case-insensitive. -2. **Mark Breaking Changes**: Include the exact phrase “BREAKING CHANGE” for backward-incompatible changes to ensure a major version bump. This safeguards against unintentional major version increments. -3. **Don’t Manually Change Versions**: Keep new changes under ## [Unreleased]. Let `ReleaseGen` handle the version bump when merged to `main`. -4. **Maintain Consistency**: Be clear and consistent in wording so the application can parse changes accurately. -5. **Organize New Entries**: Always add new changes under the ## [Unreleased] section so that `ReleaseGen` can move them into the next release. - -## Integrating ReleaseGen into a GitHub Actions Workflow - ---- - -### Prerequisites: GitHub App Setup for Branch Protection - -To enable ReleaseGen to work with branch protection rules (requiring PR reviews, status checks, etc.), you need to create a GitHub App that can bypass these protections: - -1. **Create a GitHub App**: - - Go to `https://github.com/settings/apps` (personal) or `https://github.com/organizations/YOUR_ORG/settings/apps` (organization) - - Click **New GitHub App** - - Set a name (e.g., `releasegen-bot`) - - Set Homepage URL to your repository URL - - Uncheck **Webhook** → Active - - Set **Repository permissions**: - - Contents: **Read and write** - - Click **Create GitHub App** - - **Save the App ID** shown at the top of the settings page -2. **Generate Private Key**: - - On the app settings page, scroll to "Private keys" - - Click **Generate a private key** - - Download and save the `.pem` file securely -3. **Install the App**: - - Go to app settings → **Install App** (left sidebar) - - Install on your organization or account - - Select the repositories where you want to use ReleaseGen -4. **Add Secrets and Variables**: - - For each repository, go to Settings → Secrets and variables → Actions - - Add secret: `RELEASEGEN_APP_PRIVATE_KEY` = contents of the `.pem` file - - Add secret: `RELEASEGEN_APP_ID` = your App ID -5. **Configure Branch Protection**: - - Go to repository Settings → Rules - - Create or edit branch protection for `main` - - Enable desired protections (PR reviews, status checks, etc.) - - Under **Bypass list**, add your GitHub App by name +## GitHub Actions Integration -### Workflow Example +### GitHub App Setup (for branch protection) + +If your release branch is protected (required reviews, status checks, etc.), the release commit and tag must come from an identity allowed to bypass those rules. The cleanest way is a dedicated GitHub App: -Below is an example GitHub Actions workflow to automate releases using `ReleaseGen`: +1. **Create a GitHub App** at `https://github.com/settings/apps` (personal) or `https://github.com/organizations/YOUR_ORG/settings/apps` (organization): + - Click **New GitHub App**, give it a name (e.g. `releasegen-bot`), and set the Homepage URL to your repo. + - Uncheck **Webhook → Active**. + - Under **Repository permissions**, set **Contents: Read and write**. + - Click **Create GitHub App** and note the **App ID**. +2. **Generate a private key** on the app settings page ("Private keys" → **Generate a private key**) and save the `.pem` file securely. +3. **Install the app** (left sidebar → **Install App**) on the repositories where you'll use ReleaseGen. +4. **Add repository secrets** (Settings → Secrets and variables → Actions): + - `RELEASEGEN_APP_ID` = your App ID + - `RELEASEGEN_APP_PRIVATE_KEY` = contents of the `.pem` file +5. **Allow the app to bypass branch protection** (Settings → Rules → your `main` ruleset → **Bypass list** → add the app). + +### Workflow Example ```yaml name: Release by Changelog @@ -289,17 +220,16 @@ on: jobs: release: runs-on: ubuntu-latest - steps: - name: Generate GitHub App token id: generate-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.RELEASEGEN_APP_ID }} private-key: ${{ secrets.RELEASEGEN_APP_PRIVATE_KEY }} - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6 + uses: actions/checkout@v6 with: ref: ${{ github.event.inputs.branch || github.ref_name }} fetch-depth: 0 @@ -313,14 +243,14 @@ jobs: GITHUB_REF_NAME: ${{ github.event.inputs.branch || github.ref_name }} MANUAL_VERSION: ${{ github.event.inputs.version || '' }} REASON: ${{ github.event.inputs.reason || '' }} - # Optional: EXCLUDE_DIRS exclude certain directories from changelog generation + # Optional: skip directories from changelog-based releases. EXCLUDE_DIRS: | some/app some/other/app - # Optional: CUSTOM_CHANGE_TYPES allow for custom change types + # Optional: map custom headings to bump levels. CUSTOM_CHANGE_TYPES: | - documentation:patch - performance:minor + Documentation:minor + Performance:patch run: | docker run --rm \ -e GITHUB_TOKEN \ @@ -331,141 +261,106 @@ jobs: -e REASON \ -e EXCLUDE_DIRS \ -e CUSTOM_CHANGE_TYPES \ - -v $(pwd):/workspace \ + -v "$(pwd):/workspace" \ ghcr.io/c2fo/releasegen:latest \ --repo-root /workspace ``` -> The image's entrypoint is `/usr/local/bin/release-gen`, so any args after -> the image name are passed directly. Use `--dry-run` to preview without -> publishing, or `--summary-file /workspace/release-summary.json` to capture -> a machine-readable result. - -### Explanation of the Workflow - -- **Generate GitHub App token**: Creates a short-lived authentication token from your GitHub App credentials that can bypass branch protection rules. -- **Checkout repository**: Checks out your repository using the app token so that the workflow has access to the code and `CHANGELOG.md`. -- **Run ReleaseGen**: Runs the `ReleaseGen` Docker container, which reads the `CHANGELOG.md`, determines the next version, commits the updated changelog back to the main branch, creates tags, and generates a GitHub release. -- **Environment Variables**: - - `GITHUB_TOKEN`: The GitHub App token for authentication (required) - - `GITHUB_REPOSITORY`: The repository identifier (required) - - `GITHUB_ACTOR`: The user who triggered the workflow (required) - - `GITHUB_REF_NAME`: The release branch (required; usually injected by Actions) - - `MANUAL_VERSION` / `REASON`: Used by the manual workflow dispatch to force a specific version - - `EXCLUDE_DIRS`: Optional list of directories to exclude from changelog-based releases - - `CUSTOM_CHANGE_TYPES`: Optional custom change types and their corresponding version increments - - `REPO_ROOT`: Optional path to the git working tree (defaults to `.`; useful when invoking from outside the repo) - - `SUMMARY_FILE`: Optional path; when set, releasegen writes a JSON summary of the run there - - `DEBUG`: When `true`, emits verbose tag/discovery diagnostics - - `RELEASEGEN_SELF_MODULE` / `RELEASEGEN_SELF_REPO`: Identify the "releasegen releasing itself" case so that the resolved version is printed to stdout for downstream workflow steps. Defaults are `releasegen` and `c2fo/releasegen`; override only if you fork. - -Every environment variable above also has an equivalent CLI flag. Flag values take precedence over environment values, which take precedence over built-in defaults. - -### Manual Release Workflow Dispatch - -If you want to **manually** trigger a release on a different branch: - -1. Go to **Actions** in your repository. -2. Select **Release by Changelog**. -3. Click **Run workflow**. -4. Choose the branch. -5. (Optional) Add a reason or message. -6. Click **Run workflow** again. - -This will create a release based on the changes in the specified branch. - -## FAQ - ---- - -### Table of Contents - -1. [What happens when no tags exist yet?](#what-happens-when-no-tags-exist-yet) -2. [What if there are no changes in the CHANGELOG.md?](#what-if-there-are-no-changes-in-the-changelogmd) -3. [How does ReleaseGen determine which version to increment?](#how-does-releasegen-determine-which-version-to-increment) -4. [Will a major version bump automatically update my go.mod in a Golang project?](#will-a-major-version-bump-automatically-update-my-gomod-in-a-golang-project) -5. [Can I exclude certain directories from release generation?](#can-i-exclude-certain-directories-from-release-generation) -6. [What if there are multiple CHANGELOG.md files in different directories?](#what-if-there-are-multiple-changelogmd-files-in-different-directories) -7. [Why does the release tag include the directory path in a monorepo?](#why-does-the-release-tag-include-the-directory-path-in-a-monorepo) -8. [Can I manually trigger a release from a specific branch?](#can-i-manually-trigger-a-release-from-a-specific-branch) -9. [What if I want to advance the version to a specific number?](#what-if-i-want-to-advance-the-version-to-a-specific-number) -10. [What if an error occurs during the release process?](#what-if-an-error-occurs-during-the-release-process) -11. [Can I customize the versioning logic?](#can-i-customize-the-versioning-logic) -12. [How can I contribute to ReleaseGen?](#how-can-i-contribute-to-releasegen) +> The image entrypoint is `/usr/local/bin/release-gen`, so anything after the image name is passed straight to the CLI. Add `--dry-run` to preview without publishing, or `--summary-file /workspace/release-summary.json` to capture a machine-readable result. +> +> The example uses readable version tags for clarity. For production, pin actions to a commit SHA. -### What happens when no tags exist yet? +### Manual Releases -`ReleaseGen` treats the repository as though it started at v0.0.0. It will create the first tag according to the changes found under ## [Unreleased]. +To cut a release on demand (for example, from a non-default branch or to force a specific version): -### What if there are no changes in the CHANGELOG.md? +1. Open **Actions** → **Release by Changelog** → **Run workflow**. +2. Choose the branch, optionally set a version and a reason, and run it. -No new release is created. `ReleaseGen` only processes a release if it finds valid entries under ## [Unreleased]. +The `version` input maps to `MANUAL_VERSION` and `reason` to `REASON`; the reason is appended to the changelog footer so the manual bump is recorded. -### How does ReleaseGen determine which version to increment? +## Configuration -`ReleaseGen` scans each `CHANGELOG.md` under the ## [Unreleased] section and looks for specific keywords or headings (e.g., BREAKING CHANGE) to decide whether to bump the major, minor, or patch version. +Every option can be set by environment variable or CLI flag. **Flags override environment variables, which override built-in defaults.** -### Will a major version bump automatically update my go.mod in a Golang project? +| Environment Variable | CLI Flag | Required | Description | +| -------------------- | -------- | :------: | ----------- | +| `GITHUB_TOKEN` | `--token` | ✓ | Token used to push commits/tags and create releases. | +| `GITHUB_REPOSITORY` | `--repository` | ✓ | Repository in `/` form. | +| `GITHUB_ACTOR` | `--actor` | ✓ | User the release commit is attributed to. | +| `GITHUB_REF_NAME` | `--branch` | ✓ | Branch to release from. | +| `MANUAL_VERSION` | `--manual-version` | | Force a specific SemVer instead of computing the bump. Rejected if not valid semver. | +| `REASON` | `--reason` | | Note appended to the changelog footer for a manual release. | +| `EXCLUDE_DIRS` | `--exclude-dirs` | | Newline-separated directories to skip during discovery. | +| `CUSTOM_CHANGE_TYPES` | `--custom-change-types` | | Newline-separated `:` pairs. | +| `REPO_ROOT` | `--repo-root` | | Path to the Git working tree (default `.`). | +| `SUMMARY_FILE` | `--summary-file` | | Write a JSON summary of the run to this path. | +| `DEBUG` | `--debug` | | Verbose tag/discovery diagnostics. | +| — | `--dry-run` | | Compute and print actions without writing anything. | +| — | `--version` | | Print the build version and exit. | -No. You must update your go.mod file manually if you wish to reflect the new major version. +> **Advanced:** `RELEASEGEN_SELF_MODULE` / `RELEASEGEN_SELF_REPO` control the "ReleaseGen releasing itself" case, in which the resolved version is printed to stdout for downstream steps. `RELEASEGEN_SELF_MODULE` is the module path relative to the repo root (defaults to empty, i.e. the root module) and `RELEASEGEN_SELF_REPO` defaults to `c2fo/releasegen`. The feature only triggers when `RELEASEGEN_SELF_REPO` matches the repository being released; set it to empty to disable. You only need these if you fork ReleaseGen. -### Can I exclude certain directories from release generation? +### Exit Codes -Yes. Set the `EXCLUDE_DIRS` environment variable (in YAML, as shown above) to a list of directories you want to skip. +When a run fails, the failure is logged with a `::error::` marker, no further modules are released, and the process exits non-zero with a code that tells you which layer failed: -### What if there are multiple CHANGELOG.md files in different directories? +| Code | Meaning | +| ---- | ------- | +| `0` | Success, or nothing to release. | +| `1` | Configuration error (missing/invalid input). | +| `2` | Changelog validation error (malformed `[Unreleased]`, unknown change type, incomplete `BREAKING CHANGE`). | +| `3` | Git error (push, tag, commit, etc.). | +| `4` | GitHub API error (release creation). | +| `10` | Internal error (a bug — please file an issue). | -`ReleaseGen` will independently process each file. Each directory’s changes result in its own release tag (e.g., `worker/vX.Y.Z`). +Tags or releases written before a mid-run failure are **not** rolled back. Fix the failing module, push a new commit, and rerun. -### Why does the release tag include the directory path in a monorepo? +## Building From Source -Prefixing tags (e.g., `services/api/v1.2.3`) keeps releases organized and prevents collisions in complex repos. A flat naming option may be considered in the future. - -### Can I manually trigger a release from a specific branch? - -Yes. You can use the workflow_dispatch event in GitHub Actions to specify the branch (see above workflow example). `ReleaseGen` will then create a release based on that branch’s `CHANGELOG.md`. +```bash +go build -ldflags "-X main.version=$(git describe --tags --always)" -o release-gen ./cmd/releasegen +./release-gen --help -### What if I want to advance the version to a specific number? +# Preview a release against another checkout without writing anything: +./release-gen --dry-run \ + --repo-root /path/to/your/repo \ + --repository your-org/your-repo \ + --branch main \ + --actor you \ + --token "$GH_TOKEN" +``` -Use the `MANUAL_VERSION` env var (or `--manual-version` flag, or the -`version` input on the manual workflow dispatch) to force a specific -semantic version. `REASON` / `--reason` is appended to the changelog footer -to record why the manual bump was needed. The value must be a valid semver -string; releasegen rejects anything else with exit code 1. +## FAQ -### What if an error occurs during the release process? +**What happens when no tags exist yet?** +ReleaseGen treats the repository as starting at `v0.0.0` and creates the first tag from the entries under `## [Unreleased]`. -The process exits non-zero, the failure is logged with a `::error::` -GitHub Actions marker, and no further modules are released. The exit code -tells you which layer failed: +**What if there are no changes under `## [Unreleased]`?** +No release is created. ReleaseGen only acts when it finds valid entries to promote. -| Code | Meaning | -| ---- | ------------------------------------------- | -| 0 | Success or "nothing to release" | -| 1 | Configuration error (missing/invalid input) | -| 2 | Changelog validation error (malformed `[Unreleased]`, unknown change type, incomplete `BREAKING CHANGE`) | -| 3 | Git error (push, tag, commit, etc.) | -| 4 | GitHub API error (release creation) | -| 10 | Internal error (bug; please file an issue) | +**Will a major bump update my `go.mod` for me?** +No. If a Go major version requires a module path change, you must update `go.mod` yourself. -If the run wrote any tags or releases before failing, those are not rolled -back — fix the failing module, push a new commit, and rerun. +**Can I exclude certain directories?** +Yes — set `EXCLUDE_DIRS` (or `--exclude-dirs`) to the directories you want to skip. -### Can I customize the versioning logic? +**Why does a monorepo tag include the directory path?** +Prefixing tags (e.g. `services/api/v1.2.3`) keeps releases organized and prevents collisions across modules. -By default, `ReleaseGen` follows the Keep a Changelog headings and SemVer rules. You can define additional headings and the bump they trigger via the `CUSTOM_CHANGE_TYPES` environment variable (or the `--custom-change-types` flag) using newline-separated `:` pairs, where `` is `major`, `minor`, or `patch`. For example, to make a `### Documentation` section trigger a minor release: +**Can I trigger a release from a specific branch?** +Yes — use the `workflow_dispatch` trigger shown in the [workflow example](#workflow-example) and pick the branch. -```yaml -CUSTOM_CHANGE_TYPES: | - Documentation:minor -``` +**Can I advance to a specific version?** +Yes — set `MANUAL_VERSION` (or `--manual-version`, or the `version` workflow input). The value must be valid semver or the run exits with code `1`. -See the [Workflow Example](#workflow-example) for how to set this in a GitHub Actions workflow. +**Can I customize the versioning logic?** +Yes — beyond the standard Keep a Changelog headings, define your own with [custom change types](#custom-change-types). -### How can I contribute to ReleaseGen? +## Contributing -We welcome bug reports, feature requests, and pull requests. Feel free to open an issue or submit a PR on our repository. +Bug reports, feature requests, and pull requests are welcome. Please open an issue or submit a PR. ## License -This project is licensed under the MIT License. See the `LICENSE` file for details. +ReleaseGen is licensed under the MIT License. See [`LICENSE.md`](LICENSE.md) for details. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md deleted file mode 100644 index 6c0f3e7..0000000 --- a/docs/ARCHITECTURE.md +++ /dev/null @@ -1,65 +0,0 @@ -# ReleaseGen v2 Architecture - -This document maps each section of [PRD.md](./PRD.md) to the package(s) -that implement it. Use it as a starting point when navigating the codebase. - -## Layout - -``` -releasegen/ -├── cmd/releasegen/ # tiny CLI entrypoint (Cobra) -└── internal/ - ├── config/ # typed Config, env+flag parsing, validation, BumpType - ├── changelog/ # pure parser, classifier, rewriter, Update - ├── logging/ # slog handler with GitHub Actions awareness - ├── vcs/ # Repo interface + go-git implementation (Open, GitRepo) - ├── forge/ # Releaser interface + GitHub implementation - ├── discovery/ # CHANGELOG.md walker, exclude rules, module resolution - └── runner/ # per-module orchestration: discover -> rewrite -> commit/tag/push -> publish -``` - -The CLI in `cmd/releasegen` is the only place that reads environment -variables and constructs concrete types; the rest of the code is wired -through interfaces (`vcs.Repo`, `forge.Releaser`) for testability. - -## PRD section -> package map - -| PRD section | Package(s) implementing it | -| ----------------------------------- | ----------------------------------------------------------------------- | -| §5.1 Discovery | `internal/discovery`, `internal/vcs` (`AllChangelogPaths`, `ReachableTags`, `IsChangelogModifiedSinceTag`) | -| §5.2 Version calculation | `internal/changelog` (`ExtractUnreleased`, `ExtractCurrentVersion`, `Classify`, `NextVersion`) | -| §5.3 Custom change types | `internal/config` (`ParseCustomTypes`, `BumpType`); `internal/changelog` (`Classify`) | -| §5.4 Manual version override | `internal/config` (validation), `internal/changelog/Update` (footer + override), `internal/runner` (wiring) | -| §5.5 Changelog rewrite | `internal/changelog/Rewrite` | -| §5.6 Commit, tag, push | `internal/vcs` (`GitRepo.CommitTagAndPush`) | -| §5.7 GitHub Release publication | `internal/forge` (`GitHubReleaser.CreateRelease`) | -| §5.8 Multi-module runs | `internal/runner` (`Runner.Run`) | -| §5.9 Self-release awareness | `internal/runner` (`Summary.ReleaseGenReleased/Version`); `cmd/releasegen` (prints to stdout) | -| §6 Inputs & configuration | `internal/config` (`FromEnv`, `Validate`); `cmd/releasegen` (flag overrides) | -| §7 Outputs (logs, exit codes) | `internal/logging`; `cmd/releasegen` (`exitCodeFor`) | -| §8 Constraints / guard-rails | `internal/changelog` (`ErrIncompleteBreaking`, `ErrUnrecognizedChangeType`) | -| §9 Failure modes | All packages return wrapped errors; surfaced via slog at ERROR level | - -## Differences from v1 - -- Single `package main` with global env-derived vars -> typed `Config` injected from `cmd/releasegen`. -- `panic`/`recover` for ordinary errors -> returned errors with structured exit codes. -- `c2fo/vfs/v7` and `golang.org/x/oauth2` removed; `os` and - `go-github.WithAuthToken` are sufficient. -- `bumpType string` -> typed `config.BumpType` enum with numeric priority. -- New `--dry-run`, `--summary-file`, `--version`, and per-env-var flag set. -- `internal/vcs` and `internal/forge` are interfaces -> the runner is fully - unit-testable with fakes (see `internal/runner/runner_test.go`). -- Tag/changelog logic now context-aware (`context.Context` propagated end-to-end). - -## Adding a new code-host backend (e.g. GitLab) - -1. Implement `forge.Releaser` for GitLab in a new file under `internal/forge`. -2. Branch on a flag/env in `cmd/releasegen` to construct the right releaser. -3. The runner does not need to change. - -## Adding a new VCS backend - -The `vcs.Repo` interface is small (4 methods) — implement it against your -backend and inject it into `runner.Options`. The runner does not depend on -`go-git` directly. diff --git a/docs/PRD.md b/docs/PRD.md deleted file mode 100644 index 069bd7d..0000000 --- a/docs/PRD.md +++ /dev/null @@ -1,397 +0,0 @@ -# ReleaseGen — Product Requirements Document - -## 1. Overview - -**ReleaseGen** is an automated, changelog-driven release tool. It removes the -human guesswork from versioning and release publication by treating the -project's `CHANGELOG.md` as the single source of truth for *what changed* and -*how significant the change is*. - -When a repository is merged to its release branch, ReleaseGen reads the -`## [Unreleased]` section of every relevant changelog, decides the next -[Semantic Version](https://semver.org/spec/v2.0.0.html) based on the kinds of -entries it finds, rewrites the changelog to "promote" those notes to a numbered -release, commits and tags the result, and publishes a corresponding GitHub -Release whose body is the freshly-cut release notes. - -ReleaseGen is purpose-built for **monorepos**: it understands that a single -repository can host many independently versioned modules, each with its own -`CHANGELOG.md`, its own version history, and its own tag namespace. A change to -one module produces a release for that module only, and never accidentally -implies a release of its siblings. - -ReleaseGen is designed to run unattended inside CI (typically a GitHub Actions -workflow on `push` to `main`, with an optional `workflow_dispatch` escape -hatch). It is opinionated, conventions-first, and deliberately small in scope: -it does not generate code, write changelog entries for the developer, or open -pull requests. It only completes the last mile — turning curated changelog -intent into a real, tagged, published release. - -## 2. Goals & Non-Goals - -### Goals -- Make releases a side effect of merging well-formed changelog entries, not a - separate manual ritual. -- Enforce [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) conventions - so that humans and the tool agree on what each section means. -- Apply SemVer rigorously and predictably: the highest-impact change in the - unreleased section determines the bump. -- Treat each module in a monorepo as an independent release unit, with its own - version line and its own tag. -- Produce releases that are self-describing: tag, GitHub Release, and the - changelog entry all reference the same notes and link back to each other. -- Be safe to run on every merge: do nothing if there is nothing to release; - fail loudly and atomically if something is malformed. -- Provide a controlled override for humans (manual version, manual reason) - without compromising the automated path. - -### Non-Goals -- ReleaseGen does **not** author changelog content. Developers (or their - tooling) must add entries to `## [Unreleased]` themselves. -- ReleaseGen does **not** open or merge pull requests, run tests, build - artifacts, or publish packages to language-specific registries. -- ReleaseGen does **not** rewrite version strings inside source code (e.g. - `go.mod` major version paths, `package.json` version, etc.). That is the - developer's responsibility. -- ReleaseGen does **not** support pre-release identifiers (`-rc.1`, `-beta`) - or build metadata as a first-class workflow. -- ReleaseGen does **not** support release branches other than the branch it is - invoked on; it is not a release-train manager. -- ReleaseGen does **not** delete or amend existing releases or tags. - -## 3. Users & Use Cases - -### Primary users -- **Application and library maintainers** who want a no-friction - "merge to main → cut a release" loop. -- **Monorepo platform owners** who need each module in a shared repository to - be released independently and without coordination. -- **CI/CD engineers** who want a single, auditable Docker step in their - release workflow. - -### Representative use cases -1. **Single-module repository.** A developer adds an `### Added` entry under - `## [Unreleased]` in the root `CHANGELOG.md`, opens a PR, and merges. - ReleaseGen cuts `v1.4.0`, tags it, and publishes a GitHub Release. -2. **Monorepo with many modules.** A PR touches `services/api/` and adds - a `### Fixed` entry in that module's changelog. ReleaseGen produces only - `services/api/v0.2.4`; siblings are untouched. -3. **Coordinated multi-module change.** A PR adds entries in two different - module changelogs simultaneously. ReleaseGen produces two independent - releases in the same run, each with its own version, tag, and GitHub - Release. -4. **Breaking change.** A maintainer documents a `### Changed` item that - includes the literal phrase **"BREAKING CHANGE"**. ReleaseGen bumps the - major version. -5. **Manual override.** An on-call engineer triggers the workflow via - `workflow_dispatch`, supplying an explicit version (e.g. `v2.0.0`) and a - reason. ReleaseGen ignores the calculated bump, uses the supplied version, - and appends the override reason and actor to the release notes. -6. **First-ever release.** A brand-new module has a `CHANGELOG.md` with only - an `## [Unreleased]` section and no prior tags. ReleaseGen treats the - starting point as `0.0.0`, applies the bump, and creates the module's - first tag and release. - -## 4. Core Concepts - -### Changelog as source of truth -The `CHANGELOG.md` file, written in Keep a Changelog format, is the contract -between the developer and ReleaseGen. The developer expresses intent by -choosing a section heading (`### Added`, `### Fixed`, etc.) and writing -human-readable bullets. ReleaseGen interprets that intent. - -### Module -A *module* is the directory in which a `CHANGELOG.md` lives. The module name -is the directory path relative to the repository root. A `CHANGELOG.md` at the -repository root has an empty module name and is treated as the "root module". -Every other changelog defines its own module, identified by its full relative -directory path (e.g. `worker`, `services/api`, `pkg/logger`). - -### Module-scoped tag -Each module has its own independent version history expressed through tags: -- Root module: `vX.Y.Z` (e.g. `v1.2.3`). -- Sub-module: `/vX.Y.Z` (e.g. `services/api/v0.2.0`). - -Tags are the canonical record of what has been released. ReleaseGen reads -existing tags to determine each module's current version and writes a new tag -to record the new one. - -### Unreleased section -Each changelog has exactly one `## [Unreleased]` section. Everything between -that heading and the most recent versioned heading represents work that has -been merged but not yet released. ReleaseGen consumes this section, decides -the next version from its contents, and rewrites the file so that the -unreleased section becomes a versioned section and a new (empty) unreleased -section takes its place at the top. - -### Bump type -The bump type — *major*, *minor*, or *patch* — is computed from the -section headings present in the unreleased block, plus any user-defined -custom change types. The highest-impact match wins. - -## 5. Behavior - -### 5.1 Discovery - -On startup, ReleaseGen scans the working tree of the current commit for every -file named `CHANGELOG.md`. From this set it removes any path under a directory -listed in the `EXCLUDE_DIRS` configuration. For each remaining changelog, it -identifies the module name from the file's directory. - -For each module, ReleaseGen finds the most recent existing tag belonging to -that specific module by reading all tags in the repository, parsing the -module prefix from each tag name, and selecting the newest one (by tagger -date) whose commit is reachable from the branch being released. Tags whose -commits are not reachable from the current branch are ignored. Modules with -no prior tag are treated as if they were at version `0.0.0` and are always -considered eligible for release. - -ReleaseGen then determines whether each changelog has actually been modified -since its module's most recent tag. Changelogs that have not changed are -silently dropped from the run. The remaining changelogs are the candidates -to release. - -### 5.2 Version calculation - -For each candidate changelog, ReleaseGen extracts the contents of the -`## [Unreleased]` section and the most recent prior version recorded in the -file. (If no prior version exists in the file, the starting version is -`0.0.0`.) - -It then classifies the unreleased content: - -- **Major** — A `### Changed` or `### Removed` heading is present *and* the - literal phrase `BREAKING CHANGE` appears somewhere in the unreleased block. -- **Minor** — Any `### Added`, `### Deprecated`, `### Security` heading is - present, or a `### Changed` / `### Removed` heading is present together - with `BREAKING CHANGE` text (already classified as major above), or a - custom change type is configured at minor. -- **Patch** — Only `### Fixed` entries (or custom change types configured at - patch) are present. - -If a `### Changed` or `### Removed` heading is present but `BREAKING CHANGE` -is **not**, ReleaseGen refuses to proceed and surfaces an explicit error -explaining that those headings are reserved for breaking changes and -suggesting an appropriate alternative section. This is a deliberate -guard-rail against accidental major bumps and against misuse of the -sections. - -If the unreleased section exists but contains no recognized change type -(default or custom), ReleaseGen errors out for that module. - -If the unreleased section is empty or absent, ReleaseGen treats the module -as having "no changes detected" and skips it without error. - -The calculated next version is produced by incrementing the appropriate -component of the current version using SemVer rules. - -### 5.3 Custom change types - -Operators may extend the default vocabulary by configuring additional section -names mapped to bump types via `CUSTOM_CHANGE_TYPES`. For example, -`documentation:patch` causes a `### Documentation` section to drive a patch -bump, and `performance:minor` causes a `### Performance` section to drive a -minor bump. - -Custom types follow the same priority rule as built-in types (major beats -minor beats patch), and a custom *major* mapping still requires the -`BREAKING CHANGE` text in the unreleased section before it will actually -produce a major bump. - -### 5.4 Manual version override - -If a `MANUAL_VERSION` is supplied (typically via `workflow_dispatch` input), -ReleaseGen uses it verbatim as the next version, bypassing its calculation. -When a manual version is used, ReleaseGen also appends a footer to the -release notes for that release in the form -`Manual release by : `, where the actor and the reason are -also supplied by the workflow context. This makes manual interventions -permanently visible in the changelog and the GitHub Release. - -A manual version is applied uniformly to every changelog being processed in -the run. - -### 5.5 Changelog rewrite - -For each module being released, ReleaseGen rewrites the changelog file in -place: - -- The existing `## [Unreleased]` heading is preserved at the top, but its - content is moved out of it, leaving it empty for the next cycle. -- A new versioned section is inserted directly below it. The heading takes - the form `## [[]()] - `, where - `` is the module-scoped release identifier (`vX.Y.Z` or - `/vX.Y.Z`), `` deep-links to the GitHub Release that - is about to be created, and the date is the current UTC date. -- The body of the new section is the original unreleased content (plus the - manual-release footer if applicable). - -### 5.6 Commit, tag, push - -For each released module, ReleaseGen stages the rewritten changelog file, -commits it on the current branch with the message -`chore: release version /v () [skip ci]`, and pushes -the commit. The `[skip ci]` marker prevents the release commit from -re-triggering the release workflow. - -It then creates an annotated tag whose name follows the module-scoped tag -convention (`vX.Y.Z` or `/vX.Y.Z`) pointing at the new commit, and -pushes that tag to the remote. The tagger identity is the standard -GitHub Actions bot. - -### 5.7 GitHub Release publication - -After the tag is in place, ReleaseGen calls the GitHub API to create a -Release for that tag. The release name is `[] - `, and -the release body is the unreleased section that was promoted (including the -manual override footer, when applicable). This means the GitHub Release, the -changelog entry, and the tag are all consistent with one another and easy to -navigate between. - -### 5.8 Multi-module runs - -When a single invocation produces releases for several modules, each module is -processed independently and sequentially: discover → bump → rewrite → commit -→ tag → push → publish. Modules that have nothing to release are skipped -with a clear log message. A failure on one module aborts the entire run with -a non-zero exit code; ReleaseGen does not attempt partial recovery or -rollback. - -### 5.9 Self-release awareness - -When ReleaseGen is releasing itself (i.e. running inside the `c2fo/releasegen` -repository and producing a new version of the `releasegen` module), it emits -the new releasegen version to standard output. This is intended to be -captured by the surrounding workflow so that a downstream step (such as a -container image build) can be conditionally executed only when releasegen -itself was bumped. - -## 6. Inputs & Configuration - -ReleaseGen is configured entirely through environment variables. There is no -configuration file. This keeps the tool trivial to run as a CI step. - -### Required (typically supplied by the CI environment) -- `GITHUB_TOKEN` — Token used to push commits/tags and to create the GitHub - Release. Must be authorized to bypass branch protection on the release - branch (a GitHub App token is the supported pattern). -- `GITHUB_REPOSITORY` — `/` identifier of the repository being - released. -- `GITHUB_ACTOR` — User attributed to the run; surfaced in commit messages - and in the manual-release footer. -- `GITHUB_REF_NAME` — The branch being released. Used to determine which - tags are reachable and to push to the right ref. - -### Optional -- `MANUAL_VERSION` — Explicit version string that overrides the calculated - bump for every module released in the run. -- `REASON` — Free-text justification appended (along with the actor) to the - release notes when `MANUAL_VERSION` is set. -- `EXCLUDE_DIRS` — Newline- or comma-separated list of directory prefixes to - exclude from changelog discovery. Useful to keep vendored or third-party - changelogs out of the release pipeline. -- `CUSTOM_CHANGE_TYPES` — Newline-separated list of `:` pairs - that extend the default Keep a Changelog vocabulary. -- `DEBUG` — When set to `true`, emits verbose logs about tag discovery, - module-name extraction, reachability decisions, and which tags were - accepted or skipped. Intended for diagnosing detection problems in - complex monorepos. - -## 7. Outputs - -- **Modified `CHANGELOG.md` files** — Committed back to the release branch. -- **Git commits** — One per released module, on the release branch, marked - with `[skip ci]`. -- **Git tags** — One per released module, annotated, pushed to the remote. -- **GitHub Releases** — One per released module, with notes lifted from the - promoted unreleased section. -- **stderr logs** — Grouped using GitHub Actions `::group::` markers for - readable workflow logs; errors are annotated with `::error::` so they - surface in the Actions UI. -- **stdout** — Empty in the general case; emits the new releasegen version - string when ReleaseGen has just released itself in `c2fo/releasegen`. -- **Exit code** — `0` on success (including the "nothing to release" case); - non-zero on any failure, with the failing error annotated in the logs. - -## 8. Constraints, Conventions, and Guard-Rails - -- **Keep a Changelog format is mandatory.** Section headings drive behavior; - free-form changelogs are not supported. -- **Section heading matching is case-insensitive** but the literal phrase - `BREAKING CHANGE` is matched case-sensitively. This is intentional: the - developer must opt in to a major bump deliberately. -- **One unreleased section per file.** ReleaseGen extracts the content - between `## [Unreleased]` and the next versioned section (or end of file - if none exists). -- **Tags are the version oracle.** The version recorded in the changelog - file is informational; the tag history is authoritative for determining - the current version of each module. -- **Reachability matters.** Tags whose commits are not reachable from the - current branch are ignored when determining a module's current version. - This prevents tags from feature branches or abandoned histories from - influencing releases. -- **Atomicity per module, not across modules.** A run that releases three - modules will release them one at a time. A failure mid-run leaves earlier - modules already released and later modules unreleased; the operator must - reconcile by adding a new commit. -- **No version downgrades.** ReleaseGen only ever increments. Manual - override does not validate that the supplied version is greater than the - current version; the operator is trusted. -- **`go.mod` is not touched.** A major bump for a Go module does not modify - the module path. Maintainers are expected to handle major-version - migrations manually. - -## 9. Failure Modes - -ReleaseGen aims to fail loudly and clearly. Notable failure conditions: - -- **Malformed unreleased section** — A `### Changed` or `### Removed` heading - without a `BREAKING CHANGE` marker. ReleaseGen errors out and explains the - rule. No release is created. -- **Unrecognized change type** — Unreleased section contains content that - matches no built-in or custom heading. ReleaseGen errors out. -- **Unparseable current version** — Existing tag/version cannot be parsed as - SemVer. ReleaseGen errors out. -- **Git push or tag failure** — Authentication problem, branch protection - not bypassed, or network failure. ReleaseGen errors out; any earlier - modules in the same run that already succeeded remain released. -- **GitHub Release API failure** — The tag exists but the Release call - failed. ReleaseGen errors out; the operator may need to delete the tag - before retrying or create the Release manually. -- **Empty unreleased section** — *Not* an error. The module is silently - skipped. - -## 10. Distribution and Execution - -ReleaseGen is distributed as a single binary packaged in a Docker image. The -expected invocation is from a GitHub Actions workflow that: - -1. Generates a short-lived GitHub App token authorized to bypass branch - protection on the release branch. -2. Checks out the repository at the chosen branch with full history - (`fetch-depth: 0`) so all tags are available. -3. Runs the ReleaseGen container, mounting the workspace and passing the - environment variables described in §6. - -A manual `workflow_dispatch` entry point is supported, allowing on-call -operators to specify a branch, an explicit version, and a reason for -auditability. - -## 11. Future Considerations - -The current design is intentionally narrow. Several extensions have been -identified but are out of scope for the present product: - -- **Flat tag naming.** Optionally drop the module-path prefix from tags in - monorepos where teams prefer a flat namespace. -- **Pre-release versions.** First-class support for `-rc`, `-beta`, etc. -- **Authoritative manual version targeting.** A first-class way to fast- - forward a module to a specific version without going through the - `MANUAL_VERSION` override path. -- **Automatic source updates** for files that need to track the version - (e.g. `go.mod` major path, `package.json`, embedded version constants). -- **Pull-request-based releases.** Open a release PR rather than committing - directly to the release branch, for repositories whose policies forbid - bot commits even with branch-protection bypass. -- **Richer release notes.** Optional inclusion of contributor lists, PR - links, or auto-categorization beyond what the changelog already states. diff --git a/internal/config/config.go b/internal/config/config.go index 25dabfd..53e36fc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -37,10 +37,13 @@ type Config struct { SummaryFile string // SelfReleaseModule and SelfReleaseRepo together identify the - // "releasegen releasing itself" case: when a module with this name - // is released inside this repository, the resulting version is - // printed to stdout for downstream workflow steps to consume. - // Both must be non-empty for the feature to be active. + // "releasegen releasing itself" case: when the module with this name is + // released inside SelfReleaseRepo, the resulting version is printed to + // stdout for downstream workflow steps to consume. SelfReleaseModule is + // the module's directory path relative to the repo root; an empty value + // means the root module (releasegen lives at the repository root). The + // feature is active whenever SelfReleaseRepo is non-empty and matches + // the repository being released; set SelfReleaseRepo to "" to disable. SelfReleaseModule string SelfReleaseRepo string } @@ -102,18 +105,20 @@ func FromEnv() (*Config, error) { return nil, err } return &Config{ - Token: os.Getenv("GITHUB_TOKEN"), - OwnerRepo: os.Getenv("GITHUB_REPOSITORY"), - Actor: os.Getenv("GITHUB_ACTOR"), - Branch: os.Getenv("GITHUB_REF_NAME"), - ManualVersion: os.Getenv("MANUAL_VERSION"), - Reason: os.Getenv("REASON"), - ExcludeDirs: ParseExcludeDirs(os.Getenv("EXCLUDE_DIRS")), - CustomTypes: customTypes, - Debug: strings.EqualFold(os.Getenv("DEBUG"), "true"), - RepoRoot: envOr("REPO_ROOT", "."), - SummaryFile: os.Getenv("SUMMARY_FILE"), - SelfReleaseModule: envOr("RELEASEGEN_SELF_MODULE", "releasegen"), + Token: os.Getenv("GITHUB_TOKEN"), + OwnerRepo: os.Getenv("GITHUB_REPOSITORY"), + Actor: os.Getenv("GITHUB_ACTOR"), + Branch: os.Getenv("GITHUB_REF_NAME"), + ManualVersion: os.Getenv("MANUAL_VERSION"), + Reason: os.Getenv("REASON"), + ExcludeDirs: ParseExcludeDirs(os.Getenv("EXCLUDE_DIRS")), + CustomTypes: customTypes, + Debug: strings.EqualFold(os.Getenv("DEBUG"), "true"), + RepoRoot: envOr("REPO_ROOT", "."), + SummaryFile: os.Getenv("SUMMARY_FILE"), + // Empty default: releasegen lives at the root of c2fo/releasegen, so + // the root module (module name "") is the self-release module. + SelfReleaseModule: os.Getenv("RELEASEGEN_SELF_MODULE"), SelfReleaseRepo: envOr("RELEASEGEN_SELF_REPO", "c2fo/releasegen"), }, nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 8ecdde4..d9a40aa 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -129,7 +129,7 @@ func (s *ConfigTestSuite) TestFromEnv_Defaults() { s.Require().NoError(err) s.Equal(".", cfg.RepoRoot) s.Empty(cfg.SummaryFile) - s.Equal("releasegen", cfg.SelfReleaseModule) + s.Empty(cfg.SelfReleaseModule) s.Equal("c2fo/releasegen", cfg.SelfReleaseRepo) } diff --git a/internal/runner/runner.go b/internal/runner/runner.go index b286ac1..4dffb91 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -250,7 +250,7 @@ func (r *Runner) fail(res ModuleResult, err error) (ModuleResult, error) { // isSelfRelease reports whether the named module is the releasegen module // inside the configured self-release repository. func (r *Runner) isSelfRelease(module string) bool { - if r.cfg.SelfReleaseModule == "" || r.cfg.SelfReleaseRepo == "" { + if r.cfg.SelfReleaseRepo == "" { return false } return module == r.cfg.SelfReleaseModule && diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index 6caf085..4e401d8 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -45,7 +45,7 @@ func (s *RunnerTestSuite) SetupTest() { Actor: "tester", Branch: "main", RepoRoot: s.tmpDir, - SelfReleaseModule: "releasegen", + SelfReleaseModule: "", // releasegen is the root module SelfReleaseRepo: "c2fo/releasegen", } } @@ -208,11 +208,13 @@ func (s *RunnerTestSuite) TestSummaryFileWritten() { } func (s *RunnerTestSuite) TestReleaseGenSelfReleaseTracked() { + // releasegen lives at the root of c2fo/releasegen, so the root module + // (module name "") releasing itself must be tracked as a self-release. s.cfg.OwnerRepo = "c2fo/releasegen" - s.stageChangelog("releasegen/CHANGELOG.md", `## [Unreleased] + s.stageChangelog("CHANGELOG.md", `## [Unreleased] ### Added - x -## [releasegen/v1.0.0] - 2024-01-01 +## [v1.0.0] - 2024-01-01 `) s.repo.EXPECT().CommitTagAndPush(mock.Anything, mock.Anything).Return(nil).Once() s.releaser.EXPECT().CreateRelease(mock.Anything, mock.Anything).Return(nil).Once() @@ -222,3 +224,21 @@ func (s *RunnerTestSuite) TestReleaseGenSelfReleaseTracked() { s.True(sum.ReleaseGenReleased) s.Equal("1.1.0", sum.ReleaseGenVersion) } + +func (s *RunnerTestSuite) TestSelfReleaseNotTrackedInOtherRepo() { + // The same root-module release in a different repository must not be + // treated as a releasegen self-release. + s.cfg.OwnerRepo = "acme/widgets" + s.stageChangelog("CHANGELOG.md", `## [Unreleased] +### Added +- x +## [v1.0.0] - 2024-01-01 +`) + s.repo.EXPECT().CommitTagAndPush(mock.Anything, mock.Anything).Return(nil).Once() + s.releaser.EXPECT().CreateRelease(mock.Anything, mock.Anything).Return(nil).Once() + + sum, err := s.newRunner().Run(context.Background()) + s.Require().NoError(err) + s.False(sum.ReleaseGenReleased) + s.Empty(sum.ReleaseGenVersion) +}