diff --git a/.buildkite/release.yml b/.buildkite/release.yml new file mode 100644 index 0000000000..7f51edc121 --- /dev/null +++ b/.buildkite/release.yml @@ -0,0 +1,14 @@ +agents: + provider: "gcp" + +steps: + - label: "Run the snapshot" + key: "release" + commands: .ci/release.sh + artifact_paths: + - "release.txt" + - "**/target/*" + +notify: + - slack: "#apm-agent-java" + if: 'build.state != "passed"' diff --git a/.ci/ReleaseChangelog.java b/.ci/ReleaseChangelog.java new file mode 100644 index 0000000000..8d6092e0e1 --- /dev/null +++ b/.ci/ReleaseChangelog.java @@ -0,0 +1,126 @@ +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; +import java.util.function.Predicate; + +public class ReleaseChangelog { + + public static void main(String[] args) throws IOException { + if (args.length != 2) { + System.out.println("Expected exactly two arguments: "); + System.exit(-1); + } + Path fileName = Paths.get(args[0]); + String version = args[1].trim(); + if (!version.matches("\\d+\\.\\d+\\.\\d+")) { + System.out.println("Version must be in the format x.x.x but was not: " + version); + System.exit(-1); + } + Lines f = new Lines(Files.readAllLines(fileName, StandardCharsets.UTF_8)); + int unreleasedStart = f.findLine(str -> str.trim().equals("=== Unreleased"), 0).orElseThrow() + 1; + int unreleasedEnd = f.findLine(str -> str.startsWith("[[release-notes-"), unreleasedStart).orElseThrow(); + + Lines changes = f.cut(unreleasedStart, unreleasedEnd); + f.insert(new Lines(List.of("")), unreleasedStart); //add a blank line below unreleased heading + changes.trim(); + + // a few sanity checks + if (changes.lineCount() == 0) { + System.out.println("Unreleased changes are empty, there must be something wrong!"); + System.exit(-1); + } + OptionalInt wrongIndentedHeading = changes.findLine(str -> str.matches("^==?=?=?[^=].*"), 0); + if (wrongIndentedHeading.isPresent()) { + System.out.println("Found heading which is too high level (must be at least =====) within changes: " + + changes.getLine(wrongIndentedHeading.getAsInt())); + System.exit(-1); + } + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd") + .withZone(ZoneOffset.UTC); + + changes.insert(new Lines(List.of( + "", + String.format("[[release-notes-%s]]", version), + String.format("==== %s - %s", version, formatter.format(Instant.now())), + "" + )), 0); + + int majorVersion = Integer.parseInt(version.split("\\.")[0]); + String majorHeading = String.format("=== Java Agent version %d.x", majorVersion); + OptionalInt sectionStart = f.findLine(str -> str.trim().equals(majorHeading), 0); + if (sectionStart.isEmpty()) { + System.out.println("Could not find heading for major version: " + majorHeading); + System.out.println("Is this a new major version? If yes, please add an empty section for it manually to the Changelog"); + System.exit(-1); + } + + f.insert(changes, sectionStart.getAsInt() + 1); + + Files.writeString(fileName, f.toString(), StandardCharsets.UTF_8); + } + + static class Lines { + + private final List lines; + + public Lines(List lines) { + this.lines = new ArrayList<>(lines); + } + + int lineCount() { + return lines.size(); + } + + OptionalInt findLine(Predicate condition, int startAt) { + for (int i = startAt; i < lines.size(); i++) { + if (condition.test(lines.get(i))) { + return OptionalInt.of(i); + } + } + return OptionalInt.empty(); + } + + Lines cut(int startInclusive, int endExclusive) { + List cutLines = new ArrayList<>(); + for (int i = startInclusive; i < endExclusive; i++) { + cutLines.add(lines.remove(startInclusive)); + } + return new Lines(cutLines); + } + + void insert(Lines other, int insertAt) { + this.lines.addAll(insertAt, other.lines); + } + + /** + * Trims lines consisting of only blanks at the top and bottom + */ + void trim() { + while (!lines.isEmpty() && lines.get(0).matches("\\s*")) { + lines.remove(0); + } + while (!lines.isEmpty() && lines.get(lines.size() - 1).matches("\\s*")) { + lines.remove(lines.size() - 1); + } + } + + @Override + public String toString() { + return String.join("\n", lines); + } + + public String getLine(int number) { + return lines.get(number); + } + } + +} diff --git a/.ci/release.sh b/.ci/release.sh new file mode 100755 index 0000000000..f8e5dd73d4 --- /dev/null +++ b/.ci/release.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +## This script runs the release given the different environment variables +## dry_run +## +## It relies on the .buildkite/hooks/pre-command so the Vault and other tooling +## are prepared automatically by buildkite. +## + +set -eo pipefail + +# Make sure we delete this folder before leaving even in case of failure +clean_up () { + ARG=$? + export VAULT_TOKEN=$PREVIOUS_VAULT_TOKEN + echo "--- Deleting tmp workspace" + rm -rf $TMP_WORKSPACE + exit $ARG +} +trap clean_up EXIT + +echo "--- Debug JDK installation :coffee:" +echo $JAVA_HOME +echo $PATH +java -version + +set +x +echo "--- Deploy the release :package:" +if [[ "$dry_run" == "true" ]] ; then + ./mvnw clean install -DskipTests=true -Dmaven.javadoc.skip=true | tee release.txt +else + echo "hello" | tee release.txt + # ./mvnw -V -s .ci/settings.xml -Pgpg clean deploy -DskipTests --batch-mode | tee release.txt +fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..a3df1e3f58 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,212 @@ +--- +# Releases the agent +name: release + +on: + workflow_dispatch: + inputs: + branch: + description: 'The branch to release' + required: true + default: 'main' + version: + description: 'The version to release (e.g. 1.2.3). This workflow will automatically perform the required version bumps' + required: true + update_changelog: + description: | + If enabled, everything in the changelog from the "Unreleased" section will be automatically moved to a new section for the new release. + If disabled, the changelog needs to be prepared for the release manually before triggering this workflow. + type: boolean + required: true + default: true + +env: + JAVA_VERSION: 17 + JAVA_DIST: adopt + +permissions: + contents: write + +jobs: + prepare_release: + permissions: + contents: write + name: "Changelog and Version Bump" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ inputs.branch }} + - uses: elastic/apm-pipeline-library/.github/actions/setup-git@current + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v3 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DIST }} + cache: 'maven' + - name: Prepare changelog for release + if: ${{ inputs.update_changelog }} + run: | + java .ci/ReleaseChangelog.java CHANGELOG.asciidoc ${{ inputs.version }} + git commit -m "Prepare changelog for release ${{ inputs.version }}" CHANGELOG.asciidoc + - name: Bump version and add git tag + run: ./mvnw release:prepare -B -DpushChanges=false "-Darguments=-DskipTests -Dmaven.javadoc.skip=true" -DreleaseVersion=${{ inputs.version }} + - run: git push --atomic origin ${{ inputs.branch }} v${{ inputs.version }} + + + maven_central_deploy: + name: "Deploy to Maven Central (Buildkite)" + runs-on: ubuntu-latest + needs: + - prepare_release + steps: + - id: buildkite + name: Run Deploy + uses: reakaleek/apm-pipeline-library/.github/actions/buildkite@feature/buildkite-download-artifacts + with: + vaultUrl: ${{ secrets.VAULT_ADDR }} + vaultRoleId: ${{ secrets.VAULT_ROLE_ID }} + vaultSecretId: ${{ secrets.VAULT_SECRET_ID }} + pipeline: apm-agent-java-release + pipelineVersion: ${{ inputs.version }} + waitFor: true + printBuildLogs: true + artifactName: lambda-zip + artifactPath: "elastic-apm-agent/target/**/elastic-apm-java-aws-lambda-layer-*.zip" + buildEnvVars: | + dry_run=true +# buildEnvVars: | +# dry_run=${{ inputs.dry_run || 'false' }} + +# await_artifact_on_maven_central: +# name: "Wait for artifacts to be available on maven central" +# runs-on: ubuntu-latest +# needs: +# - maven_central_deploy +# steps: +# - uses: actions/checkout@v3 +# - shell: bash +# run: | +# until .ci/release/wait_maven_artifact_published.sh ${{ inputs.version }} +# do +# echo "Artifacts not found on maven central. Sleeping 30 seconds, retrying afterwards" +# sleep 30s +# done +# + +# update_major_branch: +# name: "Update Major Branch" +# runs-on: ubuntu-latest +# needs: +# - await_artifact_on_maven_central +# steps: +# - uses: actions/checkout@v3 +# with: +# ref: ${{ inputs.branch }} +# - uses: elastic/apm-pipeline-library/.github/actions/setup-git@current +# - run: .ci/release/update_major_branch.sh ${{ inputs.version }} +# - run: git push -f origin "$(echo '${{ inputs.version }}' | sed -E 's/\..+/.x/')" + +# update_cloudfoundry: +# name: "Update Cloudfoundry" +# runs-on: ubuntu-latest +# needs: +# - await_artifact_on_maven_central +# steps: +# - uses: actions/checkout@v3 +# with: +# ref: ${{ inputs.branch }} +# - uses: elastic/apm-pipeline-library/.github/actions/setup-git@current +# - name: "Update Cloudfoundry index.yml file" +# shell: bash +# run: .ci/release/update_cloudfoundry.sh ${{ inputs.version }} +# - run: git push origin ${{ inputs.branch }} + + +# build_docker_images: +# name: "Build and push docker images" +# runs-on: ubuntu-latest +# needs: +# - await_artifact_on_maven_central +# env: +# TAG_NAME: v${{ inputs.version }} +# SONATYPE_FALLBACK: 1 +# steps: +# - uses: actions/checkout@v3 +# with: +# ref: ${{ inputs.branch }} +# fetch-depth: 0 # Load entire history as it is required for the push-script +# - uses: elastic/apm-pipeline-library/.github/actions/docker-login@current +# with: +# registry: docker.elastic.co +# secret: secret/apm-team/ci/docker-registry/prod +# url: ${{ secrets.VAULT_ADDR }} +# roleId: ${{ secrets.VAULT_ROLE_ID }} +# secretId: ${{ secrets.VAULT_SECRET_ID }} +# - name: "Build docker image" +# shell: bash +# run: | +# ./scripts/jenkins/build_docker.sh +# ./scripts/jenkins/push_docker.sh + +# publish_aws_lambda: +# name: "Publish AWS Lambda" +# runs-on: ubuntu-latest +# needs: +# - await_artifact_on_maven_central +# outputs: +# arn_content: ${{ steps.arn_output.outputs.arn_content }} +# steps: +# - uses: actions/checkout@v3 +# with: +# ref: ${{ inputs.branch }} +# # TODO: actual lambda creation + upload +# - name: Setup dummy ARN file +# run: | +# { +# echo "### ARNs of the APM Java Agent's AWS Lambda Layer" +# echo '' +# echo '|Region|ARN|' +# echo '|------|---|' +# } > ".ci/arn-file.md" +# - uses: actions/upload-artifact@v3 +# with: +# name: arn-file +# path: .ci/arn-file.md +# - name: Add ARN file to output +# id: arn_output +# run: | +# echo 'arn_content<> $GITHUB_OUTPUT +# cat .ci/arn-file.md >> $GITHUB_OUTPUT +# echo 'ARN_CONTENT_EOF' >> $GITHUB_OUTPUT +# + +# create_github_release: +# name: "Create GitHub Release" +# needs: +# - publish_aws_lambda +# - update_major_branch +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v3 +# with: +# ref: ${{ inputs.branch }} +# - name: Await release-notes published +# shell: bash +# run: | +# until .ci/release/wait_release_notes_published.sh ${{ inputs.version }} +# do +# echo "Release notes not published yet. Sleeping 30 seconds, retrying afterwards" +# sleep 30s +# done +# - name: Compute major.x branch +# id: get_dotx_branch +# run: echo "dotx_branch=$(echo '${{ inputs.version }}' | sed -E 's/\..+/.x/')" >> $GITHUB_OUTPUT +# - name: Create GitHub Release +# env: +# GH_TOKEN: ${{ github.token }} +# run: | +# gh release create v${{ inputs.version }} \ +# --title="Release ${{ inputs.version }}" \ +# --notes="[Release Notes for ${{ inputs.version }}](https://www.elastic.co/guide/en/apm/agent/java/current/release-notes-${{ steps.get_dotx_branch.outputs.dotx_branch }}.html#release-notes-${{ inputs.version }}) +# ${{ needs.publish_aws_lambda.outputs.arn_content }}" diff --git a/.github/workflows/tmp-test.yml b/.github/workflows/tmp-test.yml new file mode 100644 index 0000000000..41ee6fc38e --- /dev/null +++ b/.github/workflows/tmp-test.yml @@ -0,0 +1,23 @@ +name: tmp-test + +on: pull_request + +jobs: + tmp-test: + runs-on: ubuntu-latest + steps: + - id: buildkite + name: Run Deploy + uses: reakaleek/apm-pipeline-library/.github/actions/buildkite@feature/buildkite-download-artifacts + with: + vaultUrl: ${{ secrets.VAULT_ADDR }} + vaultRoleId: ${{ secrets.VAULT_ROLE_ID }} + vaultSecretId: ${{ secrets.VAULT_SECRET_ID }} + pipeline: apm-agent-java-release + pipelineVersion: ${{ github.head_ref }} + waitFor: true + printBuildLogs: false + artifactName: lambda-zip + artifactPath: "elastic-apm-agent/target/**/elastic-apm-java-aws-lambda-layer-*.zip" + buildEnvVars: | + dry_run=true diff --git a/scripts/jenkins/push_docker.sh b/scripts/jenkins/push_docker.sh index e6f26bd0ee..2953639ce9 100755 --- a/scripts/jenkins/push_docker.sh +++ b/scripts/jenkins/push_docker.sh @@ -48,7 +48,7 @@ if [ ${WORKERS+x} ] # We are on a CI worker then retry $RETRIES docker push $DOCKER_PUSH_IMAGE || echo "Push failed after $RETRIES retries" else # We are in a local (non-CI) environment - docker push $DOCKER_PUSH_IMAGE || echo "You may need to run 'docker login' first and then re-run this script" + docker push $DOCKER_PUSH_IMAGE || { echo "You may need to run 'docker login' first and then re-run this script"; exit 1; } fi readonly LATEST_TAG=$(git tag --list --sort=version:refname "v*" | grep -v RC | sed s/^v// | tail -n 1) @@ -62,6 +62,6 @@ then then retry $RETRIES docker push $DOCKER_PUSH_IMAGE_LATEST || echo "Push failed after $RETRIES retries" else # We are in a local (non-CI) environment - docker push $DOCKER_PUSH_IMAGE_LATEST || echo "You may need to run 'docker login' first and then re-run this script" + docker push $DOCKER_PUSH_IMAGE_LATEST || { echo "You may need to run 'docker login' first and then re-run this script"; exit 1; } fi fi