diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9456e8f0..a12313b8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,11 +2,10 @@ --- -If this merge represents a feature addition to ROSS, the following items must be completed before the branch will be merged: +## Checklist -- [ ] Document the feature on the blog (See the [website Contributing guide](https://github.com/ROSS-org/ross-org.github.io/blob/master/CONTRIBUTING.md)). - Include a link to your blog post in the Pull Request. -- [ ] Builds should cleanly compile with -Wall and -Wextra. -- [ ] One or more TravisCI tests should be created (and they should pass) -- [ ] Through the TravisCI tests, coverage should increase -- [ ] Test with CODES to ensure everything continues to work +- [ ] Builds cleanly with `-Wall` and `-Wextra` +- [ ] CI is green +- [ ] Added a changelog fragment under [`Documentation/dev/`](Documentation/dev/README.md), unless the change is invisible to anyone outside the PR (test refactors, internal renames, comment-only tweaks) +- [ ] Confirmed nothing in CODES breaks. Build CODES against this branch's installed ROSS +- [ ] For new features: blog post on the [ROSS website](https://github.com/ROSS-org/ross-org.github.io/blob/master/CONTRIBUTING.md), with link in this PR diff --git a/.version b/.version new file mode 100644 index 00000000..0e791524 --- /dev/null +++ b/.version @@ -0,0 +1 @@ +8.1.1 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..dac5654d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +Notable changes per ROSS release. Pre-changelog history (v8.1.1 and earlier) +lives in the [GitHub Releases](https://github.com/ROSS-org/ROSS/releases) page. + +This file is compiled at release time from per-PR fragments in +[`Documentation/dev/`](Documentation/dev/) by +[`scripts/compile-changelog.sh`](scripts/compile-changelog.sh). See the +[release process](Documentation/RELEASE_PROCESS.md) for how it ties together. + +Entries that affect consumers in a non-backwards-compatible way are prefixed +with **[Breaking]**. ROSS follows [Semantic Versioning](https://semver.org/); +breaking changes only ship in MAJOR releases. diff --git a/CMakeLists.txt b/CMakeLists.txt index d67734fc..ef98e484 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,16 +1,61 @@ -PROJECT(ROSS_TOP C) -CMAKE_MINIMUM_REQUIRED(VERSION 3.5) +cmake_minimum_required(VERSION 3.16) + +# Canonical version lives in .version at the repo root — bumped at release +# time alongside the matching git tag (e.g., .version = "8.1.1", tag v8.1.1). +# Tarball, shallow-clone, and no-tag builds all get the right answer from +# this file. git-describe is only consulted to enrich the runtime +# ROSS_VERSION string with a commit-count + sha + -dirty suffix on developer +# builds; it is never authoritative. +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/.version" ROSS_DETECTED_VERSION) +string(STRIP "${ROSS_DETECTED_VERSION}" ROSS_DETECTED_VERSION) +if(NOT ROSS_DETECTED_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+$") + message(FATAL_ERROR + "Malformed version in .version: '${ROSS_DETECTED_VERSION}'. " + "Expected semantic version X.Y.Z (no leading 'v').") +endif() -SET(CMAKE_POSITION_INDEPENDENT_CODE ON) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/core/cmake/") +include(GetGitRevisionDescription) +git_describe_working_tree(ROSS_GIT_DESCRIBE --tags --dirty) +if(ROSS_GIT_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+(.*)$") + set(VERSION "${ROSS_DETECTED_VERSION}${CMAKE_MATCH_1}") +else() + set(VERSION "${ROSS_DETECTED_VERSION}") +endif() +message(STATUS "ROSS version: ${VERSION}") + +project(ross + VERSION ${ROSS_DETECTED_VERSION} + DESCRIPTION "Rensselaer's Optimistic Simulation System" + HOMEPAGE_URL "https://github.com/ROSS-org/ROSS" + LANGUAGES C) + +# Polyfill PROJECT_IS_TOP_LEVEL for CMake < 3.21. Drop this block when the +# minimum required CMake version is bumped to 3.21+ — the variable is then +# set by project() automatically. +if(NOT DEFINED PROJECT_IS_TOP_LEVEL) + if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(PROJECT_IS_TOP_LEVEL TRUE) + else() + set(PROJECT_IS_TOP_LEVEL FALSE) + endif() +endif() + +# Suffix after MAJOR.MINOR.PATCH (e.g., "-18-g90cb62f4-dirty") preserved as +# VERSION_SHA1 for legacy compatibility. +string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.[0-9]+(.*)" "\\1" VERSION_SHA1 "${VERSION}") +# VERSION_SHORT consumed by core/ross.pc.in; In the future it will switch to +# @PROJECT_VERSION@. +set(VERSION_SHORT "${PROJECT_VERSION}") + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) # ROSS Configuration Options ENABLE_TESTING() INCLUDE(CTest) -LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/core/cmake/") - -# Follow section based on Spack doc: +# Follow section based on Spack doc: # https://spack.readthedocs.io/en/latest/workflows.html#write-the-cmake-build # enable @rpath in the install name for any shared library being built # note: it is planned that a future version of CMake will enable this by default diff --git a/Documentation/RELEASE_PROCESS.md b/Documentation/RELEASE_PROCESS.md new file mode 100644 index 00000000..811e4b95 --- /dev/null +++ b/Documentation/RELEASE_PROCESS.md @@ -0,0 +1,165 @@ +# ROSS release process + +This document captures the release process for ROSS. There is no separate release manager or +formal release branch in active use. Releases happen by tagging `master`. + +## Versioning + +ROSS follows [Semantic Versioning](https://semver.org/) (adopted at v7.0.0). +Tags use a `v` prefix; the `.version` file does not. + +| Bump | When | Example | +|-------|------------------------------------------------------------|-----------------| +| MAJOR | Breaking changes to the model-facing API or wire/checkpoint format | `v8.0.0` | +| MINOR | New features, backwards compatible | `v8.1.0` | +| PATCH | Bug fixes only, no API changes | `v8.1.1` | + +## What ships in a release + +- A signed-off commit on `master` +- A git tag of the form `vX.Y.Z` pointing at that commit +- A new section in [`CHANGELOG.md`](../CHANGELOG.md), compiled from + per-PR fragments under [`Documentation/dev/`](dev/) +- A GitHub Release attached to the tag, body lifted from the new + CHANGELOG section (optionally lightly edited for narrative) +- GitHub's auto-generated source tarball/zip for the tag (no extra + build artifacts are uploaded today) + +There is no published binary and no Doxygen auto-publish today (though there +are plans to add auto-publish for Doxygen docs). + +## Changelog fragments + +Every user-visible PR adds a fragment file under +[`Documentation/dev/`](dev/) named +`..md`. Categories are `feature`, `bugfix`, +`removal`, `build`, and `misc`. Breaking changes are marked inline with +a `**[Breaking]**` prefix. See [Documentation/dev/README.md](dev/README.md) +for the full contributor-facing format. + +At release time, [`scripts/compile-changelog.sh`](../scripts/compile-changelog.sh) +concatenates the pending fragments into a new section at the top of +`CHANGELOG.md` and `git rm`s the fragments. This is what step 2 of +"Cutting a release" below uses. + +## The `.version` file + +`.version` at the repo root is the canonical source of truth for the +project version. It contains a single line, `X.Y.Z`, with no `v` prefix and +no trailing whitespace. + +The top-level `CMakeLists.txt` reads `.version` before calling `project()` +and uses it for: + +- `project(ross VERSION X.Y.Z ...)` — and thus `${ross_VERSION}`, + `${PROJECT_VERSION}`, etc. +- The `Version:` field in the installed `ross.pc` +- The `ROSS_VERSION` macro embedded into `config.h` + +If the working tree is a git checkout, `git describe --tags --dirty` is +also consulted to enrich `ROSS_VERSION` with a commit-count + sha + +`-dirty` suffix on developer builds (e.g., `8.1.1-18-g90cb62f4-dirty`). +Tarball / shallow / no-tag builds simply skip the suffix and report the +plain `X.Y.Z`. Git is never authoritative for the version. + +A malformed `.version` (missing, empty, or not matching `^[0-9]+\.[0-9]+\.[0-9]+$`) +fails configure with a `FATAL_ERROR` — there is no silent fallback. + +## Cutting a release + +`master` is a protected branch — all changes, including the version bump, +land via pull request with CI green. Direct pushes are not allowed. + +1. **Make sure `master` is in the desired state.** All PRs intended for + the release are merged and CI is green on the latest `master` commit. + +2. **Open a release PR that bumps `.version` and compiles the changelog:** + + ```bash + git checkout -b release-8.2.0 + echo "8.2.0" > .version + ./scripts/compile-changelog.sh 8.2.0 + git add .version # compile-changelog.sh already staged CHANGELOG.md + # and git-rm'd the fragments + git commit -m "Release v8.2.0" + git push -u origin release-8.2.0 + ``` + + Open the PR against `master`, wait for CI to pass, get a review, and + merge. The resulting commit on `master` is what gets tagged in the + next step. + + Review the compiled `CHANGELOG.md` diff before pushing — the script + produces a usable first draft, but it's worth a copy-edit pass for + prose flow, especially if many fragments landed in the cycle. The + GitHub Release body in step 5 can lift the new section verbatim or + trim further. + +3. **Tag the merged bump commit on `master`:** + + ```bash + git checkout master + git pull --ff-only origin master + git tag -a v8.2.0 -m "ROSS v8.2.0" + git push origin v8.2.0 + ``` + + Pulling first ensures the tag lands on the merge/squash commit GitHub + produced, not on a stale local `master`. Branch protection does not + apply to tag pushes, so `git push origin v8.2.0` succeeds directly. + + Annotated tags (`-a`) are preferred — they carry the tagger identity + and date and survive `git describe` more reliably than lightweight + tags. Historically most ROSS tags are lightweight; only `v8.1.0` is + annotated. Either works. + +4. **Verify the version flows correctly** by building from a clean + checkout at the tag: + + ```bash + git checkout v8.2.0 + cmake -S . -B build -DROSS_BUILD_MODELS=ON -DCMAKE_BUILD_TYPE=Debug + cmake --build build -j + # Confirm "ROSS version: 8.2.0" in the configure log (no -dirty suffix + # on a clean checkout at the tag). + ``` + +5. **Create the GitHub Release.** Open + https://github.com/ROSS-org/ROSS/releases/new, choose the tag, and + copy the new section from `CHANGELOG.md` into the release body. Add + a one-sentence summary at the top if the changelog content alone + feels too dry. The release body and CHANGELOG.md should say + substantively the same thing. + + For pre-changelog releases (v8.1.1 and earlier), GitHub Releases + were written from scratch with varying detail. + +## After the release + +- **Notify downstream consumers** if there are breaking changes. The + primary consumer is [CODES](https://github.com/codes-org/codes), which + discovers ROSS via `pkg_check_modules(ROSS REQUIRED IMPORTED_TARGET ross)`. + Anything that affects the install layout (header paths, `ross.pc` + contents, library filename) is a coordination point. + +## Historical patterns (no longer in active use) + +- **`release-X.Y.Z` branches:** v7.2.0 and v7.2.1 were prepared on + dedicated `release-X.Y.Z` branches that were then merged into `master` + and tagged. v8.x releases skipped this and tagged `master` directly. + Either pattern is fine; the branch approach is useful when stabilizing + a release while feature work continues on `master`. +- **`develop` branch:** an `origin/develop` branch exists but hasn't + driven a release in several years. The current convention is to merge + feature branches into `master` directly. + +## Quick checklist + +- [ ] `master` CI green +- [ ] Release PR opened: `.version` bumped, `scripts/compile-changelog.sh` + run, CHANGELOG diff reviewed, CI green, reviewed, merged +- [ ] Annotated tag `vX.Y.Z` pushed against the merged commit on `master` +- [ ] Configure log shows the new version (no `-dirty` on the clean tag) +- [ ] GitHub Release created, body lifted from CHANGELOG section +- [ ] CODES / downstream consumers notified if anything in the install + contract changed diff --git a/Documentation/dev/README.md b/Documentation/dev/README.md new file mode 100644 index 00000000..6e87e4c9 --- /dev/null +++ b/Documentation/dev/README.md @@ -0,0 +1,71 @@ +# Changelog fragments + +Pending release notes live here as one file per pull request. At release +time, [`scripts/compile-changelog.sh`](../../scripts/compile-changelog.sh) +concatenates everything in this directory into a new section of the +top-level [`CHANGELOG.md`](../../CHANGELOG.md) and `git rm`s the +fragments. + +## Adding a fragment + +Every user-visible change should add one file to this directory in the +same PR that introduces the change. Skip only for changes that are +genuinely invisible to anyone outside the PR (e.g., test refactors, +internal renames, comment-only tweaks). + +**Filename**: `..md` + +- `` — kebab-case, descriptive of the change, unique within + the directory. No PR number needed. +- `` — one of: + - `feature` — new capability or option + - `bugfix` — fixed incorrect behavior + - `removal` — removed or renamed a public-facing API, option, or path + - `build` — build-system / packaging / CMake / install-layout change + - `misc` — internal cleanup, doc tweaks, anything else worth noting + +**Contents**: one or two sentences in past tense, written for someone +upgrading ROSS. Focus on what changed and (briefly) why. + +If the change breaks consumers — removed symbol, renamed option, changed +default, install-path move — prefix the entry with `**[Breaking]**`. The +marker stays inline with the entry in its natural category section. + +## Examples + +`Documentation/dev/version-file.build.md`: + +```markdown +The project version is now read from a tracked `.version` file at the +repo root, not from `git describe`. Tarball and shallow-clone builds +report the correct version instead of falling back to a sentinel. +``` + +`Documentation/dev/mpi-discovery.build.md`: + +```markdown +**[Breaking]** MPI is now discovered via `find_package(MPI)` rather +than the legacy `SetupMPI.cmake` helper. Users no longer need to set +`CC=mpicc` or `-DCMAKE_C_COMPILER=mpicc`; non-standard MPI installs are +hinted with `-DMPI_HOME=...` or `module load `. +``` + +`Documentation/dev/coveralls-removal.removal.md`: + +```markdown +Removed the dead Coveralls coverage path (`-DCOVERALLS=ON`, +`core/cmake/Coveralls*.cmake`). Coverage is now driven by +`-DROSS_ENABLE_COVERAGE=ON` plus the Codecov GitHub Actions job. +``` + +## Conventions + +- One concept per fragment. Two unrelated changes from the same PR + should produce two fragments. +- Refer to options, flags, and identifiers in backticks. +- Don't reference PR numbers, issue numbers, or contributor handles + inside the fragment — those belong in the GitHub release notes the + maintainer writes from this output. Fragments should still read + cleanly five years from now. +- Don't quote configure-log output, full error messages, or large code + blocks. Keep the fragment to a paragraph. diff --git a/Documentation/dev/cmake-minimum-3.16.build.md b/Documentation/dev/cmake-minimum-3.16.build.md new file mode 100644 index 00000000..e7a7ff06 --- /dev/null +++ b/Documentation/dev/cmake-minimum-3.16.build.md @@ -0,0 +1,4 @@ +**[Breaking]** Bumped `cmake_minimum_required` from 3.5 to 3.16. Older +CMake versions can no longer configure ROSS. CMake 3.16 (released +November 2019) is needed for the modern target/install/export +machinery used in the ongoing CMake modernization. diff --git a/Documentation/dev/coverage-modernized.build.md b/Documentation/dev/coverage-modernized.build.md new file mode 100644 index 00000000..9e0903d3 --- /dev/null +++ b/Documentation/dev/coverage-modernized.build.md @@ -0,0 +1,5 @@ +Coverage tracking moved from the dead Coveralls path to Codecov. The +`-DCOVERALLS=ON` option and the `Coveralls*.cmake` helpers under +`core/cmake/` are gone; a new `-DROSS_ENABLE_COVERAGE=ON` opt-in adds +`--coverage` to the build, and a dedicated coverage job in the GitHub +Actions workflow uploads results to Codecov. diff --git a/Documentation/dev/gha-ci.build.md b/Documentation/dev/gha-ci.build.md new file mode 100644 index 00000000..209e3420 --- /dev/null +++ b/Documentation/dev/gha-ci.build.md @@ -0,0 +1,3 @@ +Replaced the long-defunct Travis CI configuration with a GitHub +Actions workflow (`.github/workflows/build.yml`) that builds and tests +ROSS on Ubuntu against MPICH on every push and pull request. diff --git a/Documentation/dev/release-process.misc.md b/Documentation/dev/release-process.misc.md new file mode 100644 index 00000000..f021cefd --- /dev/null +++ b/Documentation/dev/release-process.misc.md @@ -0,0 +1,7 @@ +Added a release-process workflow built around per-PR changelog +fragments under `Documentation/dev/`. Maintainers compile them into +`CHANGELOG.md` at release time via `scripts/compile-changelog.sh`. See +[Documentation/RELEASE_PROCESS.md](Documentation/RELEASE_PROCESS.md) +for the maintainer side and +[Documentation/dev/README.md](Documentation/dev/README.md) for the +contributor format. diff --git a/Documentation/dev/remove-damaris.removal.md b/Documentation/dev/remove-damaris.removal.md new file mode 100644 index 00000000..7188d538 --- /dev/null +++ b/Documentation/dev/remove-damaris.removal.md @@ -0,0 +1,5 @@ +**[Breaking]** Removed the `USE_DAMARIS` CMake option and its dependent build paths. +The Damaris/RISA in-situ visualization has been inert for years and is +slated for a future rewrite; the RISA submodule and the +`#cmakedefine USE_DAMARIS` site in `config.h.in` remain pending that +work. diff --git a/Documentation/dev/remove-ross-config.removal.md b/Documentation/dev/remove-ross-config.removal.md new file mode 100644 index 00000000..5c44006f --- /dev/null +++ b/Documentation/dev/remove-ross-config.removal.md @@ -0,0 +1,5 @@ +**[Breaking]** Removed the `ross-config` shell-script wrapper from the install tree +(`/bin/ross-config`). Its `--cflags` / `--libs` / `--cc` / +`--ld` queries are redundant with `pkg-config ross` (which ROSS has +always shipped), and CODES was verified not to depend on it before +removal. diff --git a/Documentation/dev/sys-time-include.bugfix.md b/Documentation/dev/sys-time-include.bugfix.md new file mode 100644 index 00000000..38e72a01 --- /dev/null +++ b/Documentation/dev/sys-time-include.bugfix.md @@ -0,0 +1,2 @@ +Added a missing `#include ` that was previously masked by +implicit includes on some platforms. diff --git a/Documentation/dev/version-file.build.md b/Documentation/dev/version-file.build.md new file mode 100644 index 00000000..bdb8a0d6 --- /dev/null +++ b/Documentation/dev/version-file.build.md @@ -0,0 +1,6 @@ +The project version is now read from a tracked `.version` file at the +repo root rather than solely from `git describe`. Tarball, +shallow-clone, and no-tag builds report the correct release version +instead of falling back to a sentinel; `git describe` still augments +the runtime `ROSS_VERSION` string with a commit-count and `-dirty` +suffix on developer builds. diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 3f94c900..dbcec0f2 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -80,28 +80,6 @@ check-revent/crv-state.c INCLUDE (CheckFunctionExists) CHECK_FUNCTION_EXISTS(ctime HAVE_CTIME) -## Print ROSS Git Hash -# From http://stackoverflow.com/questions/1435953/how-can-i-pass-git-sha1-to-compiler-as-definition-using-cmake -# Now following this approach (which is based on the previous): -# http://ipenguin.ws/2012/11/cmake-automatically-use-git-tags-as.html -# This way lets us use the actual version numbers of ROSS, instead of the git commit -LIST(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/") -INCLUDE(GetGitRevisionDescription) -#GET_GIT_HEAD_REVISION(GIT_REFSPEC GIT_SHA1) - -# changed to look at the working tree and not the latest commit hash -- allows for use of --dirty. -git_describe_working_tree(VERSION --tags --dirty) -message(STATUS "ROSS VERSION=${VERSION}") - -#parse the version information into pieces. -string(REGEX REPLACE "^v([0-9]+)\\..*" "\\1" VERSION_MAJOR "${VERSION}") -string(REGEX REPLACE "^v[0-9]+\\.([0-9]+).*" "\\1" VERSION_MINOR "${VERSION}") -string(REGEX REPLACE "^v[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" VERSION_PATCH "${VERSION}") -string(REGEX REPLACE "^v[0-9]+\\.[0-9]+\\.[0-9]+(.*)" "\\1" VERSION_SHA1 "${VERSION}") -# VERSION_SHORT used in the ross.pc file -set(VERSION_SHORT "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") - - # Data Structure for remote Events # If AVL_TREE is OFF, ROSS reverts to hashing OPTION(AVL_TREE "Use AVL trees for optimistic mode events? (hash tables otherwise)" ON) diff --git a/scripts/compile-changelog.sh b/scripts/compile-changelog.sh new file mode 100755 index 00000000..4c48f811 --- /dev/null +++ b/scripts/compile-changelog.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# +# compile-changelog.sh — compile Documentation/dev/*.md fragments into a new +# CHANGELOG.md section, then git-rm the fragments. Run as part of the release +# PR. See Documentation/dev/README.md for the fragment format. +# +set -euo pipefail + +usage() { + cat < [--date YYYY-MM-DD] [--dry-run] + +Compiles changelog fragments under Documentation/dev/ into a new section +prepended to CHANGELOG.md, then 'git rm's the fragments. + +Arguments: + Semantic version (X.Y.Z) for the new release. + +Options: + --date DATE Override the release date (default: today, UTC). + --dry-run Print the proposed section to stdout without + modifying any files. +EOF +} + +VERSION="" +DATE="$(date -u +%Y-%m-%d)" +DRY_RUN=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=1; shift ;; + --date) DATE="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + --) shift; break ;; + -*) echo "Error: unknown option '$1'" >&2; usage >&2; exit 1 ;; + *) + if [[ -n "$VERSION" ]]; then + echo "Error: unexpected argument '$1'" >&2 + usage >&2 + exit 1 + fi + VERSION="$1" + shift + ;; + esac +done + +if [[ -z "$VERSION" ]]; then + echo "Error: version argument required" >&2 + usage >&2 + exit 1 +fi +if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: version must be X.Y.Z, got '$VERSION'" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +FRAGMENT_DIR="$REPO_ROOT/Documentation/dev" +CHANGELOG="$REPO_ROOT/CHANGELOG.md" + +[[ -d "$FRAGMENT_DIR" ]] || { echo "Error: $FRAGMENT_DIR not found" >&2; exit 1; } +[[ -f "$CHANGELOG" ]] || { echo "Error: $CHANGELOG not found" >&2; exit 1; } + +CATEGORIES=(feature bugfix removal build misc) +HEADERS=("Features" "Bug fixes" "Removals" "Build & packaging" "Misc") + +# Validation pass: warn on fragments whose suffix isn't a recognized category. +unrecognized=() +shopt -s nullglob +for f in "$FRAGMENT_DIR"/*.md; do + base=$(basename "$f") + [[ "$base" == "README.md" ]] && continue + name="${base%.md}" + category="${name##*.}" + valid=0 + for c in "${CATEGORIES[@]}"; do + [[ "$c" == "$category" ]] && { valid=1; break; } + done + [[ $valid -eq 0 ]] && unrecognized+=("$base") +done +shopt -u nullglob + +if [[ ${#unrecognized[@]} -gt 0 ]]; then + echo "Warning: skipping fragments with unrecognized category:" >&2 + for u in "${unrecognized[@]}"; do echo " $u" >&2; done + echo "(Valid categories: ${CATEGORIES[*]})" >&2 +fi + +# Bail if nothing matched any recognized category. +total=0 +shopt -s nullglob +for c in "${CATEGORIES[@]}"; do + for f in "$FRAGMENT_DIR"/*.${c}.md; do + total=$((total+1)) + done +done +shopt -u nullglob +if [[ $total -eq 0 ]]; then + echo "Error: no usable fragments found in $FRAGMENT_DIR" >&2 + exit 1 +fi + +# Render one fragment file as a markdown bullet: first non-empty line gets the +# "- " prefix; subsequent lines are indented by two spaces (markdown bullet +# continuation). Leading and trailing blank lines are trimmed. +fragment_to_bullet() { + awk ' + { lines[++n] = $0 } + END { + start = 0; for (i = 1; i <= n; i++) if (lines[i] != "") { start = i; break } + end = 0; for (i = n; i >= 1; i--) if (lines[i] != "") { end = i; break } + if (start == 0) exit + for (i = start; i <= end; i++) { + if (i == start) printf("- %s\n", lines[i]) + else if (lines[i] == "") printf("\n") + else printf(" %s\n", lines[i]) + } + } + ' "$1" +} + +section=$(mktemp) +out=$(mktemp) +trap 'rm -f "$section" "$out"' EXIT + +{ + printf '## v%s — %s\n\n' "$VERSION" "$DATE" + for i in "${!CATEGORIES[@]}"; do + c="${CATEGORIES[$i]}" + h="${HEADERS[$i]}" + shopt -s nullglob + files=( "$FRAGMENT_DIR"/*.${c}.md ) + shopt -u nullglob + [[ ${#files[@]} -eq 0 ]] && continue + printf '### %s\n\n' "$h" + # Sort by basename so the order is stable across filesystems. + for f in $(printf '%s\n' "${files[@]}" | sort); do + fragment_to_bullet "$f" + done + printf '\n' + done +} > "$section" + +if [[ $DRY_RUN -eq 1 ]]; then + cat "$section" + exit 0 +fi + +# Prepend the new section to CHANGELOG.md, before the first existing '## v' +# section header. If no prior versioned section exists, append at the end with +# a blank-line separator from the intro. +awk -v section_file="$section" ' + BEGIN { inserted = 0 } + !inserted && /^## v/ { + while ((getline line < section_file) > 0) print line + close(section_file) + inserted = 1 + } + { print } + END { + if (!inserted) { + print "" + while ((getline line < section_file) > 0) print line + close(section_file) + } + } +' "$CHANGELOG" > "$out" +mv "$out" "$CHANGELOG" + +# Stage CHANGELOG.md and remove the fragments. Tracked fragments go through +# 'git rm' so the deletion appears in the release PR diff; untracked fragments +# (new in this release cycle) are just deleted from disk. +git add "$CHANGELOG" +for c in "${CATEGORIES[@]}"; do + shopt -s nullglob + for f in "$FRAGMENT_DIR"/*.${c}.md; do + if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then + git rm --quiet "$f" + else + rm -f "$f" + fi + done + shopt -u nullglob +done + +echo "Compiled changelog for v${VERSION}. Review the diff before pushing the release PR."