diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..8931a1bb608 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,52 @@ +version: 2.1 + +aliases: + - &jfrog-login + name: Rename jfrog environment variable for maven setting.xml + command: | + echo "export JFROG_USER=$ARTIFACTORY_USER" >> $BASH_ENV + echo "export JFROG_PASS=$ARTIFACTORY_PASSWORD" >> $BASH_ENV + - &post-build + name: Update deployment with OTP version + command: | + sudo apt-get update + sudo apt-get install libxml2-utils + chmod u+x .circleci/update_deployment_config + .circleci/update_deployment_config + +jobs: + build: + docker: + - image: cimg/openjdk:21.0-node + environment: + DEBIAN_FRONTEND: "noninteractive" + MAVEN_OPTS: -Xmx6G + resource_class: xlarge + steps: + - checkout + - restore_cache: + keys: + - dep-cache-{{ checksum "pom.xml" }} + # fallback to the most recent cache if there is no exact match for this pom.xml + - dep-cache- + - run: wget https://raw.githubusercontent.com/entur/circleci-toolbox-image-java11/master/tools/m2/settings.xml -O .circleci/settings.xml + - run: *jfrog-login + - run: mvn org.apache.maven.plugins:maven-dependency-plugin:3.1.0:go-offline -s .circleci/settings.xml + - save_cache: + paths: + - ~/.m2 + key: dep-cache-{{ checksum "pom.xml" }} + - run: mvn install -PprettierSkip org.apache.maven.plugins:maven-deploy-plugin:2.8.2:deploy -s .circleci/settings.xml -DaltDeploymentRepository=snapshots::default::https://entur2.jfrog.io/entur2/libs-release-local + - run: *post-build + +workflows: + version: 2 + release: + jobs: + - build: + name: build-release + context: global + filters: + branches: + only: + - otp2_entur_develop \ No newline at end of file diff --git a/.circleci/prepare_release b/.circleci/prepare_release new file mode 100755 index 00000000000..5b2ad0e23c4 --- /dev/null +++ b/.circleci/prepare_release @@ -0,0 +1,235 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ENTUR_DEVELOP=otp2_entur_develop +REMOTE_REPO="`git remote -v | grep "entur/OpenTripPlanner" | grep "push" | awk '{print $1;}'`" +STATUS_FILE=".prepare_release.tmp" +STATUS="" +DRY_RUN="" +OTP_BASE="" + +function main() { + setup "$@" + resumePreviousExecution + resetEnturDevelop + rebaseAndMergeExtBranch otp2_ext_config + rebaseAndMergeExtBranch otp2_ext_hack_sorlandsbanen + logSuccess +} + +function setup() { + if [[ $# -eq 2 && "$1" == "--dryRun" ]] ; then + DRY_RUN="--dryRun" + OTP_BASE="$2" + elif [[ $# -eq 1 ]] ; then + OTP_BASE="$1" + else + printHelp + exit 1 + fi + + echo "" + echo "Options: ${DRY_RUN}" + echo "Git base branch/commit: ${OTP_BASE}" + echo "Entur develop branch: ${ENTUR_DEVELOP}" + echo "Entur remote repo(pull/push): ${REMOTE_REPO}" + echo "" + + if git diff-index --quiet HEAD --; then + echo "" + echo "OK - No local changes, prepare to checkout '${ENTUR_DEVELOP}'" + echo "" + else + echo "" + echo "You have local modification, the script will abort. Nothing done!" + exit 2 + fi + + git fetch ${REMOTE_REPO} +} + +# This script create a status file '.prepare_release.tmp'. This file is used to resume the +# script in the same spot as where is left when the error occurred. This allow us to fix the +# problem (merge conflict or compile error) and re-run the script to complete the proses. +function resumePreviousExecution() { + readStatus + + if [[ -n "${STATUS}" ]] ; then + echo "" + echo "Resume: ${STATUS}?" + echo "" + echo " If all problems are resolved you may continue." + echo " Exit to clear status and start over." + echo "" + + ANSWER="" + while [[ ! "$ANSWER" =~ [yx] ]]; do + echo "Do you want to resume: [y:Yes, x:Exit]" + read ANSWER + done + + if [[ "${ANSWER}" == "x" ]] ; then + exit 0 + fi + fi +} + +function resetEnturDevelop() { + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## RESET '${ENTUR_DEVELOP}' TO '${OTP_BASE}'" + echo "## ------------------------------------------------------------------------------------- ##" + echo "" + echo "Would you like to reset the '${ENTUR_DEVELOP}' to '${OTP_BASE}'? " + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Checkout '${ENTUR_DEVELOP}'" + git checkout ${ENTUR_DEVELOP} + + echo "" + echo "Reset '${ENTUR_DEVELOP}' branch to '${OTP_BASE}' (hard)" + git reset --hard "${OTP_BASE}" + echo "" + fi +} + +function rebaseAndMergeExtBranch() { + EXT_BRANCH="$1" + EXT_STATUS_REBASE="Rebase '${EXT_BRANCH}'" + EXT_STATUS_COMPILE="Compile '${EXT_BRANCH}'" + + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## REBASE AND MERGE '${EXT_BRANCH}' INTO '${ENTUR_DEVELOP}'" + echo "## ------------------------------------------------------------------------------------- ##" + echo "" + echo "You are about to rebase and merge '${EXT_BRANCH}' into '${ENTUR_DEVELOP}'. Any local" + echo "modification in the '${EXT_BRANCH}' will be lost." + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Checkout '${EXT_BRANCH}'" + git checkout "${EXT_BRANCH}" + + echo "" + echo "Reset to '${REMOTE_REPO}/${EXT_BRANCH}'" + git reset --hard "${REMOTE_REPO}/${EXT_BRANCH}" + + echo "" + echo "Top 2 commits in '${EXT_BRANCH}'" + echo "-------------------------------------------------------------------------------------------" + git --no-pager log -2 + echo "-------------------------------------------------------------------------------------------" + echo "" + echo "You are about to rebase the TOP COMMIT ONLY(see above). Check that the " + echo "'${EXT_BRANCH}' only have ONE commit that you want to keep." + echo "" + + whatDoYouWant + + if [[ "${ANSWER}" == "y" ]] ; then + echo "" + echo "Rebase '${EXT_BRANCH}' onto '${ENTUR_DEVELOP}'" + setStatus "${EXT_STATUS_REBASE}" + git rebase --onto ${ENTUR_DEVELOP} HEAD~1 + fi + fi + + if [[ "${STATUS}" == "${EXT_STATUS_REBASE}" || "${STATUS}" == "${EXT_STATUS_COMPILE}" ]] ; then + # Reset status in case the test-compile fails. We need to do this because the status file + # is deleted after reading the status in the setup() function. + setStatus "${EXT_STATUS_COMPILE}" + + mvn clean test-compile + clearStatus + + echo "" + echo "Push '${EXT_BRANCH}'" + if [[ -z "${DRY_RUN}" ]] ; then + git push -f + else + echo "Skip: git push -f (--dryRun)" + fi + + echo "" + echo "Checkout '${ENTUR_DEVELOP}' and merge in '${EXT_BRANCH}'" + git checkout "${ENTUR_DEVELOP}" + git merge "${EXT_BRANCH}" + fi +} + +function logSuccess() { + echo "" + echo "## ------------------------------------------------------------------------------------- ##" + echo "## PREPARE RELEASE DONE -- SUCCESS" + echo "## ------------------------------------------------------------------------------------- ##" + echo " - '${REMOTE_REPO}/${ENTUR_DEVELOP}' reset to '${OTP_BASE}'" + echo " - 'otp2_ext_config' merged" + echo " - 'otp2_ext_hack_sorlandsbanen' merged" + echo "" + echo "" +} + +function whatDoYouWant() { + echo "" + ANSWER="" + + if [[ -n "${STATUS}" ]] ; then + # Skip until process is resumed + ANSWER="s" + else + while [[ ! "$ANSWER" =~ [ysx] ]]; do + echo "Do you want to continue: [y:Yes, s:Skip, x:Exit]" + read ANSWER + done + + if [[ "${ANSWER}" == "x" ]] ; then + exit 0 + fi + fi +} + +function setStatus() { + STATUS="$1" + echo "$STATUS" > "${STATUS_FILE}" +} + +function readStatus() { + if [[ -f "${STATUS_FILE}" ]] ; then + STATUS=`cat $STATUS_FILE` + rm "$STATUS_FILE" + else + STATUS="" + fi +} + +function clearStatus() { + STATUS="" + rm "${STATUS_FILE}" +} + +function printHelp() { + echo "" + echo "This script take ONE argument , the base **branch** or **commit** to use for the" + echo "release. The '${ENTUR_DEVELOP}' branch is reset to this commit and then the extension" + echo "branches is rebased onto that. The 'release' script is used to complete the release." + echo "It tag and push all changes to remote git repo." + echo "" + echo "Options:" + echo " --dryRun : Run script locally, nothing is pushed to remote server." + echo "" + echo "Usage:" + echo " $ .circleci/prepare_release otp/dev-2.x" + echo " $ .circleci/prepare_release --dryRun otp/dev-2.x" + echo "" +} + +main "$@" diff --git a/.circleci/release b/.circleci/release new file mode 100755 index 00000000000..9cac0d97958 --- /dev/null +++ b/.circleci/release @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +set -euo pipefail + +GIT_REMOTE_REPO="`git remote -v | grep "entur/OpenTripPlanner" | grep "push" | awk '{print $1;}'`" +MASTER_BRANCH=otp2_entur_develop +TAGS_FILE=target/git-entur-tags.txt +DRY_RUN="" + +function main() { + setup "$@" + listAllTags + mergeInOldReleaseWithNoChanges + setPomVersion + tagRelease + pushToRemote +} + +function setup() { + echo "" + echo "git fetch ${GIT_REMOTE_REPO}" + git fetch ${GIT_REMOTE_REPO} + + echo "Verify current branch is ${MASTER_BRANCH} " + git status | grep -q "On branch ${MASTER_BRANCH}" + + if [[ "${1+x}" == "--dryRun" ]] ; then + DRY_RUN="--dryRun" + fi +} + +function listAllTags() { + ## List all Entur tags to allow the UpdatePomVersion java program find the next version number + echo "" + echo "Dump all entur tags to ${TAGS_FILE}" + git tag -l | grep entur > ${TAGS_FILE} +} + +function setPomVersion() { + echo "" + echo "Update pom.xml with new version" + javac -d target/classes src/main/java/EnturUpdatePomVersion.java + + VERSION="`java -cp target/classes EnturUpdatePomVersion ${TAGS_FILE}`" + echo "" + echo "New version set: ${VERSION}" + echo "" + + ## Verify everything builds and tests run + echo "" + mvn clean test + + ## Add [ci skip] here before moving this to the CI server + echo "" + echo "Add and commit pom.xml" + git commit -m "Version ${VERSION}" pom.xml +} + +function mergeInOldReleaseWithNoChanges() { + echo "" + echo "Merge the old version of '${GIT_REMOTE_REPO}' into the new version. This only keep " + echo "a reference to the old version, the resulting tree of the merge is that of the new" + echo "branch head, effectively ignoring all changes from the old release." + git merge -s ours "${GIT_REMOTE_REPO}/${MASTER_BRANCH}" -m "Merge old release into '${MASTER_BRANCH}' - NO CHANGES COPIED OVER" +} + + +function tagRelease() { + echo "" + echo "Tag version ${VERSION}" + git tag -a v${VERSION} -m "Version ${VERSION}" +} + +function pushToRemote() { + echo "" + echo "Push pom.xml and new tag" + if [[ -z "${DRY_RUN}" ]] ; then + git push -f ${GIT_REMOTE_REPO} "v${VERSION}" ${MASTER_BRANCH} + else + echo "Skip: push -f ${GIT_REMOTE_REPO} "v${VERSION}" ${MASTER_BRANCH} (--dryRun)" + fi +} + +main "$@" diff --git a/.circleci/update_deployment_config b/.circleci/update_deployment_config new file mode 100755 index 00000000000..64550f19797 --- /dev/null +++ b/.circleci/update_deployment_config @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +## - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ## +## This script run AFTER OTP is build on the CI Server. It checkout the +## deployment config and update the version number, commit and push. This +## trigger the deployment config build witch create and deploy a new OTP2 +## docker image. +## +## Note! There is no need to run this script locally, unless you want to debug it. +## - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ## + + +set -euo pipefail + +OTP_DEPLOYMENT_CONFIG=target/otp-deployment-config +DOCKER_FILE=Dockerfile + +VERSION=$(xmllint --xpath "//*[local-name()='project']/*[local-name()='version']/text()" pom.xml) + +echo "Checkout otp-deplyment-config in ${OTP_DEPLOYMENT_CONFIG}" +git clone -n https://github.com/entur/otp-deployment-config.git --depth 1 ${OTP_DEPLOYMENT_CONFIG} + +pushd ${OTP_DEPLOYMENT_CONFIG} + +echo "Checkout latest version of master Dockerfile" +git checkout master ${DOCKER_FILE} + +echo "Update OTP version number in ${DOCKER_FILE}" +if [[ "$OSTYPE" == "darwin"* ]]; then + sed -E -i '' "s/OTP_VERSION=[\.0-9]+-entur-[0-9]+/OTP_VERSION=${VERSION}/" ${DOCKER_FILE} +else + sed -E -i "s/OTP_VERSION=[\.0-9]+-entur-[0-9]+/OTP_VERSION=${VERSION}/" ${DOCKER_FILE} +fi + +if [[ "$CIRCLE_USERNAME" != "" ]]; then + git config user.email "circleci@entur.no" + git config user.name "circleci ($CIRCLE_USERNAME)" +fi + +echo "Add and commit Dockerfile" +git commit -m "New OTP2 Version ${VERSION}" ${DOCKER_FILE} + +echo "Push otp-deployment-config to GitHub" +git push + +popd \ No newline at end of file diff --git a/.gitignore b/.gitignore index a90f06cdcb0..3bc96654225 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .project .pydevproject .settings +.prepare_release.tmp .sonar .nvmrc *~ diff --git a/pom.xml b/pom.xml index 5880d9dcc62..585b93f8848 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ - 150 + EN-0067 31.1 2.51.1 @@ -84,11 +84,11 @@ - - github - OpenTripPlanner Maven Repository on Github Packages - https://maven.pkg.github.com/${GITHUB_REPOSITORY}/ - + + snapshots + entur2-snapshots + https://entur2.jfrog.io/entur2/libs-snapshot-local + diff --git a/src/main/java/EnturUpdatePomVersion.java b/src/main/java/EnturUpdatePomVersion.java new file mode 100644 index 00000000000..97bd72b3c6e --- /dev/null +++ b/src/main/java/EnturUpdatePomVersion.java @@ -0,0 +1,135 @@ +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; + +public class EnturUpdatePomVersion { + + private static final String VERSION_SEP = "-"; + private static final String ENTUR_PREFIX = "entur" + VERSION_SEP; + private static final Path POM_FILE = Path.of("pom.xml"); + + private final List tags = new ArrayList<>(); + private final List pomFile = new ArrayList<>(); + private String mainVersion; + private int versionNumber = 0; + + public static void main(String[] args) { + try { + new EnturUpdatePomVersion().withArgs(args).run(); + } catch (Exception e) { + System.err.println(e.getMessage()); + e.printStackTrace(System.err); + System.exit(10); + } + } + + private EnturUpdatePomVersion withArgs(String[] args) throws IOException { + if (args.length != 1 || args[0].matches("(?i)-h|--help")) { + printHelp(); + System.exit(1); + } + String arg = args[0]; + + if (arg.matches("\\d+")) { + versionNumber = resolveVersionFromNumericString(arg); + } else { + tags.addAll(readTagsFromFile(arg)); + } + return this; + } + + private void run() throws IOException { + readAndReplaceVersion(); + replacePomFile(); + } + + public void readAndReplaceVersion() throws IOException { + var pattern = Pattern.compile( + "(\\s*)(\\d+.\\d+.\\d+)" + + VERSION_SEP + + "(" + + ENTUR_PREFIX + + "\\d+|SNAPSHOT)(\\s*)" + ); + boolean found = false; + int i = 0; + + for (String line : Files.readAllLines(POM_FILE, UTF_8)) { + // Look for the version in the 25 first lines + if (!found) { + var m = pattern.matcher(line); + if (m.matches()) { + mainVersion = m.group(2); + String newVersion = mainVersion + VERSION_SEP + ENTUR_PREFIX + resolveVersionNumber(); + line = m.group(1) + newVersion + m.group(4); + System.out.println(newVersion); + found = true; + } + if (++i == 25) { + throw new IllegalStateException( + "Version not found in first 25 lines of the pom.xml file." + ); + } + } + pomFile.add(line); + } + if (!found) { + throw new IllegalStateException( + "Version not found in 'pom.xml'. Nothing matching pattern: " + pattern + ); + } + } + + public void replacePomFile() throws IOException { + Files.delete(POM_FILE); + Files.write(POM_FILE, pomFile, UTF_8); + } + + private static void printHelp() { + System.err.println( + "Use this small program to replace the OTP version '2.1.0-SNAPSHOT' \n" + + "with a new version number with a Entur qualifier like '2.1.0-entur-1'.\n" + + "\n" + + "Usage:\n" + + " $ java -cp .circleci UpdatePomVersion \n" + ); + } + + private int resolveVersionNumber() { + var pattern = Pattern.compile("v" + mainVersion + VERSION_SEP + ENTUR_PREFIX + "(\\d+)"); + int maxTagVersion = tags + .stream() + .mapToInt(tag -> { + var m = pattern.matcher(tag); + return m.matches() ? Integer.parseInt(m.group(1)) : -999; + }) + .max() + .orElse(-999); + + return 1 + Math.max(maxTagVersion, versionNumber); + } + + public static int resolveVersionFromNumericString(String arg) { + try { + return Integer.parseInt(arg); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Unable to parse input, decimal number expected: '" + arg + "'" + ); + } + } + + private static Collection readTagsFromFile(String arg) throws IOException { + var tags = Files.readAllLines(Path.of(arg)); + if (tags.isEmpty()) { + throw new IllegalStateException("Unable to load git tags from file: " + arg); + } + return tags; + } +}