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
8 changes: 8 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Shared TFMC XML formatting; see Docs/POM-CONVENTIONS.md.
[*.xml]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
70 changes: 70 additions & 0 deletions .github/scripts/plugin-artifact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Validate the Maven runtime JAR and optionally stage a release bundle."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import zipfile


def validate(source, version):
source = Path(source).resolve()
target = Path.cwd().resolve() / "target"
if not source.is_relative_to(target) or not source.is_file():
raise ValueError("Expected the Maven runtime JAR inside target/")
if not re.fullmatch(r"[a-z][a-z0-9]*-" + re.escape(version) + r"\.jar", source.name):
raise ValueError("Runtime JAR filename must use a lowercase alphanumeric name followed by the Maven version")
with zipfile.ZipFile(source) as jar:
descriptors = {"plugin.yml", "paper-plugin.yml"}.intersection(jar.namelist())
if not descriptors:
raise ValueError("Runtime JAR has no plugin descriptor")
if jar.testzip() is not None:
raise ValueError("Runtime JAR is corrupt")
for name in descriptors:
text = jar.read(name).decode("utf-8-sig")
versions = re.findall(
r'''^version:[ \t]*(?:"([^"\r\n]*)"|'([^'\r\n]*)'|([^\s#]+))(?:(?:[ \t]+\#[^\r\n]*)|[ \t]*)\r?$''',
text, re.MULTILINE,
)
if len(versions) != 1 or next((v for v in versions[0] if v), "") != version:
raise ValueError(f"{name} version must match Maven version {version}")
return source


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--jar", required=True, type=Path)
parser.add_argument("--version", required=True)
parser.add_argument("--stage", type=Path)
parser.add_argument("--dependencies", type=Path)
args = parser.parse_args()
try:
source = validate(args.jar, args.version)
if args.stage:
if os.environ["TAG"] != f"v{args.version}":
raise ValueError("Release tag must match the Maven version")
dependencies = json.loads(args.dependencies.read_text()) if args.dependencies else []
digest = hashlib.sha256(source.read_bytes()).hexdigest()
metadata = {
"repository": os.environ["GITHUB_REPOSITORY"],
"commit": os.environ["GITHUB_SHA"],
"tag": os.environ["TAG"],
"run": f"{os.environ['GITHUB_SERVER_URL']}/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}",
"plugin_dependencies": dependencies,
"artifact": source.name,
"sha256": digest,
}
args.stage.mkdir()
shutil.copyfile(source, args.stage / source.name)
(args.stage / "SHA256SUMS").write_text(f"{digest} {source.name}\n")
(args.stage / "build.json").write_text(json.dumps(metadata, indent=2) + "\n")
print(f"Verified {source.name}: filename and embedded version match {args.version}")
except (ValueError, OSError, KeyError, zipfile.BadZipFile) as error:
parser.exit(1, f"Plugin artifact validation failed: {error}\n")


if __name__ == "__main__":
main()
80 changes: 80 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Build

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '21'
- name: Install shared plugin dependencies
uses: TF-Minecraft/TLibs/.github/actions/setup-plugins@8ef41de589f121c98ff892b10696e7c1feb4ca74
with:
mode: pinned

- name: Set build version
id: dev
shell: bash
run: |
dev_version="main-SNAPSHOT"
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
dev_version="DEV-$(date -u +%Y%m%d-%H%M)"
fi
mvn -B --no-transfer-progress org.codehaus.mojo:versions-maven-plugin:2.22.0:set \
-DnewVersion="$dev_version" -DgenerateBackupPoms=false
mvn -B --no-transfer-progress help:evaluate \
-Dexpression=project.build.finalName -Doutput="$RUNNER_TEMP/final-name"
final_name=$(cat "$RUNNER_TEMP/final-name")
[[ "$final_name" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || exit 1
echo "version=$dev_version" >> "$GITHUB_OUTPUT"
echo "name=$final_name" >> "$GITHUB_OUTPUT"
echo "jar=target/$final_name.jar" >> "$GITHUB_OUTPUT"

- name: Run unit tests and build
run: mvn -B --no-transfer-progress clean verify -DskipTests=false -Dmaven.test.skip=false

- name: Verify runtime JAR
env:
ARTIFACT_PATH: ${{ steps.dev.outputs.jar }}
BUILD_VERSION: ${{ steps.dev.outputs.version }}
run: python3 .github/scripts/plugin-artifact.py --jar "$ARTIFACT_PATH" --version "$BUILD_VERSION"

- if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v7
with:
name: ${{ steps.dev.outputs.name }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ steps.dev.outputs.jar }}
.build/plugin-dependencies.json
include-hidden-files: true
if-no-files-found: error

- name: Upload unit test reports
if: ${{ !cancelled() && hashFiles('target/surefire-reports/*.xml') != '' }}
uses: actions/upload-artifact@v7
with:
name: unit-test-reports-${{ github.run_id }}-${{ github.run_attempt }}
path: target/surefire-reports/
if-no-files-found: error

- name: Upload coverage reports
if: ${{ !cancelled() && hashFiles('target/site/jacoco/jacoco.xml') != '' }}
uses: actions/upload-artifact@v7
with:
name: coverage-reports-${{ github.run_id }}-${{ github.run_attempt }}
path: target/site/jacoco/
if-no-files-found: error
137 changes: 137 additions & 0 deletions .github/workflows/maven-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: Maven plugin release

on:
workflow_call:
inputs:
java-version:
required: true
type: string
secrets:
DEPS_TOKEN:
required: false

permissions:
contents: read

concurrency:
group: plugin-release-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: false

defaults:
run:
shell: bash

jobs:
build:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: ${{ inputs.java-version }}

- name: Validate release version
env:
TAG: ${{ github.ref_name }}
run: |
[[ "$TAG" =~ ^v[0-9]+\.[0-9]+(\.[0-9]+)?(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]] || {
echo '::error::Expected vMAJOR.MINOR or vMAJOR.MINOR.PATCH, with an optional prerelease suffix.'
exit 1
}
[[ "${TAG^^}" != *SNAPSHOT* ]] || exit 1

- name: Install shared plugin dependencies
uses: TF-Minecraft/TLibs/.github/actions/setup-plugins@8ef41de589f121c98ff892b10696e7c1feb4ca74
with:
mode: pinned

- name: Prepare pinned dependencies
env:
GH_TOKEN: ${{ secrets.DEPS_TOKEN }}
run: |
if [[ -f .github/scripts/prepare-release.sh ]]; then
bash .github/scripts/prepare-release.sh
fi

- name: Set version from tag and build
id: maven
env:
TAG: ${{ github.ref_name }}
run: |
mvn -B --no-transfer-progress org.codehaus.mojo:versions-maven-plugin:2.22.0:set \
-DnewVersion="${TAG#v}" -DgenerateBackupPoms=false
mvn -B --no-transfer-progress help:evaluate \
-Dexpression=project.build.finalName -Doutput="$RUNNER_TEMP/final-name"
final_name=$(cat "$RUNNER_TEMP/final-name")
[[ "$final_name" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || exit 1
echo "jar=target/$final_name.jar" >> "$GITHUB_OUTPUT"
mvn -B --no-transfer-progress clean verify -DskipTests=false -Dmaven.test.skip=false

- name: Stage only the release JAR
env:
ARTIFACT_PATH: ${{ steps.maven.outputs.jar }}
TAG: ${{ github.ref_name }}
run: |
python3 .github/scripts/plugin-artifact.py \
--jar "$ARTIFACT_PATH" --version "${TAG#v}" \
--stage "$RUNNER_TEMP/plugin-release" \
--dependencies .build/plugin-dependencies.json

- uses: actions/upload-artifact@v7
with:
name: plugin-release-${{ github.sha }}
path: ${{ runner.temp }}/plugin-release/
if-no-files-found: error
retention-days: 30

publish:
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: write
steps:
- uses: actions/download-artifact@v8
with:
name: plugin-release-${{ github.sha }}
path: release

- name: Create draft release
working-directory: release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
sha256sum --check SHA256SUMS
flags=()
if [[ "$TAG" == *-* ]]; then flags+=(--prerelease); fi
gh api --method POST "repos/$GH_REPO/releases/generate-notes" \
-f tag_name="$TAG" --jq .body > "$RUNNER_TEMP/release-changes.md"
python3 - <<'PY'
import os, re
from pathlib import Path
from urllib.parse import quote
temp = Path(os.environ['RUNNER_TEMP'])
changes = (temp / 'release-changes.md').read_text().strip()
changes = re.sub(r"^## What's Changed\s*", '', changes)
changes = re.sub(r'^(#{1,5}) ', r'\1# ', changes, flags=re.MULTILINE)
lines = ['## Changes', '', changes or 'Initial release.', '', '## Downloads', '']
assets = sorted([*Path('.').glob('*.jar'), *Path('.').glob('*.tar.gz')])
assets += [Path('SHA256SUMS'), Path('build.json')]
base = f"https://github.com/{os.environ['GH_REPO']}/releases/download/{quote(os.environ['TAG'], safe='')}/"
for asset in assets:
if not asset.is_file():
raise SystemExit(f'Missing release asset: {asset}')
description = {'SHA256SUMS': ' — SHA-256 checksums.', 'build.json': ' — Build metadata.'}.get(asset.name, '')
lines.append(f'- [`{asset.name}`]({base}{quote(asset.name)}){description}')
(temp / 'release-notes.md').write_text('\n'.join(lines) + '\n')
PY
gh release create "$TAG" ./*.jar SHA256SUMS build.json \
--verify-tag --draft --title "$TAG" --notes-file "$RUNNER_TEMP/release-notes.md" "${flags[@]}"
26 changes: 26 additions & 0 deletions .github/workflows/pr-size.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: PR size

on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]

permissions:
contents: read
issues: write
pull-requests: write

concurrency:
group: pr-size-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
size:
name: PR size
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: TF-Minecraft/TLibs/.github/actions/pr-size@0bfb259256648c6139c6a94a0935492830cbbec9
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Release

on:
push:
tags: ['v*']

permissions:
contents: read

jobs:
release:
permissions:
contents: write
uses: ./.github/workflows/maven-release.yml
with:
java-version: '21'
secrets:
DEPS_TOKEN: ${{ secrets.DEPS_TOKEN }}
Loading