Skip to content
Open
Show file tree
Hide file tree
Changes from 17 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
2 changes: 1 addition & 1 deletion .github/workflows/build-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
uses: gradle/actions/wrapper-validation@v4

- name: Execute Gradle build
run: ./gradlew clean check integrationTest --scan --stacktrace
run: ./gradlew clean check integrationTest build --scan --stacktrace

- name: Cache SonarCloud packages
uses: actions/cache@v4
Expand Down
148 changes: 148 additions & 0 deletions .github/workflows/cleanup-ghcr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: Clean up old GHCR images

on:
schedule:
- cron: '0 2 * * *' # every night at 02:00 UTC
workflow_dispatch:
inputs:
retention_days:
description: 'Delete images older than this many days'
required: false
default: '14'
dry_run:
description: 'If true, only print which versions would be deleted (no deletion performed)'
required: false
type: boolean
default: true

permissions:
contents: read
packages: write

jobs:
cleanup-ghcr:
name: Remove timestamped images older than 14 days
runs-on: ubuntu-latest
if: ${{ github.event_name != 'schedule' || github.ref == 'refs/heads/main' }}
steps:
- name: Clean up old container versions
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const org = 'aim42';
const package_type = 'container';
const package_name = 'hsc';
const per_page = 100;
const tsTagRegex = /^\d{14}$/; // yyyyMMddHHmmss
const sha256Regex = /^[a-f0-9]{64}$/i; // sha256 digest-like tag

const daysInput = (context.payload && context.payload.inputs && context.payload.inputs.retention_days) || '14';
const daysParsed = parseInt(daysInput, 10);
const retentionDays = Number.isFinite(daysParsed) && daysParsed > 0 ? daysParsed : 14;
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000); // retentionDays days ago

const dryRunInput = context.payload && context.payload.inputs ? context.payload.inputs.dry_run : undefined;
const dryRun = (typeof dryRunInput === 'boolean') ? dryRunInput
: (dryRunInput === undefined ? true : String(dryRunInput).toLowerCase() === 'true');

core.info(`Using retention period: ${retentionDays} day(s). Dry-run: ${dryRun}.`);

function parseTimestampTag(tag) {
// tag format: yyyyMMddHHmmss, interpreted as UTC
const y = parseInt(tag.slice(0, 4), 10);
const m = parseInt(tag.slice(4, 6), 10) - 1;
const d = parseInt(tag.slice(6, 8), 10);
const hh = parseInt(tag.slice(8, 10), 10);
const mm = parseInt(tag.slice(10, 12), 10);
const ss = parseInt(tag.slice(12, 14), 10);
return new Date(Date.UTC(y, m, d, hh, mm, ss));
}

let page = 1;
let totalDeleted = 0;
let wouldDelete = 0;
let scanned = 0;

while (true) {
const { data: versions } = await github.request(
'GET /orgs/{org}/packages/{package_type}/{package_name}/versions',
{ org, package_type, package_name, per_page, page }
);

if (!versions || versions.length === 0) break;

for (const v of versions) {
scanned++;
const tags = (v.metadata && v.metadata.container && v.metadata.container.tags) || [];
const createdAt = new Date(v.created_at || v.updated_at || 0);

core.debug (`Checking version '${v.id}' (${v.name}) of '${createdAt}' with tags: '${tags.join(', ')}'`);

// Skip protected tags to avoid removing latest or release tags that share the same version
const isProtected = tags.some(t => t === 'latest' || /^v\d[\.\d]*/.test(t));
Comment thread
ascheman marked this conversation as resolved.
if (isProtected) {
core.info(`Skipping protected version ${v.id} with tags: ${tags.join(', ')}`);
continue;
}

const tsTags = tags.filter(t => tsTagRegex.test(t));
const shaTags = tags.filter(t => sha256Regex.test(t));
const nonShaTags = tags.filter(t => !sha256Regex.test(t));

let shouldDelete = false;

// If any timestamp tag is older than cutoff, delete the entire version
if (tsTags.length > 0) {
shouldDelete = tsTags.some(t => parseTimestampTag(t) < cutoff);
}

// Additionally handle versions that are tagged only by sha256 values.
// Delete if older than retention (by created_at/updated_at) unless there is any non-sha256 tag.
if (!shouldDelete && shaTags.length > 0 && nonShaTags.length === 0) {
if (createdAt instanceof Date && !isNaN(createdAt) && createdAt < cutoff) {
shouldDelete = true;
}
}

// Handle versions with no tags at all: delete if older than retention by created/updated timestamp
if (!shouldDelete && (!tags || tags.length === 0)) {
if (createdAt instanceof Date && !isNaN(createdAt) && createdAt < cutoff) {
shouldDelete = true;
}
}

if (shouldDelete) {
if (dryRun) {
wouldDelete++;
core.info(`[DRY-RUN] Would delete version '${v.id}' (${v.name}) of '${createdAt}' with tags: '${tags.join(', ')}'`);
} else {
try {
await github.request(
'DELETE /orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}',
{
org,
package_type,
package_name,
package_version_id: v.id,
}
);
totalDeleted++;
core.info(`Deleted version '${v.id}' (${v.name}) of '${createdAt}' with tags: ${tags.join(', ')}`);
} catch (err) {
core.warning(`Failed to delete version ${v.id}: ${err.message}`);
}
}
} else {
core.debug (`Not deleting '${v.id}' (${v.name}) of '${createdAt}' with tags: '${tags.join(', ')}'`);
}
}

page++;
}

if (dryRun) {
core.info(`Scanned versions: ${scanned}. Would delete versions: ${wouldDelete}.`);
} else {
core.info(`Scanned versions: ${scanned}. Deleted versions: ${totalDeleted}.`);
}
115 changes: 115 additions & 0 deletions .github/workflows/gradle-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ on:
pull_request:
push:
workflow_dispatch:
inputs:
additional_tags:
description: 'Additional tags for Docker images (comma-separated)'
required: false
type: string

jobs:
build-artifacts:
Expand All @@ -18,6 +23,58 @@ jobs:
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

test-gh-action:
needs: build-artifacts
runs-on: ubuntu-latest

steps:
- name: Check out
uses: actions/checkout@v5

- name: Prepare Docker image for test
run: |
tag=$(git branch --show-current | tr '/' '-')
docker pull ghcr.io/aim42/hsc:${tag}
docker tag ghcr.io/aim42/hsc:${tag} ghcr.io/aim42/hsc:v2

- name: Download Artifacts
uses: actions/download-artifact@v5
with:
name: build-artifacts
path: .

- name: Run GH Action as provided by this repository
uses: ./
with:
args: -r build/gh-action-test-report integration-test/common/build/docs --exclude 'https://www\.baeldung\.com/.*' --fail-on-errors

- name: Upload GH Action test results
uses: actions/upload-artifact@v4
with:
path: build/gh-action-test-report

- name: Test Result of GH Action
run: |
if test -s build/gh-action-test-report/index.html; then
echo "Test Successful"
else
echo "Test Failed: Report not found" >&2
exit 1
fi

- name: Collect state upon failure
if: failure()
run: |
echo "Git:"
git status
echo "Env:"
env | sort
echo "PWD:"
pwd
echo "Files:"
find * -ls
./gradlew javaToolchains

post-build:
needs: build-artifacts
runs-on: ubuntu-latest
Expand Down Expand Up @@ -49,6 +106,64 @@ jobs:
- name: Collect state upon failure
if: failure()
run: |
echo "Git:"
git status
echo "Env:"
env | sort
echo "PWD:"
pwd
echo "Files:"
find * -ls
./gradlew javaToolchains

publish-docker-images:
needs: test-gh-action
permissions:
packages: write
contents: read
env:
DOCKER_USERNAME: ${{ github.repository_owner }}
DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v4

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

- name: Cache Gradle Toolchain JDKs
uses: actions/cache@v4
with:
path: ~/.gradle/jdks
key: "${{ runner.os }}-gradle-jdks"
restore-keys: ${{ runner.os }}-gradle-jdks

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}

- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: "${{ runner.os }}-gradle-caches }}"
Comment thread
ascheman marked this conversation as resolved.
Outdated
restore-keys: ${{ runner.os }}-gradle-caches

- name: Execute Gradle build
env:
ADDITIONAL_TAGS: ${{ inputs.additional_tags }}
run: ./gradlew dockerPush -Ddocker.image.additional.tags="${ADDITIONAL_TAGS}" --scan --stacktrace

- name: Collect state upon failure
if: failure()
run: |
echo "Maven Repo:"
(cd $HOME && find .m2 -ls)
echo "Git:"
git status
echo "Env:"
Expand Down
15 changes: 15 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: 'hsc'
description: |
HSC (HTML Sanity Check) is a fast and lightweight tool for checking HTML, links, and accessibility issues.
It helps ensure clean, error-free web content and integrates seamlessly into CI/CD workflows.
inputs:
args:
description: 'CLI arguments (cf. https://hsc.aim42.org/manual/20_cli.html)'
required: false
runs:
using: 'docker'
# If the image tag changes, e.g., to v3, the action in the test workflow (workflows/gradle-build.yml) must be adjusted accordingly
image: 'docker://ghcr.io/aim42/hsc:v2'
entrypoint: '/hsc.sh'
args:
- ${{ inputs.args }}
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ tasks.register("integrationTestOnly") {
dependsOn(
':publishAllPublicationsToMyLocalRepositoryForFullIntegrationTestsRepository',
':htmlSanityCheck-cli:installDist',
':htmlSanityCheck-cli:dockerBuildLocal'
)

doLast {
Expand Down
14 changes: 14 additions & 0 deletions htmlSanityCheck-cli/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM eclipse-temurin:21-jre-alpine

ARG DESCRIPTION='HSC (HTML Sanity Check) is a fast and lightweight tool for checking HTML, links, and accessibility issues.'
ARG VERSION=Unknown

LABEL version=${VERSION}
LABEL org.opencontainers.image.description=${DESCRIPTION}

COPY hsc.sh /hsc.sh
RUN chmod 755 /hsc.sh

COPY build/libs/htmlSanityCheck-cli-${VERSION}-all.jar /hsc.jar

ENTRYPOINT ["/hsc.sh"]
Loading
Loading