Skip to content
Open
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
5 changes: 5 additions & 0 deletions .github/workflows/build-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ jobs:
steps:
- name: Check out
uses: actions/checkout@v5
with:
# git-versioning derives the project version from the current branch;
# the default fetch-depth: 1 leaves HEAD detached without a branch ref
# and the plugin falls back to gradle.properties' static version.
fetch-depth: 0

- name: Setup Java
uses: actions/setup-java@v5
Expand Down
88 changes: 88 additions & 0 deletions .github/workflows/cleanup-gh-packages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Cleanup orphaned GH Packages

# Removes htmlsanitycheck-* SNAPSHOT versions on GitHub Packages whose branch
# no longer exists on origin AND that are older than ORPHAN_RETENTION_DAYS.
# Release versions (anything not ending in -SNAPSHOT) are never touched.

on:
schedule:
- cron: '17 3 * * *' # daily 03:17 UTC
workflow_dispatch:
inputs:
dry-run:
description: 'Log what would be deleted without actually deleting'
type: boolean
default: false

jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
env:
ORG: aim42
ORPHAN_RETENTION_DAYS: 28
DRY_RUN: ${{ inputs.dry-run || 'false' }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Delete SNAPSHOTs of removed branches older than retention threshold
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

# Slugify active remote branches the same way git-versioning does:
# feature/432-add-gh-packages -> feature-432-add-gh-packages
mapfile -t active_slugs < <(
git for-each-ref --format='%(refname:strip=3)' refs/remotes/origin/ \
| grep -v '^HEAD$' \
| tr '/' '-'
)

now_epoch=$(date -u +%s)
retention_seconds=$(( ORPHAN_RETENTION_DAYS * 86400 ))

for pkg in \
htmlsanitycheck-core \
htmlsanitycheck-cli \
htmlsanitycheck-gradle-plugin \
htmlsanitycheck-maven-plugin \
org.aim42.htmlsanitycheck.gradle.plugin
do
echo "::group::${pkg}"
gh api --paginate "/orgs/${ORG}/packages/maven/${pkg}/versions" \
--jq '.[] | [.id, .name, .created_at] | @tsv' \
| while IFS=$'\t' read -r id name created_at; do

# Keep release versions
[[ "${name}" == *-SNAPSHOT ]] || continue

# Slug = name without trailing -SNAPSHOT
slug="${name%-SNAPSHOT}"

# Keep if branch still exists
if printf '%s\n' "${active_slugs[@]}" | grep -Fxq -- "${slug}"; then
continue
fi

# Branch gone — check age
created_epoch=$(date -u -d "${created_at}" +%s)
age=$(( now_epoch - created_epoch ))
if (( age < retention_seconds )); then
echo "::notice::keep (still in grace): ${pkg}@${name} (age $((age/86400))d, branch ${slug} gone)"
continue
fi

if [[ "${DRY_RUN}" == "true" ]]; then
echo "::notice::[DRY RUN] would delete: ${pkg}@${name} (age $((age/86400))d, branch ${slug} gone)"
else
echo "deleting orphan: ${pkg}@${name} (age $((age/86400))d, branch ${slug} gone)"
gh api -X DELETE "/orgs/${ORG}/packages/maven/${pkg}/versions/${id}"
fi
done
echo "::endgroup::"
done
39 changes: 39 additions & 0 deletions .github/workflows/gradle-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,45 @@ jobs:
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

publish-gh-packages:
# Publish per-branch SNAPSHOTs to GitHub Packages so downstream consumers
# can pull a feature/bugfix branch before it merges. Only runs on push
# events to feature/bugfix branches (git-versioning's SNAPSHOT regex);
# develop and main fall through to the static gradle.properties version
# and would conflict with already-published releases.
needs: build-artifacts
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature/') || startsWith(github.ref, 'refs/heads/bugfix/'))
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Check out
uses: actions/checkout@v5
with:
# Needed for git-versioning to derive the branch-slug-SNAPSHOT version.
fetch-depth: 0

- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '17'

- name: Setup Gradle
# Pin to commit SHA per SonarCloud rule githubactions:S7637 (supply-chain hardening).
# Update both the SHA and the comment together when bumping; verify the SHA at
# https://github.com/gradle/actions/releases.
uses: gradle/actions/setup-gradle@48b5f213c81028ace310571dc5ec0fbbca0b2947 # v4.4.3
with:
cache-read-only: true

- name: Publish SNAPSHOT to GitHub Packages
env:
GITHUB_USER: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew publishAllPublicationsToGitHubPackagesRepository --no-daemon

post-build:
needs: build-artifacts
runs-on: ubuntu-latest
Expand Down
16 changes: 14 additions & 2 deletions .github/workflows/test-java-os-mix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ jobs:

- name: Check out
uses: actions/checkout@v5
with:
# Needed for git-versioning to derive the project version from the branch ref.
fetch-depth: 0

- name: Setup JDK
uses: actions/setup-java@v5
Expand All @@ -68,17 +71,26 @@ jobs:
if: runner.os != 'Windows'
run: |
uname -a
# The integration-test subproject reads htmlSanityCheckVersion from gradle.properties
# (static 2.0.0-rcN), but build-artifacts upstream published under git-versioning's
# <branch-slug>-SNAPSHOT. Read the actual published version straight from the maven-repo
# tree we just downloaded — avoids invoking gradle from root here, which would otherwise
# have to resolve every root plugin (sonar, git-versioning, ...) on this matrix worker.
HSC_VERSION="$(basename "$(find build/maven-repo/org/aim42/htmlsanitycheck/htmlsanitycheck-core -maxdepth 1 -mindepth 1 -type d | head -1)")"
echo "Resolved HSC version: ${HSC_VERSION}"
cd integration-test
../gradlew integrationTest --scan ${GRADLE_DEBUG}
../gradlew integrationTest -PhtmlSanityCheckVersion=${HSC_VERSION} --scan ${GRADLE_DEBUG}

- name: Execute integration tests (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
uname -a
$HSC_VERSION = (Get-ChildItem -Directory build/maven-repo/org/aim42/htmlsanitycheck/htmlsanitycheck-core | Select-Object -First 1).Name
Write-Output "Resolved HSC version: $HSC_VERSION"
cd ./integration-test/
pwd
cmd /c "echo off && ..\gradlew.bat integrationTest --scan $env:GRADLE_DEBUG"
cmd /c "echo off && ..\gradlew.bat integrationTest -PhtmlSanityCheckVersion=$HSC_VERSION --scan $env:GRADLE_DEBUG"

- name: Collect state upon failure (On Unix)
if: failure() && runner.os != 'Windows'
Expand Down
38 changes: 36 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,42 @@
## Publication (Generic)

- [Documentation](https://hsc.aim42.org)
- [Maven Central](https://central.sonatype.com/namespace/org.aim42.htmlSanityCheck)
- [Gradle Plugin Portal](https://plugins.gradle.org/search?term=org.aim42.htmlSanityCheck)
- [Maven Central](https://central.sonatype.com/namespace/org.aim42.htmlsanitycheck) (lowercase, see breaking change below)
- [Gradle Plugin Portal](https://plugins.gradle.org/search?term=org.aim42.htmlsanitycheck)

## Unreleased

### ⚠️ BREAKING CHANGE — Lowercase GA coordinates ([#432](https://github.com/aim42/htmlSanityCheck/issues/432))

All Maven `groupId` and `artifactId` values are now **lowercase**, aligning with
Maven conventions and enabling publication to repositories that enforce this
constraint (e.g., GitHub Packages).

**Old → New:**

| Coordinate | Before | After |
|---|---|---|
| Group | `org.aim42.htmlSanityCheck` | `org.aim42.htmlsanitycheck` |
| Core | `htmlSanityCheck-core` | `htmlsanitycheck-core` |
| CLI | `htmlSanityCheck-cli` | `htmlsanitycheck-cli` |
| Gradle plugin id | `org.aim42.htmlSanityCheck` | `org.aim42.htmlsanitycheck` |
| Gradle plugin artifact | `htmlSanityCheck-gradle-plugin` | `htmlsanitycheck-gradle-plugin` |
| Maven plugin artifact | `htmlSanityCheck-maven-plugin` | `htmlsanitycheck-maven-plugin` |

**Action required when upgrading:**

- Gradle users: replace `id 'org.aim42.htmlSanityCheck'` with `id 'org.aim42.htmlsanitycheck'`.
- Maven users: update `groupId` and `artifactId` in the plugin block to the lowercase form.
- Direct dependency users: update group and artifactId coordinates in your build.

Existing released versions (≤ `2.0.0-rc4`) remain available under the old CamelCase coordinates:

- [Maven Central — `org.aim42.htmlSanityCheck`](https://central.sonatype.com/namespace/org.aim42.htmlSanityCheck) (historical artifacts up to and including `2.0.0-rc4`)
- [Gradle Plugin Portal — `org.aim42.htmlSanityCheck`](https://plugins.gradle.org/plugin/org.aim42.htmlSanityCheck) (historical plugin versions)
- [GitHub Releases](https://github.com/aim42/htmlSanityCheck/releases) (CLI binaries)

The internal Gradle project / source directory names (e.g., `htmlSanityCheck-core/`)
are unchanged for readability — only the published Maven coordinates differ.

## 2.0.0-rc4

Expand Down
65 changes: 64 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,35 @@ plugins {
alias(libs.plugins.jreleaser)

id 'com.dorongold.task-tree' version '4.0.1'

id 'me.qoomon.git-versioning' version '6.4.4'
}

allprojects {
group = group
version = htmlSanityCheckVersion

// Derive the version from the current Git ref so feature/bugfix branches publish under their own
// -SNAPSHOT coordinate and don't collide with each other or with release versions on GitHub Packages.
// Tags shaped 'v<version>' produce a release version; any other ref falls back to gradle.properties.
gitVersioning.apply {
refs {
branch('^(feature|bugfix)/.+') {
version = '${ref}-SNAPSHOT'
}
tag('v(?<version>.*)') {
version = '${ref.version}'
}
}
rev {
// Use Groovy interpolation (double quotes) so the gradle.properties value is captured
// at config time. git-versioning only substitutes its own placeholders (${ref},
// ${commit}, ...) and would otherwise pass the single-quoted literal as the version,
// which leaks into the published POM as "${htmlSanityCheckVersion}".
version = "${htmlSanityCheckVersion}"
}
}

repositories {
mavenCentral()
mavenLocal()
Expand All @@ -46,6 +69,13 @@ allprojects {
}
}

// Single-line `./gradlew -q printVersion` returns the git-versioning–derived project.version.
// Used by generate-pages (and any shell caller) to propagate the dynamic version to standalone
// sub-builds like self-check that read htmlSanityCheckVersion from gradle.properties.
tasks.register("printVersion") {
doLast { println project.version }
}

dependencies {
// Add all subprojects to the aggregation
subprojects.forEach {
Expand Down Expand Up @@ -216,6 +246,9 @@ configure(subprojects) {
publishing {
publications.all { publication ->
if (publication instanceof MavenPublication) {
// GA coordinates must be lowercase (Maven convention, enforced by GitHub Packages).
// Each subproject sets base.archivesName explicitly to its lowercase artifactId.
artifactId = project.base.archivesName.get()
publication.pom {
name = project.name
description = project.description
Expand Down Expand Up @@ -261,6 +294,21 @@ configure(subprojects) {
name = 'myLocalRepositoryForFullIntegrationTests'
url = mavenBuildRepo
}
// Only register GitHubPackages when credentials are present. Otherwise Gradle's task-
// configuration validation fails for any publish task in the project with a cryptic
// "credentials.username doesn't have a configured value" — even for unrelated tasks.
// Skipping registration means trying to invoke publish*ToGitHubPackagesRepository
// without env vars yields a clear "task not found" instead.
if (System.getenv("GITHUB_USER") && System.getenv("GITHUB_TOKEN")) {
maven {
name = "GitHubPackages"
url = "https://maven.pkg.github.com/aim42/htmlSanityCheck"
credentials {
username = System.getenv("GITHUB_USER")
password = System.getenv("GITHUB_TOKEN")
}
}
}
mavenLocal()
}
}
Expand All @@ -270,6 +318,17 @@ configure(subprojects) {
outputs.upToDateWhen { false }
}

// GitHub Packages rejects GETs to the plugin marker's snapshot maven-metadata.xml with
// HTTP 400, presumably because the marker artifactId 'org.aim42.htmlsanitycheck.gradle.plugin'
// contains dots that GH's package URL parser treats specially. Skip publishing the marker
// to GitHub Packages; consumers can still resolve it through the Gradle Plugin Portal once
// a release is cut. Local + Maven Central publish targets keep the marker.
tasks.withType(PublishToMavenRepository).configureEach {
if (name.endsWith("PluginMarkerMavenPublicationToGitHubPackagesRepository")) {
enabled = false
}
}

tasks.named('test', Test) {
useJUnitPlatform()
}
Expand Down Expand Up @@ -362,7 +421,11 @@ tasks.register("integrationTestOnly") {
commandLine((System.getProperty("os.name") ==~ /Windows.*/
? "..\\gradlew.bat"
: "../gradlew"),
"integrationTest")
"integrationTest",
// Forward the git-versioning–derived project.version to the child build so it
// can locate the just-published artifact in build/maven-repo.
"-PhtmlSanityCheckVersion=${project.version}",
)
}
logger.debug "Script output: ${result}"
}
Expand Down
5 changes: 4 additions & 1 deletion generate-pages
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ run copyPdf cp -rp build/pdf build/microsite/output
run copyStandalone cp -rp build/html5/images build/microsite/output \
&& mkdir -p build/microsite/output/single-page/ \
&& cp -p build/html5/arc42/hsc_arc42.html build/microsite/output/single-page/hsc_arc42-single-page.html
run htmlSanityCheck "(cd self-check && ../gradlew htmlSanityCheck --scan --refresh-dependencies --scan)"
# Propagate the git-versioning–derived project.version to self-check so it picks up the just-published
# branch-slug-SNAPSHOT (or release version on tags) instead of the static value in gradle.properties.
HSC_VERSION="$(./gradlew -q printVersion)"
run htmlSanityCheck "(cd self-check && ../gradlew -PhtmlSanityCheckVersion=${HSC_VERSION} htmlSanityCheck --scan --refresh-dependencies --scan)"
run copyCheckResult cp -rp build/reports/htmlchecks build/microsite/output
run fixDocLinks "sed -i .bak \
-e 's, href=\"${PWD}/build/microsite/output/, href=\"../,g' \
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
htmlSanityCheckVersion=2.0.0-rc5
# end::version[]

group = org.aim42.htmlSanityCheck
group = org.aim42.htmlsanitycheck
description = HTML Sanity Check

org.gradle.jvmargs=-Xmx2G
Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ junit5-version = '5.12.2'
picocli-version = "4.7.7"
slf4j-version = '2.0.17'
string-similarity-version = '1.0.0'
testcontainers-version = '1.20.6'
testcontainers-version = '1.21.4'
wiremock-testcontainers-version = '1.0-alpha-15'

[libraries]
Expand All @@ -26,7 +26,7 @@ wiremock-testcontainers = { module = 'org.wiremock.integrations.testcontainers:w
gradle-versions = { id= 'com.github.ben-manes.versions', version = '0.52.0' }
sonar = { id = 'org.sonarqube', version = '6.3.1.5724' }
jreleaser = { id = 'org.jreleaser', version = '1.17.0'}
gitProperties = { id = 'com.gorylenko.gradle-git-properties', version = '2.5.3' }
gitProperties = { id = 'com.gorylenko.gradle-git-properties', version = '2.5.4' }

# Copyright Gerd Aschemann and aim42 contributors.
#
Expand Down
2 changes: 2 additions & 0 deletions htmlSanityCheck-cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ plugins {
id 'application'
}

base.archivesName = 'htmlsanitycheck-cli'

dependencies {
implementation libs.picocli.impl
annotationProcessor libs.picocli.annotationprocessor
Expand Down
Loading
Loading