diff --git a/.env.example b/.env.example index d5430e9..eaa4e05 100644 --- a/.env.example +++ b/.env.example @@ -23,3 +23,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 baab1f7..b431150 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..03509f2 --- /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 + + metropolis-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: Metropolis-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 Metropolis Plugin Jar + run: ./tools/build-plugin.sh + + - name: Upload Release Jar + if: success() + uses: actions/upload-artifact@v4 + with: + name: Metropolis-${{ 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: Metropolis ${{ needs.prepare-release.outputs.tag_name }} + files: build/libs/*.jar + generate_release_notes: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf14589..df03ca8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,8 +32,8 @@ rules. - `src/test/` contains isolated JUnit/Mockito tests. `src/integrationTest/` contains the opt-in tests that require a running server and RCON. - `build.gradle` defines dependencies, Java/Gradle versions, formatting, and - test tasks. `.github/workflows/gradle.yml` runs tests and builds on pushes - and pull requests to `develop`. + test tasks. `.github/workflows/gradle.yml` runs tests and builds on changes + to `main` and `develop`, and any `v*` tags. ## Prerequisites and Checks diff --git a/tools/publish-modrinth.sh b/tools/publish-modrinth.sh index 9cb8cad..6c10cb0 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,7 +32,13 @@ load_project_env() { fi done < "$PROJECT_ROOT/.env" } -load_project_env + +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:-metropolis-builder}" @@ -44,9 +46,10 @@ MODRINTH_VERSION="${MODRINTH_VERSION:-${VERSION:-}}" if [[ -z "$MODRINTH_VERSION" ]]; then MODRINTH_VERSION=$(cd "$PROJECT_ROOT" && ./gradlew -q properties --property version | sed -n 's/^version: //p' | head -n1) 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" @@ -60,12 +63,18 @@ 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 if [[ -z "$MODRINTH_VERSION" ]]; then echo "Error: could not determine the project version from Gradle." >&2 @@ -89,7 +98,7 @@ 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 @@ -159,3 +168,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