diff --git a/.github/scripts/prune-maven-repo.py b/.github/scripts/prune-maven-repo.py new file mode 100644 index 0000000..21ffe1d --- /dev/null +++ b/.github/scripts/prune-maven-repo.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Prune a static Maven repo to a rolling window of the newest N incremental versions. + +Incremental versions look like ``0.3.0-rc.`` where ```` is the +monotonic ``git rev-list --count HEAD`` at build time. For every artifact directory +(the parent of a ``maven-metadata.xml``) this keeps the ``--keep`` newest versions by +that count, deletes the rest, then rewrites ``maven-metadata.xml`` and its checksum +companions so nothing points at a pruned directory. + +Usage: prune-maven-repo.py [--keep N] +""" + +from __future__ import annotations + +import argparse +import hashlib +import re +import shutil +import sys +import time +from pathlib import Path +from xml.sax.saxutils import escape + +VERSION_RE = re.compile(r"-rc(\d+)\.") + + +def incremental_count(version: str) -> int | None: + """Commit count embedded in an incremental version, or None if not incremental.""" + match = VERSION_RE.search(version) + return int(match.group(1)) if match else None + + +def artifact_dirs(root: Path) -> list[Path]: + """Every directory that already holds a maven-metadata.xml (an artifact root).""" + return sorted({meta.parent for meta in root.rglob("maven-metadata.xml")}) + + +def version_dirs(artifact_dir: Path) -> list[Path]: + return [p for p in artifact_dir.iterdir() if p.is_dir()] + + +def write_with_checksums(path: Path, content: str) -> None: + data = content.encode("utf-8") + path.write_bytes(data) + for algo in ("md5", "sha1", "sha256", "sha512"): + digest = hashlib.new(algo, data).hexdigest() + path.with_name(path.name + "." + algo).write_text(digest, encoding="utf-8") + + +def render_metadata(group_id: str, artifact_id: str, versions: list[str], stamp: str) -> str: + version_lines = "\n".join(f" {escape(v)}" for v in versions) + latest = versions[-1] if versions else "" + return ( + '\n' + "\n" + f" {escape(group_id)}\n" + f" {escape(artifact_id)}\n" + " \n" + f" {escape(latest)}\n" + f" {escape(latest)}\n" + " \n" + f"{version_lines}\n" + " \n" + f" {stamp}\n" + " \n" + "\n" + ) + + +def coordinates(root: Path, artifact_dir: Path) -> tuple[str, str]: + """Derive (groupId, artifactId) from the artifact dir's path under the repo root.""" + parts = artifact_dir.relative_to(root).parts + return ".".join(parts[:-1]), parts[-1] + + +def prune_artifact(root: Path, artifact_dir: Path, keep: int, stamp: str) -> None: + incrementals: list[tuple[int, Path]] = [] + others: list[Path] = [] + for vdir in version_dirs(artifact_dir): + count = incremental_count(vdir.name) + (incrementals.append((count, vdir)) if count is not None else others.append(vdir)) + + incrementals.sort(key=lambda item: item[0]) + doomed = incrementals[:-keep] if keep > 0 else [] + for _, vdir in doomed: + shutil.rmtree(vdir) + print(f"pruned {vdir.relative_to(root)}") + + kept = [vdir for _, vdir in incrementals[-keep:]] if keep > 0 else [v for _, v in incrementals] + kept_names = sorted( + [v.name for v in kept] + [v.name for v in others], + key=lambda name: (incremental_count(name) or -1, name), + ) + group_id, artifact_id = coordinates(root, artifact_dir) + metadata = render_metadata(group_id, artifact_id, kept_names, stamp) + write_with_checksums(artifact_dir / "maven-metadata.xml", metadata) + print(f"metadata {artifact_dir.relative_to(root)}: {len(kept_names)} version(s)") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("root", type=Path, help="Maven repo root directory") + parser.add_argument("--keep", type=int, default=5, help="versions to retain (default 5)") + args = parser.parse_args() + + root: Path = args.root.resolve() + if not root.is_dir(): + print(f"error: {root} is not a directory", file=sys.stderr) + return 1 + + stamp = time.strftime("%Y%m%d%H%M%S", time.gmtime()) + dirs = artifact_dirs(root) + if not dirs: + print("no maven-metadata.xml found โ€” nothing to prune") + return 0 + for artifact_dir in dirs: + prune_artifact(root, artifact_dir, args.keep, stamp) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0a7251..66f105f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,7 @@ name: CI on: - push: - branches: ["main"] - pull_request: + pull_request: # master pushes are built + published by incrementals.yml jobs: build: diff --git a/.github/workflows/incrementals.yml b/.github/workflows/incrementals.yml new file mode 100644 index 0000000..e28f9fc --- /dev/null +++ b/.github/workflows/incrementals.yml @@ -0,0 +1,65 @@ +name: incrementals + +# Every commit on master is published as a unique, immutable Maven version +# (0.3.0-rc.) to the static Maven repo served from gh-pages. +on: + push: + branches: [master] + +permissions: + contents: write + +concurrency: + group: incrementals + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 # git-changelist-maven-extension needs the full commit count + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + distribution: oracle + java-version: '25' + cache: maven + + - name: Checkout existing Maven repo from gh-pages + uses: actions/checkout@v4 + continue-on-error: true # first run: gh-pages does not exist yet + with: + ref: gh-pages + path: maven-repo + + - name: Ensure repo dir exists + run: mkdir -p "$GITHUB_WORKSPACE/maven-repo" + + - name: Build, test, and deploy the incremental + run: > + mvn -B -Prelease -Dset.changelist -pl garnish -am clean deploy + -DaltDeploymentRepository=gh-pages::file://$GITHUB_WORKSPACE/maven-repo + + - name: Publish javadoc (latest only) + run: | + rm -rf "$GITHUB_WORKSPACE/maven-repo/apidocs" + mkdir -p "$GITHUB_WORKSPACE/maven-repo/apidocs" + cp -r garnish/target/reports/apidocs/. "$GITHUB_WORKSPACE/maven-repo/apidocs/" \ + || cp -r garnish/target/site/apidocs/. "$GITHUB_WORKSPACE/maven-repo/apidocs/" + touch "$GITHUB_WORKSPACE/maven-repo/.nojekyll" + + - name: Prune to the newest 5 versions + regenerate metadata + run: python3 .github/scripts/prune-maven-repo.py "$GITHUB_WORKSPACE/maven-repo" --keep 5 + + - name: Publish to gh-pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./maven-repo + publish_branch: gh-pages + keep_files: false + force_orphan: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 344f02e..1c45226 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,60 +1,67 @@ name: release +# Tags are human milestone markers. The tagged commit was already published as an +# immutable incremental when it landed on master (see incrementals.yml); this workflow +# just cuts a GitHub Release: auto-generated notes plus a link to the update site and a +# copy-pasteable consumer snippet for the exact version this tag points at. on: push: tags: ['v*'] permissions: contents: write - pages: write - id-token: write jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 # match the incremental version the commit was published under - name: Set up JDK 25 uses: actions/setup-java@v4 with: distribution: oracle java-version: '25' cache: maven - - name: Build, test, and javadoc - run: mvn -B -Prelease verify javadoc:javadoc + - name: Build jars (incremental version) + run: mvn -B -Prelease -Dset.changelist -pl garnish -am clean package + - name: Derive the incremental version + id: ver + run: | + jar=$(ls garnish/target/garnish-*.jar | grep -v -- '-sources.jar' | grep -v -- '-javadoc.jar' | head -1) + version=$(basename "$jar" .jar) + version=${version#garnish-} + echo "version=$version" >> "$GITHUB_OUTPUT" - name: Create GitHub release uses: softprops/action-gh-release@v2 with: generate_release_notes: true + append_body: true files: garnish/target/*.jar + body: | + **Update site:** https://aneveux.github.io/garnish/ ยท [maven-metadata.xml](https://aneveux.github.io/garnish/me/neveux/garnish/maven-metadata.xml) - # Maven Central publishing is deferred until the Tamboui dependency is a - # non-snapshot release (Central rejects SNAPSHOT-resolving artifacts). When it - # is, enable the step below with the Central credentials and GPG key configured. - # - # - name: Publish to Maven Central - # run: mvn -B -Prelease -pl garnish deploy -DskipTests - # env: - # MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} - # MAVEN_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} - # MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - - pages: - needs: release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 25 - uses: actions/setup-java@v4 - with: - distribution: oracle - java-version: '25' - cache: maven - - name: Generate javadoc - run: mvn -B -pl garnish -am javadoc:javadoc -DskipTests - - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v3 - with: - path: garnish/target/site/apidocs - - name: Deploy to GitHub Pages - uses: actions/deploy-pages@v4 + This tag points at the immutable version `${{ steps.ver.outputs.version }}`, published to the token-free Maven repo on GitHub Pages. Add the repository and pin the version: + + ```xml + + + garnish-gh-pages + https://aneveux.github.io/garnish/ + true + false + + + + + me.neveux + garnish + ${{ steps.ver.outputs.version }} + + ``` + + > Only the latest 5 versions are retained on the update site. Pin a current version and bump when you update. + + --- diff --git a/.gitignore b/.gitignore index 8109d04..46a256b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ target/ out/ +.flattened-pom.xml # ----------------------------------------------------------------------------- # ๐Ÿ’ก IDE diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml new file mode 100644 index 0000000..7b132ac --- /dev/null +++ b/.mvn/extensions.xml @@ -0,0 +1,12 @@ + + + + + io.jenkins.tools.incrementals + git-changelist-maven-extension + 1.13 + + diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 0000000..a5ddd15 --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1 @@ +-Dchangelist.format=-rc%d.%s diff --git a/CHANGELOG.md b/CHANGELOG.md index 27fb5f2..5abb9f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to garnish are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed +- **Publishing model** โ€” every commit on `master` is now published as an immutable + incremental version (`0.3.0-rc.`) to a token-free static Maven repo on + GitHub Pages, modelled on Jenkins Incrementals (`git-changelist-maven-extension` + + `flatten-maven-plugin`). Replaces the GitHub Packages target, which required a token even + to read. Only the latest 5 versions are retained. +- Tag pushes (`v*`) now only cut a GitHub Release; they no longer drive Maven publishing. + +## [0.2.0] - 2026-07-15 + +### Added +- Multi-select with toggle-order preservation, preview panes, additional themes, and + configurable icons/borders. + +### Changed +- Extracted `PickerConfig`; simplified the public API surface. +- Renamed the package root and project identity to `me.neveux` / `garnish`. + ## [0.1.0] - 2026-06-29 ### Added @@ -30,4 +50,6 @@ All notable changes to garnish are documented here. The format follows - Maven Central publishing is prepared but deferred until the Tamboui dependency reaches a non-snapshot release. +[Unreleased]: https://github.com/aneveux/garnish/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/aneveux/garnish/releases/tag/v0.2.0 [0.1.0]: https://github.com/aneveux/garnish/releases/tag/v0.1.0 diff --git a/garnish-demo/pom.xml b/garnish-demo/pom.xml index af806f1..033cc27 100644 --- a/garnish-demo/pom.xml +++ b/garnish-demo/pom.xml @@ -7,7 +7,7 @@ me.neveux garnish-parent - 0.2.0 + ${revision}${changelist} ../pom.xml diff --git a/garnish/README.md b/garnish/README.md index 4b1f4af..5cafdae 100644 --- a/garnish/README.md +++ b/garnish/README.md @@ -3,7 +3,8 @@ A personal component library for [Tamboui](https://github.com/tamboui/tamboui), the Java TUI framework. garnish holds the terminal UI components I reuse across CLI projects. -> **Status:** v0.1.0. Published on GitHub Packages. +> **Status:** every commit on `master` is published as an immutable version to a +> token-free Maven repo on GitHub Pages. ## Requirements @@ -15,22 +16,31 @@ framework. garnish holds the terminal UI components I reuse across CLI projects. garnish depends only on `tamboui-toolkit`. You pick a terminal backend in your own application (the demo uses `tamboui-jline3-backend`). +garnish is published to a static Maven repository on GitHub Pages โ€” **no token required**. +Add the repository, then pin an exact version. Every commit on `master` produces a unique, +immutable version like `0.3.0-rc474.9577fe6b8ea6`; browse the current list at +[maven-metadata.xml](https://aneveux.github.io/garnish/me/neveux/garnish/maven-metadata.xml). + ```xml + + + garnish-gh-pages + https://aneveux.github.io/garnish/ + true + false + + + me.neveux garnish - 0.1.0 + 0.3.0-rc474.9577fe6b8ea6 ``` -garnish is published to GitHub Packages. Add the repository to your Maven settings or `pom.xml`: - -```xml - - github-garnish - https://maven.pkg.github.com/aneveux/garnish - -``` +> Only the **latest 5** versions are retained, so treat a pinned version as short-lived โ€” +> bump to a current one when you update. Versions are immutable: a given version always +> maps to the exact commit it was built from. ## Quick start @@ -228,9 +238,27 @@ elements. ## Releasing -The release plumbing (sources/javadoc jars, signing) lives behind the `release` profile. -garnish is published to GitHub Packages. Maven Central is a future option once there is -broader adoption. +Publishing is fully automatic and modelled on [Jenkins Incrementals](https://www.jenkins.io/doc/developer/publishing/incrementals/). +Every push to `master` triggers `.github/workflows/incrementals.yml`, which builds, tests, +and deploys an immutable version to the `gh-pages` branch (served as a static Maven repo). + +- **Version scheme:** `${revision}${changelist}`. A local build resolves to + `0.3.0-SNAPSHOT`. CI passes `-Dset.changelist` and the + [`git-changelist-maven-extension`](https://github.com/jenkins-infra/incrementals-tools) + (in `.mvn/`) rewrites `changelist` to `-rc.` โ€” one unique version per + commit. `flatten-maven-plugin` bakes the resolved version into the deployed POMs. +- **Reproduce a versioned build locally** (clean tree required โ€” the extension refuses to + run on uncommitted changes): + + ```sh + mvn -Dset.changelist -DskipTests clean install + ``` +- **Retention:** `.github/scripts/prune-maven-repo.py` keeps the newest 5 versions and + regenerates `maven-metadata.xml`, bounding the Pages site. +- **Tags** (`v*`) are human milestone markers: `release.yml` cuts a GitHub Release with + auto-generated notes, the matching jars, and a link to the update site plus the consumer + snippet for that version. They don't drive Maven publishing โ€” the tagged commit was already + published when it landed on `master`. ## Contributing diff --git a/garnish/pom.xml b/garnish/pom.xml index 48751fc..cf632f5 100644 --- a/garnish/pom.xml +++ b/garnish/pom.xml @@ -7,7 +7,7 @@ me.neveux garnish-parent - 0.2.0 + ${revision}${changelist} ../pom.xml diff --git a/pom.xml b/pom.xml index f1e9966..3fcde16 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ me.neveux garnish-parent - 0.2.0 + ${revision}${changelist} pom garnish-parent @@ -33,8 +33,8 @@ scm:git:https://github.com/aneveux/garnish.git scm:git:git@github.com:aneveux/garnish.git - https://github.com/aneveux/garnish/tree/main - HEAD + https://github.com/aneveux/garnish/tree/master + ${scmTag} @@ -48,6 +48,13 @@ + + 0.3.0 + -SNAPSHOT + HEAD + 25 25 UTF-8 @@ -70,6 +77,7 @@ 3.12.0 3.6.3 3.8.0 + 1.7.3 @@ -123,6 +131,30 @@ ${maven.compiler.release} + + + org.codehaus.mojo + flatten-maven-plugin + ${flatten-maven-plugin.version} + + true + resolveCiFriendliesOnly + ${project.build.directory} + + + + flatten + process-resources + flatten + + + flatten-clean + clean + clean + + + org.apache.maven.plugins maven-enforcer-plugin @@ -193,17 +225,4 @@ - - - - release - - - github - GitHub Packages - https://maven.pkg.github.com/aneveux/garnish - - - -