diff --git a/.github/workflows/post-tag.yaml b/.github/workflows/post-tag.yaml new file mode 100644 index 0000000..b2ea88e --- /dev/null +++ b/.github/workflows/post-tag.yaml @@ -0,0 +1,65 @@ +name: Publish Release +"on": + workflow_dispatch: + push: + tags: + - "*" + +permissions: + contents: read + checks: write + +jobs: + test_and_lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: gradle/gradle-build-action@v3 + - uses: open-policy-agent/setup-opa@v2 + - run: ./gradlew test lint checkstyleMain checkstyleTest jar + - run: ./scripts/check-version-mismatch.sh + + publish-maven-central: + name: Publish Java SDK to Maven Central + runs-on: ubuntu-latest + steps: + - name: Tune GitHub-hosted runner network + uses: smorimoto/tune-github-hosted-runner-network@v1 + - uses: actions/checkout@v5 + - name: Set up Java + uses: actions/setup-java@v5 + with: + java-version: "17" + distribution: "corretto" + cache: "gradle" + - name: Publish to Sonatype Central + run: |- + pwd + ./gradlew build sonatypeCentralUpload --no-daemon + env: + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_SIGNING_KEY: ${{ secrets.JAVA_GPG_SECRET_KEY }} + SIGNING_KEY_PASSPHRASE: ${{ secrets.JAVA_GPG_PASSPHRASE }} + + github-release: + name: Push Github Release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Tune GitHub-hosted runner network + uses: smorimoto/tune-github-hosted-runner-network@v1 + - name: Check out code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: gradle/gradle-build-action@v3 + - uses: open-policy-agent/setup-opa@v2 + - run: ./gradlew build jar + - name: Set TAG_NAME in Environment + # Subsequent steps will be have the computed tag name + run: echo "TAG_NAME=${GITHUB_REF##*/}" >> $GITHUB_ENV + - name: Create or Update Release + env: + # Required for the GitHub CLI + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./scripts/github-release.sh --asset-dir=build/libs --tag=${TAG_NAME} \ No newline at end of file diff --git a/.github/workflows/publish_to_maven.yaml b/.github/workflows/publish_to_maven.yaml deleted file mode 100644 index dc89b9a..0000000 --- a/.github/workflows/publish_to_maven.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: "Publish to Maven Central" -"on": - workflow_dispatch: - push: - branches: - - main - paths: - - CHANGELOG.md - -permissions: - contents: read - checks: write - -jobs: - publish: - name: Publish Java SDK - runs-on: ubuntu-latest - steps: - - name: Tune GitHub-hosted runner network - uses: smorimoto/tune-github-hosted-runner-network@v1 - - uses: actions/checkout@v5 - - run: ./scripts/is_release.sh >> $GITHUB_OUTPUT - id: build - - name: Set up Java - uses: actions/setup-java@v5 - with: - java-version: "17" - distribution: "corretto" - cache: "gradle" - if: ${{ steps.build.outputs.release == 'true' }} - - uses: gradle/actions/setup-gradle@v5 - if: ${{ steps.build.outputs.release == 'true' }} - - name: Publish to Sonatype Central - run: |- - pwd - ./gradlew build sonatypeCentralUpload --no-daemon - env: - SONATYPE_USERNAME: ${{ secrets.sonatype_username }} - SONATYPE_PASSWORD: ${{ secrets.sonatype_password }} - SONATYPE_SIGNING_KEY: ${{ secrets.java_gpg_secret_key }} - SIGNING_KEY_PASSPHRASE: ${{ secrets.java_gpg_passphrase }} - if: ${{ steps.build.outputs.release == 'true' }} - - uses: ravsamhq/notify-slack-action@v2 - if: ${{ steps.build.outputs.release == 'true' && always() && env.SLACK_WEBHOOK_URL != '' }} - with: - status: ${{ job.status }} - token: ${{ secrets.github_access_token }} - notify_when: "failure" - notification_title: "Failed to Publish Maven Central Release" - message_format: "{emoji} *{workflow}* {status_message} in <{repo_url}|{repo}>" - footer: "Linked Repo <{repo_url}|{repo}> | <{run_url}|View Run>" - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml new file mode 100644 index 0000000..509a4e0 --- /dev/null +++ b/.github/workflows/pull-request.yaml @@ -0,0 +1,118 @@ +name: PR Checks + +on: + workflow_dispatch: {} + pull_request: {} + + +permissions: + contents: read + checks: write + +# When a new revision is pushed to a PR, cancel all in-progress CI runs for that +# PR. See https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + release-check: + name: Release version bump check + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Tune GitHub-hosted runner network + uses: smorimoto/tune-github-hosted-runner-network@v1 + - uses: actions/checkout@v5 + with: + fetch-depth: 0 # Fetch full history to access all commits + - name: Check for release commits + id: check-release + run: | + RESULT=$(./scripts/get-release-from-commits.sh "origin/${{ github.base_ref }}" "origin/${{ github.head_ref }}") + echo "Release commits detected: $RESULT" + if [ -n $RESULT ]; then + echo "result=true" >> $GITHUB_OUTPUT + else + echo "result=false" >> $GITHUB_OUTPUT + fi + - name: Post or update warning comment (release commit detected) + if: steps.check-release.outputs.result == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + MARKER: "" + run: | + # Get latest version from CHANGELOG.md: + CHANGELOG_VERSION=$(grep -o -E '## [0-9.]+' CHANGELOG.md | head -n 1 | sed 's/## //') + + # Get version from main csproj: + GRADLE_VERSION=$(./gradlew properties | grep "^version:" | cut -d' ' -f2) + + # Release commit version: + COMMIT_VERSION=$(./scripts/get-release-from-commits.sh "origin/${{ github.base_ref }}" "origin/${{ github.head_ref }}") + + COMMENT=$(mktemp) + echo "ℹ️ **Release Commit Detected**" >> "$COMMENT" + echo "" >> "$COMMENT" + echo "This PR contains commit(s) that match the case-insensitive regex \`^Release .*\`" >> "$COMMENT" + echo "" >> "$COMMENT" + echo "Here are the latest versions reported from the places the release workflows will use:" >> "$COMMENT" + echo "" >> "$COMMENT" + echo "| Source | Version |" >> "$COMMENT" + echo "| --- | --- |" >> "$COMMENT" + echo "| Commit matching \`^Release .*\` | $COMMIT_VERSION |" >> "$COMMENT" + echo "| \`CHANGELOG.md\` | $CHANGELOG_VERSION |" >> "$COMMENT" + echo "| \`gradle properties\` project version (from \`gradle.properties\`) | $GRADLE_VERSION |" >> "$COMMENT" + + echo "Posting or updating release warning comment." + export COMMENT_BODY="$(cat "$COMMENT")" + ./scripts/release-pr-comment.sh "$REPO" "$PR_NUMBER" "$MARKER" + - name: Update warning comment if present (no release commit) + if: steps.check-release.outputs.result != 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + MARKER: "" + COMMENT_BODY: | + ℹ️ A release commit was detected in earlier versions of this PR. + + The current PR commits do not appear to be a release. + run: | + EXISTING_COMMENT=$(gh api repos/$REPO/issues/$PR_NUMBER/comments \ + --jq ".[] | select(.body | contains(\"$MARKER\")) | .id" | head -1) || { + echo "Error: Failed to fetch existing comments" >&2 + exit 2 + } + + if [ -n "$EXISTING_COMMENT" ]; then + echo "Updating release warning comment." + ./scripts/release-pr-comment.sh "$REPO" "$PR_NUMBER" "$MARKER" + else + echo "No release warning comment found." + fi + + test_and_lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Pre-pull container image + run: docker pull ghcr.io/open-policy-agent/eopa:latest + - uses: gradle/gradle-build-action@v3 + - run: ./gradlew test lint checkstyleMain checkstyleTest jar + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: success() || failure() # always run even if the previous step fails + with: + report_paths: "**/build/test-results/test/TEST-*.xml" + - name: Publish Checkstyle report + uses: Juuxel/publish-checkstyle-report@v2 + if: ${{ failure() || success() }} + with: + # required: The glob paths to report XML files as a multiline string + # The format below works for the Gradle Checkstyle plugin with default configurations + reports: | + build/reports/checkstyle/*.xml \ No newline at end of file diff --git a/.github/workflows/test_and_lint.yaml b/.github/workflows/test_and_lint.yaml deleted file mode 100644 index 59a174c..0000000 --- a/.github/workflows/test_and_lint.yaml +++ /dev/null @@ -1,36 +0,0 @@ -name: "Run unit tests and lint Java code" -"on": - workflow_dispatch: - pull_request: - -permissions: - contents: read - checks: write - -jobs: - test_and_lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Set up Java - uses: actions/setup-java@v5 - with: - java-version: "17" - distribution: "corretto" - cache: "gradle" - - uses: gradle/actions/setup-gradle@v5 - - run: ./gradlew test lint checkstyleMain checkstyleTest jar - - run: ls -al build/libs - - name: Publish Test Report - uses: mikepenz/action-junit-report@v5 - if: success() || failure() # always run even if the previous step fails - with: - report_paths: "**/build/test-results/test/TEST-*.xml" - - name: Publish Checkstyle report - uses: Juuxel/publish-checkstyle-report@v2 - if: ${{ failure() || success() }} - with: - # required: The glob paths to report XML files as a multiline string - # The format below works for the Gradle Checkstyle plugin with default configurations - reports: | - build/reports/checkstyle/*.xml diff --git a/scripts/get-release-from-commits.sh b/scripts/get-release-from-commits.sh new file mode 100755 index 0000000..633480a --- /dev/null +++ b/scripts/get-release-from-commits.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Script to extract version from release commits for GH Actions checks. +# Note: Remember to include the "origin/" prefix for remote branches! +# Otherwise, checks will be done against local branches of matching names. +# Usage: get-release-from-commits.sh [pattern] + +set -x +set -e # Exit on any error + +# Check args. +if [ $# -lt 2 ] || [ $# -gt 3 ]; then + echo "Usage: $0 [pattern]" + echo "" + echo "Arguments:" + echo " base_branch Base branch to compare against" + echo " head_branch Head branch to check" + echo " pattern Pattern to search for (default: '^Release [v]?[0-9.]+')" + echo "" + echo "Output: '' if release commits found, '' otherwise" + echo "" + echo "Example (local branch): $0 origin/main user/feature" + echo "Example (remote branch): $0 origin/main origin/user/feature" + exit 1 +fi + +BASE_BRANCH="$1" +HEAD_BRANCH="$2" +PATTERN="${3:-^Release v?[0-9.]+}" + +# Check if we're in a git repository. +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo "Error: Not in a git repository" >&2 + exit 2 +fi + +# Get commit messages from the range. +COMMITS=$(git log --oneline $BASE_BRANCH..$HEAD_BRANCH --pretty=format:"%s" 2>/dev/null) || { + echo "Error: Failed to get commit messages. Check if branches exist." >&2 + exit 2 +} + +# If no commits, return empty. +if [ -z "$COMMITS" ]; then + exit 0 +fi + +# Check for release pattern and extract version. +VERSION=$(echo "$COMMITS" | grep -E -i "$PATTERN" | head -n 1 | grep -E -o '[0-9.]+') || { + echo "No matches found." + exit 0 +} + +if [ -n "$VERSION" ]; then + echo "$VERSION" +fi \ No newline at end of file diff --git a/scripts/github-release.sh b/scripts/github-release.sh new file mode 100755 index 0000000..6033691 --- /dev/null +++ b/scripts/github-release.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Script to draft and edit GitHub releases. Assumes execution environment is Github Action runner. + +set -x + +usage() { + echo "github-release.sh [--asset-dir=] [--tag=]" + echo " Default --asset-dir is $PWD and --tag $TAG_NAME " +} + +TAG_NAME=${TAG_NAME} +ASSET_DIR=${PWD:-"./"} +REPO="open-policy-agent/opa-java" + +for i in "$@"; do + case $i in + --asset-dir=*) + ASSET_DIR="${i#*=}" + shift + ;; + --tag=*) + TAG_NAME="${i#*=}" + shift + ;; + *) + usage + exit 1 + ;; + esac +done + +# Collect a list of artifacts (expect binaries in the form: artifacts__[extension]) +ASSETS=() +for asset in "${ASSET_DIR}"/*.jar; do + ASSETS+=("$asset") +done + +# Gather the release notes from the CHANGELOG for the latest version +RELEASE_NOTES="release-notes.md" + +# The hub CLI expects the first line to be the title +echo -e "${TAG_NAME}\n" > "${RELEASE_NOTES}" + +# Fill in the description +./scripts/latest-release-notes.sh --output="${RELEASE_NOTES}" + +# Update or create a release on github +if gh release view "${TAG_NAME}" --repo $REPO > /dev/null; then + # Occurs when the tag is created via GitHub UI w/ a release + gh release upload "${TAG_NAME}" "${ASSETS[@]}" --repo $REPO +else + # Create a draft release + gh release create "${TAG_NAME}" "${ASSETS[@]}" -F ${RELEASE_NOTES} --draft --title "${TAG_NAME}" --repo $REPO +fi diff --git a/scripts/latest-release-notes.sh b/scripts/latest-release-notes.sh new file mode 100755 index 0000000..457ff4c --- /dev/null +++ b/scripts/latest-release-notes.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -e + +BASE_DIR=$(dirname "${BASH_SOURCE}")/.. +CHANGELOG="${BASE_DIR}/CHANGELOG.md" + +usage() { + echo "latest-release-notes.sh --output=" +} + +OUTPUT="" + +for i in "$@"; do + case $i in + --output=*) + OUTPUT="${i#*=}" + shift + ;; + *) + usage + exit 1 + ;; + esac +done + +if [ -z "${OUTPUT}" ]; then + usage + exit 1 +fi + +# Versions start with a h2 (## ), find the latest two for start and stop +# positions in the CHANGELOG +LATEST_VERSION=$(grep '## [0-9]' "${CHANGELOG}" | head -n 1) +STOP_VERSION=$(grep '## [0-9]' "${CHANGELOG}" | head -n 2 | tail -n 1) + +STARTED=false + +while IFS= read -r line +do + # Skip lines until the first version header is found + if [[ "${STARTED}" == false ]]; then + if [[ "${line}" == "${LATEST_VERSION}" ]]; then + STARTED=true + fi + continue + fi + + # Stop reading after we see the stopping point + if [[ "${line}" == "${STOP_VERSION}" ]]; then + break + fi + + # Append each line between the two onto the release notes + echo -e "${line}" >> "${OUTPUT}" + +done < "${CHANGELOG}" + +# Delete all leading blank lines at top of file +sed -i.bak '/./,$!d' "${OUTPUT}" +rm "${OUTPUT}.bak" diff --git a/scripts/release-pr-comment.sh b/scripts/release-pr-comment.sh new file mode 100755 index 0000000..c24a429 --- /dev/null +++ b/scripts/release-pr-comment.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Script to create or update a PR comment with a unique marker +# Usage: release-pr-comment.sh [marker] + +set -e # Exit on any error + +# Check arguments +if [ $# -lt 3 ] || [ $# -gt 4 ]; then + echo "Usage: $0 [marker]" + echo " or: echo 'comment body' | $0 [marker]" + echo " or: COMMENT_BODY='comment body' $0 [marker]" + echo "" + echo "Arguments:" + echo " repo Repository in format 'owner/repo'" + echo " pr_number Pull request number" + echo " marker Unique HTML comment marker (default: '')" + echo "" + echo "Comment body source (in order of precedence):" + echo " 1. COMMENT_BODY environment variable" + echo " 2. stdin (if not a terminal)" + echo "" + echo "Environment variables:" + echo " COMMENT_BODY Comment body text (optional)" + echo " GH_TOKEN GitHub token for authentication" + echo "" + echo "The script will update existing comments with the same marker or create a new one." + exit 1 +fi + +REPO="$1" +PR_NUMBER="$2" +MARKER="${3:-}" + +echo "DEBUG: $COMMENT_BODY" + +# Get comment body from environment variable or stdin +if [ -n "$COMMENT_BODY" ]; then + echo "Using comment body from COMMENT_BODY environment variable" >&2 +elif [ ! -t 0 ]; then + # stdin is not a terminal (has data piped to it) + echo "Reading comment body from stdin" >&2 + COMMENT_BODY=$(cat) +else + echo "Error: No comment body provided. Set COMMENT_BODY environment variable or pipe content to stdin." >&2 + exit 1 +fi + +# Check if comment body is empty +if [ -z "$COMMENT_BODY" ]; then + echo "Error: Comment body is empty" >&2 + exit 1 +fi + +# Check if gh CLI is available +if ! command -v gh &> /dev/null; then + echo "Error: gh CLI is not available" >&2 + exit 2 +fi + +# Check if GH_TOKEN is set (gh CLI will handle the actual auth) +if [ -z "$GH_TOKEN" ]; then + echo "Warning: GH_TOKEN environment variable not set" >&2 +fi + +# Add marker and timestamp to comment body +TIMESTAMPED_BODY="$MARKER +$COMMENT_BODY + +_Last updated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')_" + +# Look for existing comment with our marker +echo "Checking for existing comment with marker..." >&2 +EXISTING_COMMENT=$(gh api repos/$REPO/issues/$PR_NUMBER/comments \ + --jq ".[] | select(.body | contains(\"$MARKER\")) | .id" | head -1) || { + echo "Error: Failed to fetch existing comments" >&2 + exit 2 +} + +if [ -n "$EXISTING_COMMENT" ]; then + echo "Updating existing comment ID: $EXISTING_COMMENT" >&2 + gh api repos/$REPO/issues/comments/$EXISTING_COMMENT \ + --method PATCH \ + --field body="$TIMESTAMPED_BODY" || { + echo "Error: Failed to update comment" >&2 + exit 2 + } + echo "Comment updated successfully" >&2 +else + echo "Creating new comment" >&2 + gh api repos/$REPO/issues/$PR_NUMBER/comments \ + --method POST \ + --field body="$TIMESTAMPED_BODY" || { + echo "Error: Failed to create comment" >&2 + exit 2 + } + echo "Comment created successfully" >&2 +fi \ No newline at end of file