diff --git a/.env.example b/.env.example index 736c0b4..35320bf 100644 --- a/.env.example +++ b/.env.example @@ -24,3 +24,7 @@ MODRINTH_LOADERS=paper MODRINTH_VERSION_TYPE= MODRINTH_DEPENDENCIES= MODRINTH_FILE= +# Sync project body/README to Modrinth during publish (true/false) +MODRINTH_SYNC_BODY=true +MODRINTH_DESCRIPTION_FILE=docs/modrinth-description.md + diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 2e8baf9..13ad577 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -2,9 +2,10 @@ name: Java CI with Gradle on: push: - branches: [ "develop" ] + branches: [ "main", "develop" ] + tags: [ "v*" ] pull_request: - branches: [ "develop" ] + branches: [ "main", "develop" ] jobs: build: @@ -14,6 +15,8 @@ jobs: steps: - uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Set up JDK 25 uses: actions/setup-java@v6 @@ -25,7 +28,7 @@ jobs: uses: gradle/actions/setup-gradle@v5 with: gradle-version: '9.8.0' - cache-read-only: ${{ github.ref != 'refs/heads/develop' }} + cache-read-only: ${{ github.ref != 'refs/heads/develop' && github.ref != 'refs/heads/main' }} - name: Run Unit Tests run: ./gradlew test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..61d4bc4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,168 @@ +name: Release + +on: + workflow_dispatch: + inputs: + tag: + description: 'Git tag to release (e.g., v1.0.0)' + required: true + type: string + +permissions: + contents: write + +jobs: + prepare-release: + name: Validate Tag & Prepare Release Branch + runs-on: ubuntu-latest + outputs: + tag_commit: ${{ steps.validate.outputs.tag_commit }} + tag_name: ${{ steps.validate.outputs.tag_name }} + release_branch: ${{ steps.validate.outputs.release_branch }} + version_name: ${{ steps.validate.outputs.version_name }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Validate Tag, Develop, Main, and Create Release Branch + id: validate + run: | + TAG="${{ github.event.inputs.tag }}" + echo "Target tag: $TAG" + + # Fetch remote branches and tags + git fetch origin develop:develop main:main --tags + + # 1. Verify tag exists + if ! git rev-parse -q --verify "refs/tags/$TAG" > /dev/null; then + echo "::error::Tag '$TAG' does not exist in the repository." + exit 1 + fi + + TAG_COMMIT=$(git rev-list -n 1 "$TAG") + echo "Tag '$TAG' points to commit: $TAG_COMMIT" + + # 2. Verify tag commit is in develop branch + if ! git merge-base --is-ancestor "$TAG_COMMIT" origin/develop; then + echo "::error::Tag '$TAG' (commit $TAG_COMMIT) is not in the develop branch." + exit 1 + fi + echo "Confirmed: Tag '$TAG' is in develop branch." + + # 3. Verify tag commit is in main, or fast-forward merge it to main + if git merge-base --is-ancestor "$TAG_COMMIT" origin/main; then + echo "Confirmed: Tag '$TAG' is already in main branch." + else + echo "Tag '$TAG' is not in main. Attempting fast-forward only merge..." + git checkout main + if ! git merge --ff-only "$TAG_COMMIT"; then + echo "::error::Tag '$TAG' cannot be fast-forward merged into main." + exit 1 + fi + git push origin main + echo "Successfully fast-forward merged '$TAG' into main." + fi + + # 4. Create and push the release/v* branch from the tag commit + RELEASE_BRANCH="release/${TAG}" + echo "Creating release branch '$RELEASE_BRANCH' from commit $TAG_COMMIT..." + git checkout -B "$RELEASE_BRANCH" "$TAG_COMMIT" + git push -u origin "$RELEASE_BRANCH" --force + + VERSION_NAME="${TAG#v}" + + echo "tag_commit=$TAG_COMMIT" >> $GITHUB_OUTPUT + echo "tag_name=$TAG" >> $GITHUB_OUTPUT + echo "release_branch=$RELEASE_BRANCH" >> $GITHUB_OUTPUT + echo "version_name=$VERSION_NAME" >> $GITHUB_OUTPUT + + scorekeeper-release: + name: Build, Test & Publish Release + needs: prepare-release + runs-on: ubuntu-latest + + steps: + - name: Checkout release commit + uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare-release.outputs.tag_commit }} + fetch-depth: 0 + + - name: Ensure tag ref is checked out + run: | + git checkout -q "refs/tags/${{ needs.prepare-release.outputs.tag_name }}" + + - name: Validate Modrinth Credentials + id: check_credentials + run: | + if [ -z "${{ secrets.MODRINTH_TOKEN }}" ]; then + echo "::warning::Modrinth release token was not provided. Release will not be published to Modrinth." + echo "::error::Missing MODRINTH_TOKEN secret. Please configure MODRINTH_TOKEN in repository secrets." + exit 1 + fi + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '25' + + - name: Setup Gradle 9.8.0 + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: '9.8.0' + + - name: Make Gradle wrapper executable + run: chmod +x gradlew + + - name: Run Unit Tests + run: ./gradlew test + + - name: Upload Test Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: ScoreKeeper-test-report-${{ needs.prepare-release.outputs.tag_name }} + path: | + build/reports/tests/test + build/test-results/test + if-no-files-found: ignore + retention-days: 14 + + - name: Build ScoreKeeper Plugin Jar + run: ./tools/build-plugin.sh + + - name: Upload Release Jar + if: success() + uses: actions/upload-artifact@v4 + with: + name: ScoreKeeper-${{ needs.prepare-release.outputs.version_name }} + path: build/libs/*.jar + if-no-files-found: error + retention-days: 14 + + - name: Publish to Modrinth & Update Description + env: + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + MODRINTH_VERSION: ${{ needs.prepare-release.outputs.version_name }} + TAG: ${{ needs.prepare-release.outputs.tag_name }} + run: | + chmod +x ./tools/publish-modrinth.sh + ./tools/publish-modrinth.sh + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare-release.outputs.tag_name }} + name: ScoreKeeper ${{ needs.prepare-release.outputs.tag_name }} + files: build/libs/*.jar + generate_release_notes: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b58fd0c..830768f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -156,9 +156,10 @@ affected scenarios in [human-tests.md](docs/human-tests.md). Documentation-only changes need link/metadata checks, not a Paper server. CI in [gradle.yml](.github/workflows/gradle.yml) runs for pushes and pull requests -to `develop`. It invokes `test`, builds with the Bash helper, and uploads jar -artifacts (test reports only if present). It does not start Paper, publish a -release, or verify the two-version runtime compatibility claim. +to `main` and `develop`, and any `v*` tags. Manual releases are handled via +[release.yml](.github/workflows/release.yml). It invokes `test`, builds with the +Bash helper, and uploads jar artifacts (test reports only if present). It does +not start Paper or verify the two-version runtime compatibility claim. ## Local Paper server: macOS, Linux, or WSL diff --git a/tools/publish-modrinth.sh b/tools/publish-modrinth.sh index ba56e76..1b96a4a 100755 --- a/tools/publish-modrinth.sh +++ b/tools/publish-modrinth.sh @@ -4,10 +4,6 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -if [[ ! -r "$PROJECT_ROOT/.env" ]]; then - echo "Error: $PROJECT_ROOT/.env is missing or unreadable. Copy .env.example to .env and set its values." >&2 - exit 1 -fi load_project_env() { local env_line env_key env_value first_char last_char while IFS= read -r env_line || [[ -n "$env_line" ]]; do @@ -36,23 +32,27 @@ load_project_env() { fi done < "$PROJECT_ROOT/.env" } -load_project_env -# Required env: -# MODRINTH_TOKEN -# MODRINTH_PROJECT_ID (project ID or slug) -# MODRINTH_GAME_VERSIONS (optional comma-separated list; defaults to 26.2,26.3) -# MODRINTH_LOADERS (optional, comma-separated, default paper) -# MODRINTH_CHANGELOG (optional) -# Optional: -# MODRINTH_DEPENDENCIES (optional JSON array) -# MODRINTH_FILE (optional path to the jar; defaults to build/libs/*.jar) +if [[ -r "$PROJECT_ROOT/.env" ]]; then + load_project_env +elif [[ -z "${MODRINTH_TOKEN:-}" ]]; then + echo "Error: $PROJECT_ROOT/.env is missing or unreadable and MODRINTH_TOKEN is not set. Copy .env.example to .env and set its values." >&2 + exit 1 +fi MODRINTH_TOKEN="${MODRINTH_TOKEN:-}" MODRINTH_PROJECT_ID="${MODRINTH_PROJECT_ID:-5NYwxysp}" +MODRINTH_VERSION="${MODRINTH_VERSION:-${VERSION:-}}" +if [[ -z "$MODRINTH_VERSION" ]]; then + MODRINTH_VERSION=$( + "$PROJECT_ROOT/gradlew" -q -p "$PROJECT_ROOT" properties --property version \ + | sed -n 's/^version: //p' + ) +fi + generate_changelog() { local current_tag previous_tag range commits - current_tag=$(git -C "$PROJECT_ROOT" describe --tags --exact-match HEAD 2>/dev/null || true) + current_tag="${TAG:-$(git -C "$PROJECT_ROOT" describe --tags --exact-match HEAD 2>/dev/null || true)}" if [[ -n "$current_tag" ]]; then previous_tag=$(git -C "$PROJECT_ROOT" describe --tags --abbrev=0 "${current_tag}^" 2>/dev/null || true) range="${previous_tag:+$previous_tag..}$current_tag" @@ -66,17 +66,19 @@ generate_changelog() { fi printf '%s\n\n%s\n' "## What's Changed" "$commits" } + MODRINTH_CHANGELOG="${MODRINTH_CHANGELOG:-$(generate_changelog)}" MODRINTH_GAME_VERSIONS="${MODRINTH_GAME_VERSIONS:-26.2,26.3}" MODRINTH_LOADERS="${MODRINTH_LOADERS:-paper}" MODRINTH_VERSION_TYPE="${MODRINTH_VERSION_TYPE:-}" MODRINTH_FILE="${MODRINTH_FILE:-}" MODRINTH_DEPENDENCIES="${MODRINTH_DEPENDENCIES:-}" +MODRINTH_SYNC_BODY="${MODRINTH_SYNC_BODY:-true}" +MODRINTH_DESCRIPTION_FILE="${MODRINTH_DESCRIPTION_FILE:-$PROJECT_ROOT/docs/modrinth-description.md}" +if [[ ! -f "$MODRINTH_DESCRIPTION_FILE" ]]; then + MODRINTH_DESCRIPTION_FILE="$PROJECT_ROOT/README.md" +fi -MODRINTH_VERSION=$( - "$PROJECT_ROOT/gradlew" -q -p "$PROJECT_ROOT" properties --property version \ - | sed -n 's/^version: //p' -) if [[ -z "$MODRINTH_VERSION" ]]; then echo "Error: could not determine the project version from Gradle." >&2 exit 1 @@ -99,9 +101,10 @@ if [[ -n "$MODRINTH_FILE" && "$MODRINTH_FILE" != /* ]]; then fi if [[ -z "$MODRINTH_TOKEN" ]]; then - echo "Error: MODRINTH_TOKEN is not set in $PROJECT_ROOT/.env" >&2 + echo "Error: MODRINTH_TOKEN is not set (check $PROJECT_ROOT/.env or environment variable)." >&2 exit 1 fi + if [[ -z "$MODRINTH_FILE" ]]; then MODRINTH_FILE=$(ls -t "$PROJECT_ROOT"/build/libs/*.jar 2>/dev/null | grep -v -- '-all\.jar$' | head -n1 || true) fi @@ -167,3 +170,26 @@ if [[ "$HTTP_STATUS" != 2?? ]]; then exit 1 fi printf '%s\n' "$HTTP_BODY" | jq . + +if [[ "$MODRINTH_SYNC_BODY" == "true" && -f "$MODRINTH_DESCRIPTION_FILE" ]]; then + echo "Syncing project description to Modrinth from $MODRINTH_DESCRIPTION_FILE..." + BODY_CONTENT=$(cat "$MODRINTH_DESCRIPTION_FILE") + PATCH_DATA=$(jq -cn --arg body "$BODY_CONTENT" '{body: $body}') + + PATCH_RESPONSE=$(curl -sS -w $'\n%{http_code}' \ + -X PATCH "https://api.modrinth.com/v2/project/$MODRINTH_PROJECT_ID" \ + -H "Authorization: $MODRINTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$PATCH_DATA") + + PATCH_STATUS="${PATCH_RESPONSE##*$'\n'}" + PATCH_BODY="${PATCH_RESPONSE%$'\n'*}" + if [[ "$PATCH_STATUS" != 2?? ]]; then + echo "Warning: Modrinth project description update returned HTTP $PATCH_STATUS:" >&2 + if ! printf '%s\n' "$PATCH_BODY" | jq -r '.description // .error // .' >&2; then + printf '%s\n' "$PATCH_BODY" >&2 + fi + else + echo "Successfully updated Modrinth project description." + fi +fi