Central collection of reusable GitHub Actions workflows shared across OneLiteFeatherNET repositories (Butterfly, Aonyx-bom, ...).
Each workflow is exposed via workflow_call and consumed from downstream
repositories by referencing a tagged release of this repo.
| Workflow | Purpose |
|---|---|
.github/workflows/gradle-build-pr.yml |
Build & test a Gradle project on pull requests across a runner matrix. Skips when no Gradle-relevant files changed; aggregates JUnit results across the matrix; auto-enables verbose logging on debug re-runs. |
.github/workflows/gradle-publish.yml |
Build & publish a Gradle project to the OneLiteFeather Maven repository on tag pushes. |
.github/workflows/docker-publish.yml |
Build a container image and push it to the OneLiteFeather Harbor registry using chunked blob uploads (via regctl) so no single request exceeds the proxy body limit; optionally keyless-signs the image with cosign (GitHub OIDC). |
.github/workflows/release-please.yml |
Run release-please for a repository. |
.github/workflows/close-invalid-prs.yml |
Close PRs opened from a fork's default branch with a configurable message. |
.github/workflows/markdown-lint.yml |
Lint Markdown files with markdownlint-cli2 and check links with lychee. |
.github/workflows/sbom-publish.yml |
Publish a CycloneDX SBOM to the OneLiteFeather Dependency-Track instance, so the shipped dependency inventory keeps being matched against CVEs published later. Takes the project's own SBOM via an artifact, or generates one with Trivy when the project has none. |
.github/workflows/security-scan.yml |
Scan a filesystem or container image with Trivy and surface the findings in GitHub code scanning. Report-only by default, optionally gating. |
.github/workflows/resourcepack-publish.yml |
Pack a Minecraft resource pack directory into a reproducible ZIP, upload it to an S3-compatible store with a .sha1, a .sha256 and a JSON manifest beside each archive, and announce it on Discord. Separate release and snapshot channels. |
.github/workflows/pr-lint.yml |
Enforce Conventional Commits on the PR title and on every commit of the branch, so release-please cannot silently skip a release. |
- Java 25 on Temurin.
- Three-OS matrix on PRs:
ubuntu-latest,windows-latest,macos-latest. gradle-build-prrunsbuild testby default (noclean, to keep incremental caches). Setrun-tests: falsefor BOM-only / test-less projects: the default task drops tobuildand the JUnit aggregation step is skipped.- Path filter: build only runs when files under
src/,*.gradle*,buildSrc/, JVM sources, or.github/workflows/**changed. - Debug re-runs (
Re-run with debug logging) automatically activate--info --stacktrace. - Test reports are uploaded as artifacts on every run and aggregated into a unified check + PR comment.
- Concurrency cancels superseded PR runs; publish runs are never cancelled mid-flight.
markdown-lintfilters on.md/.markdownlint*/.lycheeignorepaths and runs markdownlint + lychee in parallel.
This repository is released via release-please.
Pin consumers to a tag (e.g. @v2.0.0) or a major (e.g. @v2) rather than
main for reproducible builds.
Use Renovate in
your consumer repository to auto-bump the version pin. The github-actions
manager picks up uses: OneLiteFeatherNET/workflows/.github/workflows/foo.yml@vX
out of the box. A sample renovate.json is shipped at the root of this repo;
copy it as a starting point.
name: Build PR
on: [pull_request]
jobs:
build:
uses: OneLiteFeatherNET/workflows/.github/workflows/gradle-build-pr.yml@v2
secrets: inheritSingle-OS, custom JDK, force-build:
jobs:
build:
uses: OneLiteFeatherNET/workflows/.github/workflows/gradle-build-pr.yml@v2
with:
java-version: "21"
runs-on: '["ubuntu-latest"]'
force-build: true
secrets: inheritBOM-only or test-less project:
jobs:
build:
uses: OneLiteFeatherNET/workflows/.github/workflows/gradle-build-pr.yml@v2
with:
run-tests: false
secrets: inheritCustom path filter (must define a code: key):
jobs:
build:
uses: OneLiteFeatherNET/workflows/.github/workflows/gradle-build-pr.yml@v2
with:
paths-filters: |
code:
- 'src/**'
- 'build.gradle.kts'
- 'gradle/**'
secrets: inheritname: Publish JAR
on:
push:
tags: ["v*"]
jobs:
publish:
uses: OneLiteFeatherNET/workflows/.github/workflows/gradle-publish.yml@v2
secrets: inheritPushes the image one blob at a time in chunks below the proxy body limit, so
large layers no longer fail with 413 Request Entity Too Large / 504 Gateway Timeout behind a proxy such as Cloudflare (100 MB limit). Plain docker push
/ buildx cannot chunk a blob; this workflow builds the image to an OCI archive
and pushes it with regctl (--blob-chunk /
--blob-max) instead.
Typical use is from a release job, gated on release_created:
Typical use is from a release job, gated on release_created. Grant
id-token: write on the calling job so cosign can sign keyless:
jobs:
docker:
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
permissions:
contents: read
id-token: write # required for keyless cosign signing
uses: OneLiteFeatherNET/workflows/.github/workflows/docker-publish.yml@v2
with:
image-name: "otis/otis" # registry host comes from HARBOR_REGISTRY
version: ${{ needs.release-please.outputs.version }}
# Build the container context with Gradle first ($VERSION is exported):
setup-java: true
build-command: "./gradlew jar optimizedBuildLayers optimizedDockerfile -Pversion=$VERSION"
context: "./backend/build/docker/optimized"
secrets: inheritFor a project with a plain Dockerfile checked into the repo, drop the Gradle
inputs (and the permissions block if you set sign: false):
jobs:
docker:
uses: OneLiteFeatherNET/workflows/.github/workflows/docker-publish.yml@v2
with:
image-name: "myteam/myapp"
version: "1.2.3"
context: "."
sign: false # skip signing (no id-token needed)
secrets: inheritDefault tags are {{version}}, {{major}}.{{minor}}, {{major}} and a
sha- tag; add more via extra-tags. Tune chunking with blob-chunk (bytes,
default 50 MiB) and parallel layer uploads with req-concurrent. Signing is
keyless via GitHub OIDC — no signing key/secret to manage; verify with the
workflow identity (--certificate-identity-regexp + --certificate-oidc-issuer https://token.actions.githubusercontent.com). The pushed manifest digest and
full image reference are exposed as workflow outputs.
name: release-please
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
uses: OneLiteFeatherNET/workflows/.github/workflows/release-please.yml@v2.5.0The workflow forwards the action's outputs, so a follow-up job can be gated on whether a release was actually cut:
jobs:
release-please:
uses: OneLiteFeatherNET/workflows/.github/workflows/release-please.yml@v2.5.0
publish:
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
uses: OneLiteFeatherNET/workflows/.github/workflows/gradle-publish.yml@v2.5.0
secrets: inherit| Output | Description |
|---|---|
release_created |
'true' when the root package was released. |
releases_created |
'true' when at least one release was created - use this on a multi-package manifest. |
tag_name |
Tag of the root package's release, e.g. v1.2.3. Empty when it was not released. |
version |
Version of the root package's release, e.g. 1.2.3. Empty when it was not released. |
sha |
Commit the root package's release was cut from. |
paths_released |
JSON array of released package paths. |
prs |
JSON array of the release pull requests opened or updated. |
On a multi-package manifest the action exposes per-package values as <path>--release_created
and friends. Those names are not fixed, so a reusable workflow cannot declare them - gate on
releases_created and read paths_released instead.
name: Close invalid PRs
on:
pull_request_target:
types: [opened]
jobs:
close:
uses: OneLiteFeatherNET/workflows/.github/workflows/close-invalid-prs.yml@v2
with:
protected-branch: mainname: Lint docs
on:
pull_request:
paths:
- '**/*.md'
- '.markdownlint.json'
- '.lycheeignore'
jobs:
lint:
uses: OneLiteFeatherNET/workflows/.github/workflows/markdown-lint.yml@v2
with:
force-lint: trueA .markdownlint.json and optional .lycheeignore (regex per line) at the
repo root configure rules and skip-lists.
Two shapes, depending on whether the project already generates an SBOM.
The project generates its own (preferred — a build tool resolves the dependency graph better than any external scanner). Upload it as an artifact, then hand the artifact name over:
jobs:
publish:
# ... your existing build/publish job, ending with:
# - uses: actions/upload-artifact@v4
# with:
# name: sbom
# path: build/reports/cyclonedx/bom.xml
sbom:
needs: publish
uses: OneLiteFeatherNET/workflows/.github/workflows/sbom-publish.yml@v2.6.0
with:
project-name: "MyProject"
project-version: "1.2.3"
artifact-name: "sbom"
sbom-path: "bom.xml"
secrets: inheritThe project generates nothing — leave artifact-name empty and Trivy
produces a CycloneDX SBOM from the checked-out repository:
jobs:
sbom:
uses: OneLiteFeatherNET/workflows/.github/workflows/sbom-publish.yml@v2.6.0
with:
project-name: "MyProject"
project-version: "1.2.3"
secrets: inheritRun it as its own job, not as a step inside the publish job: Dependency-Track being unreachable should never take down the release that produced the artifact.
autocreate defaults to true, which needs the API key's team to hold
PROJECT_CREATION_UPLOAD on top of BOM_UPLOAD. Without it the server
answers 403 the first time any new version is uploaded.
name: Security
on:
pull_request:
schedule:
- cron: '0 6 * * 1' # new CVEs land against unchanged code
jobs:
scan:
permissions:
contents: read
security-events: write # SARIF upload to code scanning
uses: OneLiteFeatherNET/workflows/.github/workflows/security-scan.yml@v2.6.0Report-only by default: adopting it makes findings visible in code scanning
without turning a repository's CI red on day one. Set fail-on-findings: true
once a repo is clean enough to keep it that way.
On private repositories the SARIF upload needs GitHub Advanced Security.
Without it, set upload-sarif: false and rely on the job summary plus
fail-on-findings.
To stop a vulnerable build from ever reaching a registry, put the scan in its own job between the build and everything that publishes. Have the build upload the artifact, gate on it, and let the publishing job depend on the gate:
jobs:
build: # produces the artifact, publishes nothing
# - uses: actions/upload-artifact@v4
# with: { name: plugin-jar, path: build/libs/*.jar }
security-gate:
needs: build
permissions:
contents: read
security-events: write
uses: OneLiteFeatherNET/workflows/.github/workflows/security-scan.yml@v2.6.0
with:
scan-type: rootfs # see the warning below
artifact-name: plugin-jar
fail-on-findings: true
secrets: inherit
publish:
needs: security-gate # nothing public happens until the gate is green
# ...Use
rootfs, notfs, for built JVM artifacts. Trivy'sfsscanner ignores JAR contents. Measured on AntiRedstoneClock-Remastered's shaded jar:fsreported 0 packages and 0 findings,rootfsreported 12 packages and a HIGH finding.fson the source tree of a Gradle project without agradle.lockfilealso finds nothing — a gate on it looks green because it checked nothing at all.
Everything that ships lives in one directory (pack-dir, default pack/), whose
contents become the ZIP root — so workflows, docs and changelog in the repository
cannot leak into the archive.
Two channels off the same logic. Snapshots on every push to the default branch:
jobs:
publish:
# Skip the release-please merge commit, or the same version gets published twice.
# A GitHub expression, not a shell comparison: a commit message is attacker-controllable.
if: >-
github.event_name == 'workflow_dispatch' ||
!startsWith(github.event.head_commit.message, 'chore(main): release')
uses: OneLiteFeatherNET/workflows/.github/workflows/resourcepack-publish.yml@v2.7.0
with:
channel: snapshot
s3-endpoint: "https://s3.onelitefeather.dev"
bucket: "my-pack"
secrets: inheritReleases chained off release-please:
jobs:
release-please:
uses: OneLiteFeatherNET/workflows/.github/workflows/release-please.yml@v2.7.0
publish:
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
uses: OneLiteFeatherNET/workflows/.github/workflows/resourcepack-publish.yml@v2.7.0
with:
channel: release
version: ${{ needs.release-please.outputs.version }}
s3-endpoint: "https://s3.onelitefeather.dev"
bucket: "my-pack"
secrets: inheritChain it via needs/if rather than a tag-triggered workflow: release-please tags
with the default GITHUB_TOKEN, and pushes made with that token do not trigger
further workflows in the same repository. A tag-triggered publish would never fire.
Each run writes a versioned archive plus a .sha1, a .sha256 and a .json
manifest beside it, and a latest alias carrying its own copies of all three:
releases/my-pack-1.4.2.zip releases/my-pack-latest.zip
releases/my-pack-1.4.2.zip.sha1 releases/my-pack-latest.zip.sha1
releases/my-pack-1.4.2.zip.sha256 releases/my-pack-latest.zip.sha256
releases/my-pack-1.4.2.zip.json releases/my-pack-latest.zip.json
That pairing is the point: a server points permanently at
<prefix>/<pack>-latest.zip and reads the expected hash from the file next to it.
Minecraft re-downloads a pack exactly when the hash it is handed changes, so the URL
in the server config never has to move.
SHA-1 is the functional hash. resource-pack-sha1 in server.properties and the
second argument of setResourcePack(url, hash) are both SHA-1, so that is the value
a server actually hands the client — it is what the workflow prints first in Discord
and in the job summary. SHA256 sits next to it purely as an integrity check for
anything verifying the download itself. Both files are in sha1sum -c / sha256sum -c
format, and the alias' checksum files record the alias' own file name so -c passes
against either copy.
The manifest is the machine-readable form of all of it — one request instead of parsing two text files:
{
"schemaVersion": 1,
"pack": "my-pack",
"channel": "release",
"version": "1.4.2",
"file": "my-pack-latest.zip",
"url": "https://s3.onelitefeather.dev/my-pack/releases/my-pack-latest.zip",
"size": 4823019,
"commit": "7f2094c",
"builtAt": "2026-08-10T10:56:03Z",
"hashes": { "sha1": "628821c8…", "sha256": "703de715…" }
}The latest manifest resolves which version the alias currently points at, which no
checksum file can express. hashes is an object rather than flat fields, so another
algorithm is one more key and not a schema break; schemaVersion marks a real break
if one ever happens.
Which algorithms get published is the HASH_ALGOS list at the top of the job — the
only place in the workflow that names one. Checksum files, manifest entries, the
Discord message and the job summary are all derived from it, so adding sha512 means
adding it to that list and declaring the matching output (GitHub requires outputs:
to be static). Discord and the summary read their values out of the manifest rather
than naming hashes themselves.
The ZIP is built reproducibly (fixed file order, fixed timestamp, zip -X). Without
that the hashes would differ on every run and every player would re-download an
unchanged pack after every build. The manifest carries a build timestamp and so does
differ per run — it is metadata about the archive, not part of it.
s3-endpoint is an input rather than a secret on purpose. GitHub masks secret values
wherever they appear, so an endpoint passed as a secret renders the download URL as
*** in the job summary and in the logs — the two places anyone actually looks for
it. Version the pack with release-please's release-type: simple, which maintains
the version.txt this workflow reads.
Workflows that publish or read from the OneLiteFeather Maven repository expect
these secrets to be available in the caller repository (and forwarded via
secrets: inherit):
ONELITEFEATHER_MAVEN_USERNAMEONELITEFEATHER_MAVEN_PASSWORD
docker-publish pushes to the Harbor registry, so it expects:
HARBOR_REGISTRY— registry host (no scheme), e.g.harbor.onelitefeather.devHARBOR_USERNAMEHARBOR_PASSWORD
sbom-publish talks to Dependency-Track, so it expects:
DEPENDENCYTRACK_HOSTNAME— host only, no scheme, e.g.dependency-track.onelitefeather.devDEPENDENCYTRACK_APIKEY— the key's team needsBOM_UPLOAD, plusPROJECT_CREATION_UPLOADwhileautocreateis on
security-scan needs no secrets at all.
resourcepack-publish uploads to an S3-compatible store, so it expects:
S3_ACCESS_KEY_IDS3_SECRET_ACCESS_KEYDISCORD_WEBHOOK— optional; without it the upload still runs and only the announcement is skipped
The endpoint and bucket are inputs, not secrets — see the resource pack section above.
Signing is keyless (cosign + GitHub OIDC) — no signing secrets. The calling job
just needs permissions: id-token: write when sign: true (the default).
For gradle-build-pr, JUnit XML from every matrix job is uploaded as
test-reports-<os>-jdk<version> and merged by a downstream test-report job
that uses EnricoMi/publish-unit-test-result-action
to post a unified check and PR comment.
When a workflow run fails, press Re-run with debug logging in the GitHub UI.
The reusable workflows detect RUNNER_DEBUG=1 automatically and switch Gradle
to --info --stacktrace. Build summaries (test counts, build scan link) are
posted to the run summary and as PR comments via
gradle/actions/setup-gradle.
- Conventional Commits are required (
feat:,fix:,chore:, ...). - Breaking changes use
feat!:orBREAKING CHANGE:to trigger a major bump. - Releases are produced automatically by release-please on merge to
main.
MIT — see LICENSE.