Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions .github/scripts/prune-maven-repo.py
Original file line number Diff line number Diff line change
@@ -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<count>.<sha>`` where ``<count>`` 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 <repo-root> [--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" <version>{escape(v)}</version>" for v in versions)
latest = versions[-1] if versions else ""
return (
'<?xml version="1.0" encoding="UTF-8"?>\n'
"<metadata>\n"
f" <groupId>{escape(group_id)}</groupId>\n"
f" <artifactId>{escape(artifact_id)}</artifactId>\n"
" <versioning>\n"
f" <latest>{escape(latest)}</latest>\n"
f" <release>{escape(latest)}</release>\n"
" <versions>\n"
f"{version_lines}\n"
" </versions>\n"
f" <lastUpdated>{stamp}</lastUpdated>\n"
" </versioning>\n"
"</metadata>\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())
4 changes: 1 addition & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
name: CI

on:
push:
branches: ["main"]
pull_request:
pull_request: # master pushes are built + published by incrementals.yml

jobs:
build:
Expand Down
65 changes: 65 additions & 0 deletions .github/workflows/incrementals.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: incrementals

# Every commit on master is published as a unique, immutable Maven version
# (0.3.0-rc<commitcount>.<sha>) 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
77 changes: 42 additions & 35 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
<repositories>
<repository>
<id>garnish-gh-pages</id>
<url>https://aneveux.github.io/garnish/</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>

<dependency>
<groupId>me.neveux</groupId>
<artifactId>garnish</artifactId>
<version>${{ steps.ver.outputs.version }}</version>
</dependency>
```

> Only the latest 5 versions are retained on the update site. Pin a current version and bump when you update.

---
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

target/
out/
.flattened-pom.xml

# -----------------------------------------------------------------------------
# 💡 IDE
Expand Down
12 changes: 12 additions & 0 deletions .mvn/extensions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 http://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
<!-- Rewrites ${changelist} to -rc<commitcount>.<sha> when built with -Dset.changelist,
turning every commit into a unique immutable version. Dormant otherwise. -->
<extension>
<groupId>io.jenkins.tools.incrementals</groupId>
<artifactId>git-changelist-maven-extension</artifactId>
<version>1.13</version>
</extension>
</extensions>
1 change: 1 addition & 0 deletions .mvn/maven.config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-Dchangelist.format=-rc%d.%s
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<commitcount>.<sha>`) 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
Expand All @@ -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
2 changes: 1 addition & 1 deletion garnish-demo/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>me.neveux</groupId>
<artifactId>garnish-parent</artifactId>
<version>0.2.0</version>
<version>${revision}${changelist}</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Loading
Loading