diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..d95216c3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,244 @@ +name: Release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + target: + description: "production publishes to maven.countly.com; test is a dry run against maven-test.countly.com" + type: choice + options: [production, test] + default: production + +permissions: {} + +concurrency: + group: maven-publish + cancel-in-progress: false + queue: max + +env: + TAG: ${{ github.ref_name }} + TARGET: ${{ inputs.target || 'production' }} + +jobs: + plan: + name: Plan and check + runs-on: ubuntu-24.04 + permissions: + contents: read + checks: read + outputs: + environment: ${{ steps.plan.outputs.environment }} + check_tasks: ${{ steps.plan.outputs.check_tasks }} + publish_tasks: ${{ steps.plan.outputs.publish_tasks }} + published_modules: ${{ steps.plan.outputs.published_modules }} + steps: + - name: Only tags can be released + if: github.ref_type != 'tag' + run: | + echo "::error::Run the release workflow on a tag, not on a branch." + exit 1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: Read the GitHub release + id: release + if: env.TARGET == 'production' + env: + GH_TOKEN: ${{ github.token }} + run: | + prerelease=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isPrerelease --jq .isPrerelease) + echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" + - name: Plan + id: plan + run: python3 .github/release/release.py plan --tag "$TAG" --target "$TARGET" --commit "$GITHUB_SHA" --prerelease "${{ steps.release.outputs.prerelease || 'unknown' }}" --out plan.json + - name: The tag commit is on the release branch + if: env.TARGET == 'production' + run: python3 .github/release/release.py check-branch --plan plan.json --commit "$GITHUB_SHA" + - name: Version numbers match the tag + run: python3 .github/release/release.py check-sources --plan plan.json + - name: The version is not published yet + run: python3 .github/release/release.py check-absent --plan plan.json --target "$TARGET" --nocache "${{ github.run_id }}" + - name: Required checks passed on the tag commit + if: env.TARGET == 'production' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/check-runs?per_page=100" > check-runs.json + python3 .github/release/release.py check-required --check-runs check-runs.json + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: plan + path: plan.json + retention-days: 7 + if-no-files-found: error + + build: + name: Build and check + needs: plan + runs-on: ubuntu-24.04 + # A hung test or clean-project build would otherwise hold the release queue for GitHub's 6-hour default. + timeout-minutes: 60 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: plan + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 + with: + distribution: corretto + java-version: | + 8 + 17 + - name: Record the JDK and Maven + run: | + echo '### JDK, Java 8 runtime and Maven' >> "$GITHUB_STEP_SUMMARY" + { java -version; "$JAVA_HOME_8_X64/bin/java" -version; mvn -v; } 2>&1 | sed 's/^/ /' >> "$GITHUB_STEP_SUMMARY" + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + with: + cache-disabled: true + - name: Make sure a virtual display for the JavaFX tests exists + run: command -v xvfb-run > /dev/null || { sudo apt-get update -qq && sudo apt-get install -y -qq xvfb; } + - name: Test and check the modules + run: xvfb-run -a ./gradlew --no-daemon --no-build-cache --no-configuration-cache ${{ needs.plan.outputs.check_tasks }} + - name: Publish into the staging folder + run: ./gradlew --no-daemon --no-build-cache --no-configuration-cache ${{ needs.plan.outputs.publish_tasks }} -PRELEASE_SIGNING_ENABLED=false + - name: Stage the release and check its files + run: python3 .github/release/release.py stage --plan plan.json --staging build/release-staging --commit "$GITHUB_SHA" --target "$TARGET" --manifest release-manifest.json + - name: Check the SBOMs against the CycloneDX 1.6 schema + run: | + curl -sSfL -o "$RUNNER_TEMP/cyclonedx" https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.33.1/cyclonedx-linux-x64 + echo "bfc8b2538da86fe239bc53658bbb63c1c8c510a293c1e6891aa5bea5d3c58746 $RUNNER_TEMP/cyclonedx" | sha256sum -c - + chmod +x "$RUNNER_TEMP/cyclonedx" + python3 .github/release/release.py validate-sbom --plan plan.json --staging build/release-staging --cli "$RUNNER_TEMP/cyclonedx" + - name: Check what the release promises integrators + run: python3 .github/release/release.py contract --plan plan.json --staging build/release-staging + - name: Clean projects build against the staged release + run: python3 .github/release/release.py consumer --plan plan.json --staging build/release-staging --java8-home "$JAVA_HOME_8_X64" --work "$RUNNER_TEMP/consumers" + - name: Scan the published dependencies + env: + OSV_FAIL_ON: high + OSV_BLOCKING_SCOPES: published + run: | + ./gradlew -q --no-daemon --no-configuration-cache --init-script .github/scripts/dependency-report.init.gradle "-DpublishedModules=${{ needs.plan.outputs.published_modules }}" printResolvedDependencies > resolved-dependencies.txt + python3 .github/scripts/osv_scan.py < resolved-dependencies.txt + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: staged-release + path: | + build/release-staging + release-manifest.json + retention-days: 7 + if-no-files-found: error + + publish: + name: Approve, sign and upload + needs: [plan, build] + runs-on: ubuntu-24.04 + environment: ${{ needs.plan.outputs.environment }} + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: plan + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: staged-release + - name: The files are exactly what the build produced + run: python3 .github/release/release.py verify-manifest --manifest release-manifest.json --staging build/release-staging + - name: Sign and check the signatures + env: + GNUPGHOME: ${{ runner.temp }}/gnupg + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_KEY_PASSPHRASE: ${{ secrets.SIGNING_KEY_PASSPHRASE }} + run: | + mkdir -m 700 "$GNUPGHOME" + printf '%s\n' "$SIGNING_KEY" | gpg --batch --import + python3 .github/release/release.py sign --staging build/release-staging --target "$TARGET" --public-key-out signing-public-key.asc + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: signing-public-key + path: signing-public-key.asc + retention-days: 7 + overwrite: true + if-no-files-found: error + - name: Upload to R2 and rewrite the index + env: + AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + R2_ACCOUNT_ID: ${{ vars.R2_ACCOUNT_ID }} + R2_BUCKET: ${{ vars.R2_BUCKET }} + run: | + python3 .github/release/release.py upload --plan plan.json --staging build/release-staging + python3 .github/release/release.py index --plan plan.json + - name: Remove the signing key + if: always() + env: + GNUPGHOME: ${{ runner.temp }}/gnupg + run: | + gpgconf --kill gpg-agent || true + rm -rf "$GNUPGHOME" + + verify: + name: Verify from the public address + needs: publish + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: plan + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: staged-release + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: signing-public-key + - name: Files, signatures and index + run: python3 .github/release/release.py verify-public --plan plan.json --manifest release-manifest.json --target "$TARGET" --nocache "${{ github.run_id }}-${{ github.run_attempt }}" --work "${{ runner.temp }}/downloaded" --public-key signing-public-key.asc + + announce: + name: Announce + needs: verify + if: github.event_name == 'release' && !github.event.release.prerelease + runs-on: ubuntu-24.04 + permissions: {} + steps: + - name: Slack + uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844 # v1.23.0 + with: + payload: | + { + "repository": "${{ github.repository }}", + "tag_name": "${{ github.event.release.tag_name }}", + "actor": "${{ github.actor }}", + "body": ${{ toJSON(github.event.release.body) }}, + "html_url": "${{ github.event.release.html_url }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_RELEASE }} + - name: Discord + uses: sarisia/actions-status-discord@9904e3130b8905d5b973df25623f17672dcb3466 # v1.13.0 + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} + nodetail: true + title: New ${{ github.repository }} version ${{ github.event.release.tag_name }} published by ${{ github.actor }} + description: | + Release URL: ${{ github.event.release.html_url }} + Click [here](https://github.com/Countly/countly-server/blob/master/CHANGELOG.md) to view the change log. + `${{ github.event.release.body }}`