Skip to content

Gradle, Travis CI and Maven Central

matthieun edited this page Dec 18, 2018 · 3 revisions

Overview

This is a list of the steps to perform to publish a GitHub open source project to Maven Central, in a mostly automated fashion, using Travis CI. Atlas is a working example: osmlab/atlas. Most examples below are from that project.

Assumptions

This document assumes that the project

  • is in github.com
  • is not private
  • has an open license
  • is built using gradle
  • is written in Java/Scala

Basic setup

Default branches

The repository should have a locked master branch, which will be used by Travis to build snapshots and releases, and a default branch called dev.

Gradle wrapper

If not there already, make sure to enable the gradle wrapper, so Travis does not have to guess about the version of Gradle to use.

gradle wrapper

This creates a gradlew and gradlew.bat files:

Publishing setup

Sonatype JIRA account

Sonatype is the company that maintains the Maven Central repository, and they offer free hosting for open source software. They also take care of the workflow of syncing the releases with the maven central mirror.

Open https://issues.sonatype.org and create an account in JIRA (that username/password will also be the login credentials to upload artifacts).

Sonatype OSSRH ticket

Open a JIRA ticket with Sonatype to open the group id for you: https://issues.sonatype.org/secure/CreateIssue.jspa?issuetype=21&pid=10134

Fill all the required info, and wait for a human to resolve the issue. It should then look like this: https://issues.sonatype.org/browse/OSSRH-31007

Artifact creation with gradle

Javadoc and sources

In build.gradle, add tasks for the javadoc and sources:

apply plugin: 'java'
 
task javadocJar(type: Jar) {
    classifier = 'javadoc'
    from javadoc
}
task sourcesJar(type: Jar) {
    classifier = 'sources'
    from sourceSets.main.allSource
}
artifacts
{
    archives javadocJar, sourcesJar
}

Pom file

In gradle.properties, on top of the group and version, add the following (and replace the values accordingly). This will enable Gradle to create a full pom file that Maven Central will approve of.

maven2_url=https://oss.sonatype.org/service/local/staging/deploy/maven2/
snapshot_url=https://oss.sonatype.org/content/repositories/snapshots/
project_name="OSM Atlas Library"
project_description="Library to load OSM data into an Atlas format"
project_url=https://github.com/osmlab/atlas
project_license_url=https://github.com/osmlab/atlas/blob/master/LICENSE
project_license_slug="BSD 3 Clause"
project_developer=matthieun
project_scm=scm:git:https://github.com/osmlab/atlas.git

Then, in build.gradle, use the maven plugin to format the pom so Maven Central accepts it.

apply plugin: 'maven'
 
uploadArchives
{
    repositories
    {
        mavenDeployer
        {
            beforeDeployment
            {
                MavenDeployment deployment -> signing.signPom(deployment)
            }
            repository(url: maven2_url) {
                authentication(userName: System.getenv('SONATYPE_USERNAME'), password: System.getenv('SONATYPE_PASSWORD'))
            }
            snapshotRepository(url: snapshot_url) {
                authentication(userName: System.getenv('SONATYPE_USERNAME'), password: System.getenv('SONATYPE_PASSWORD'))
            }
            pom.project
            {
                name project_name
                packaging 'jar'
                description project_description
                url project_url
                scm {
                    connection project_scm
                    developerConnection project_scm
                    url project_url
                }
                licenses {
                    license {
                        name project_license_slug
                        url project_license_url
                    }
                }
                developers {
                    developer {
                        id project_developer
                        name project_developer
                    }
                }
            }
        }
    }
}

Artifact signature

Maven central being a trusted source of artifacts, it requires you to sign the artifacts to guarantee their authenticity.

GnuPG

GnuPG is a free PGP keyring manager. It will be used to sign the artifacts. This section details the steps to follow to sign artifacts for maven central. To get a more in-depth view of that process, visit http://central.sonatype.org/pages/working-with-pgp-signatures.html

Install GnuPG

brew install gpg
gpg --version

Generate a key pair

gpg --full-generate-key

Here are the options to follow during that process:

  • size: 4096
  • The email you used in the Sonatype account (not mandatory)
  • Your github username
  • A passphrase (to remember!)

Finally, list the keys:

gpg --keyid-format short --list-secret-keys

returns:

/Users/matthieun/.gnupg/pubring.gpg
-----------------------------------
sec   rsa4096 2017-05-04 [SC]
      XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
uid           [ultimate] username <email>
ssb   rsa4096/AAAAAAAA 2017-05-04 [E]

sec   rsa4096 2018-02-06 [SC]
      YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
uid           [ultimate] username <email>
ssb   rsa4096/BBBBBBBB 2018-02-06 [E]

Here, the AAAAAAAA that corresponds to the date/username/email just created above is the short keyring identifier. Save it to a note. It will have to be encrypted for Travis later.

Export the private key to a file

gpg --export-secret-keys XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX > ~/Desktop/secret.gpg

That GPG file will later be encrypted by Travis.

Broadcast your public key

For the signature to be authenticated, the public part of the keyring needs to be uploaded to a key server. Below, replace XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX with the keyring identifier.

gpg --keyserver hkp://pool.sks-keyservers.net --send-keys XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Gradle

Gradle has a neat signing plugin that does all the work. Add the following to build.gradle:

apply plugin: 'signing'

...

import org.gradle.plugins.signing.Sign
gradle.taskGraph.whenReady { taskGraph ->
    if (taskGraph.allTasks.any { it instanceof Sign }) {
        allprojects { ext."signing.keyId" = System.getenv('GPG_KEY_ID') }
        allprojects { ext."signing.secretKeyRingFile" = System.getenv('GPG_KEY_LOCATION') }
        allprojects { ext."signing.password" = System.getenv('GPG_PASSPHRASE') }
    }
    // Do not sign archives by default (a local build without gpg keyring should succeed)
    if (taskGraph.allTasks.any { it.name == 'build' || it.name == 'assemble' }) {
        tasks.findAll { it.name == 'signArchives' || it.name == 'signDocsJar' || it.name == 'signTestJar' }.each { task ->
            task.enabled = false
        }
    }
}
signing
{
    sign configurations.archives
}
build.dependsOn.remove(signArchives)

The signing task depends on build by default. We make sure to exclude that, as we do not want to sign artifacts all the time. When a user wants to build the project, he should not have to build a keyring first. Signing is useful at deployment only.

The CI tool will then call "gradle signArchives" directly, which will enable the task and run it.

Travis CI

Travis CI is a continuous integration tool. It is free and unlimited for all open source projects.

Sign in with Github

Use GitHub to log into Travis CI:

Then, go to Github. Settings > Authorized applications

You should see Travis as an Authorized application:

Enable project

Back to Travis CI, go to the settings page (default page, of if you have enabled projects before, the "+" button next to "my repositories" on the left side)

Click sync account:

Enable your project:

Travis command

Install

Install the travis command:

gem install travis

Navigate to the repository clone on your local system. Then login with travis:

travis login

Supply your github credentials when prompted. This allows the travis command line to encrypt some files and environment variables that will be visible in .travis.yml.

Github API token

So that Travis can access your github account within scripts you write, you need to create a Github application token:

Github > Settings > Personal access tokens > Generate new token

When the token is generated, make sure to copy it as it will be visible only once.

Then go back to the root of your project clone, and type the following (be careful, the "=" here is very important):

travis encrypt "GITHUB_SECRET_TOKEN=<the token you just copied>"

The command will reply with a long encrypted string: secure: "blah blah". Save that string in a note.

Travis folder

Create a .travis folder that will hold all the automation scripts in the root of the project.

Travis yml configuration

Create a .travis.yml file at the root of the repo. Travis reads this file first to identify what environment and build steps it needs to run. More detailed documentation on the .travis.yml file here.

env:
  global:
    - # GITHUB_SECRET_TOKEN
    - secure: "XXXXXX"
    - # SONATYPE_USERNAME
    - secure: "XXXXXX"
    - # SONATYPE_PASSWORD
    - secure: "XXXXXX"
    - # GPG_KEY_ID
    - secure: "XXXXXX"
    - # GPG_PASSPHRASE
    - secure: "XXXXXX"
    - GPG_KEY_LOCATION=".travis/secret.gpg"
    - ENCRYPTED_GPG_KEY_LOCATION=".travis/secret.gpg.enc"
branches:
  only:
  - master
  - dev
language: java
jdk:
  - oraclejdk8
before_install:
  - chmod -R ug+x .travis
  - .travis/install.sh
script:
  - chmod -R ug+x .travis
  - .travis/build.sh
  - .travis/merge-dev-to-master-gate.sh
  - .travis/deploy-gate.sh
  - .travis/tag-master-gate.sh
  • The global section stores the encrypted environment variables that travis only will be able to decrypt. At that point, update the one called GITHUB_SECRET_TOKEN that was saved in a note just before.
  • The branches section lets Travis know which branches to build on. Use master and dev only.
  • The before_install step runs before the build starts. This is where decrypting the keys will happen.
  • The script step is the list of build steps. If any step fails, the next ones continue, but the build will be marked as failed.

Encrypt the keyring

Keyring file

The keyring secret file that was created before needs to be encrypted by travis.

travis encrypt-file ~/Desktop/secret.gpg

It will reply with an openssl command that looks like this:

openssl aes-256-cbc -K $encrypted_XXXXXX_key -iv $encrypted_XXXXXX_iv -in $ENCRYPTED_GPG_KEY_LOCATION -out $GPG_KEY_LOCATION -d

Copy that command, and paste it in a file called install.sh in the .travis folder. Very important: Make sure that the -in and -out arguments are adapted so they are in the .travis folder, where Travis will find them at build time.

#!/usr/bin/env sh

if [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ];
then
    openssl aes-256-cbc -K $encrypted_XXXXXX_key -iv $encrypted_XXXXXX_iv -in $ENCRYPTED_GPG_KEY_LOCATION -out $GPG_KEY_LOCATION -d
fi

Final step, the travis script also created an encrypted gpg file: secret.gpg.enc. Move it to the .travis folder.

Keyring identifier

The GPG keyring identifier (saved in a note in a previous step) needs to be encrypted.

travis encrypt "GPG_KEY_ID=<your key id>"

Paste the results to the .travis.yml in the proper global section.

Keyring passphrase

The GPG keyring passphrase (generated when the keyring was created) needs to be encrypted.

travis encrypt "GPG_PASSPHRASE=<your key passphrase>"

Paste the results to the .travis.yml in the proper global section.

Encrypt Sonatype credentials

Sonatype credentials are used by Travis to publish snapshots and releases to maven central.

Sonatype username

The Sonatype JIRA account name needs to be encrypted.

travis encrypt "SONATYPE_USERNAME=<your sonatype username>"

Paste the results to the .travis.yml in the proper global section.

Sonatype password

The Sonatype JIRA account password needs to be encrypted.

travis encrypt "SONATYPE_PASSWORD=<your sonatype password>"

Paste the results to the .travis.yml in the proper global section.

Create workflow

The next steps describe the workflow Travis will follow for each CI cycle.

Build script

Add a build.sh file to the .travis/ folder:

#!/usr/bin/env sh
 
chmod u+x gradlew
 
if [ "$TRAVIS_PULL_REQUEST" != "false" ];
then
    echo "Skip integration tests in pull request builds"
    ./gradlew clean build -x integrationTest
else
    ./gradlew clean build
fi

Pull request builds

This is the easy part. Go to the repository's settings in travis (click + on the left if you have run builds already, otherwise it is the main page).

Click the wheel between the enablement switch and the name:

In "General", select Build branch updates and build pull request updates.

At that point, pull requests will trigger builds.

Merge dev to master builds

Any commit to dev will trigger a build so far. In this section we want Travis to do the merge from dev to master automatically when a build is successful on dev.

Create a merge-dev-to-master.sh script under .travis/. Make sure to update the GITHUB_REPO variable.

#!/usr/bin/env sh
 
GITHUB_REPO="osmlab/atlas"
MERGE_BRANCH=master
SOURCE_BRANCH=dev
 
FUNCTION_NAME="merge-$SOURCE_BRANCH-to-$MERGE_BRANCH"
 
echo "$FUNCTION_NAME: $GITHUB_REPO"
echo "$FUNCTION_NAME: TRAVIS_BRANCH = $TRAVIS_BRANCH"
echo "$FUNCTION_NAME: TRAVIS_PULL_REQUEST = $TRAVIS_PULL_REQUEST"
 
if [ "$TRAVIS_BRANCH" != "$SOURCE_BRANCH" ];
then
    echo "$FUNCTION_NAME: Exiting! Branch is not $SOURCE_BRANCH: ($TRAVIS_BRANCH)"
    exit 0;
fi
 
if [ "$TRAVIS_PULL_REQUEST" != "false" ];
then
    echo "$FUNCTION_NAME: Exiting! This is a Pull Request: $TRAVIS_PULL_REQUEST"
    exit 0;
fi
 
: ${GITHUB_SECRET_TOKEN:?"GITHUB_SECRET_TOKEN needs to be set in .travis.yml!"}
 
export GIT_COMMITTER_EMAIL="[email protected]"
export GIT_COMMITTER_NAME="Travis CI"
 
TEMPORARY_REPOSITORY=$(mktemp -d)
git clone "https://github.com/$GITHUB_REPO" "$TEMPORARY_REPOSITORY"
cd $TEMPORARY_REPOSITORY
 
echo "Checking out $SOURCE_BRANCH"
git checkout $SOURCE_BRANCH
 
echo "Checking out $MERGE_BRANCH"
git checkout $MERGE_BRANCH
 
echo "Merging $SOURCE_BRANCH into $MERGE_BRANCH"
git merge --ff-only "$SOURCE_BRANCH"
 
echo "Pushing to $GITHUB_REPO"
# Redirect to /dev/null to avoid secret leakage
git push "https://$GITHUB_SECRET_TOKEN@github.com/$GITHUB_REPO" $MERGE_BRANCH > /dev/null 2>&1

That script makes sure that the build is on dev, and that it is not a pull request build. It will then merge dev to master locally and then push it to the github repo.

To make sure it runs only when the build is successful, wrap in a gate script that runs it only accordingly: .travis/merge_dev_to_master_gate.sh

#!/usr/bin/env sh
 
if [ $TRAVIS_TEST_RESULT -eq 0 ];
then
    .travis/merge-dev-to-master.sh
    RETURN_VALUE=$?
    if [ "$RETURN_VALUE" != "0" ];
    then
        exit $RETURN_VALUE
    fi
fi

Master release builds

Release builds need to be run manually only. That involves slight modifications to the scripts, as well as a new script that will call the Travis API v3 to trigger a build.

Alter the build script

In .travis/build.sh, add the following prior to the gradle clean build calls, to make sure the version of the project does not contain any -SNAPSHOT

if [ "$MANUAL_RELEASE_TRIGGERED" = "true" ];
then
    # This is a release job, triggered manually
    # Change the version locally to remove the -SNAPSHOT
    sed -i "s/-SNAPSHOT//g" gradle.properties
    echo "This is a manual release!"
else
    echo "Not a manual release"
fi

In the end, build.sh should look like this:

#!/usr/bin/env sh

chmod u+x gradlew

if [ "$MANUAL_RELEASE_TRIGGERED" = "true" ];
then
	# This is a release job, triggered manually
	# Change the version locally to remove the -SNAPSHOT
	sed -i "s/-SNAPSHOT//g" gradle.properties
	echo "This is a manual release!"
else
	echo "Not a manual release"
fi

if [ "$TRAVIS_PULL_REQUEST" != "false" ];
then
	echo "Skip integration tests in pull request builds"
	./gradlew clean build -x integrationTest
else
	echo "Temporarily skip integration tests in all builds. Too heavy for Travis"
	./gradlew clean build -x integrationTest
fi

Add a deploy.sh script to the .travis folder:

#!/usr/bin/env sh

if [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ];
then
	if [ "$MANUAL_RELEASE_TRIGGERED" = "true" ];
	then
		echo "Sign, Upload archives to local repo, Upload archives to Sonatype, Close and release repository."
		./gradlew uploadArchives publishToNexusAndClose
	fi
fi

To make sure it runs only when the build is successful, wrap in a gate script that runs it only accordingly:

#!/usr/bin/env sh

if [ $TRAVIS_TEST_RESULT -eq 0 ];
then
    .travis/deploy.sh
    RETURN_VALUE=$?
    if [ "$RETURN_VALUE" != "0" ];
    then
        exit $RETURN_VALUE
    fi
fi
Setup the gradle promote plugin

This is deprecated and stopped working: https://github.com/travis-ci/travis-ci/issues/9555. See next section for a workaround.

Once an artifact is uploaded as a release to Sonatype, it needs to undergo some verifications while being promoted. There is a gradle plugin to do that.

in build.gradle add:

plugins
{
    id "io.codearte.nexus-staging" version "0.8.0"
}

After the gradle calls in deploy.sh, add the following:

if [ "$MANUAL_RELEASE_TRIGGERED" = "true" ];
then
    echo "Promote repository"
    ./gradlew closeAndReleaseRepository
fi

This will call the promote plugin to automatically promote the release.

In the end, deploy.sh should look like this:

#!/usr/bin/env sh

if [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ];
then
	echo "Sign Archives"
	./gradlew signArchives
	echo "Upload Archives"
	./gradlew uploadArchives
	if [ "$MANUAL_RELEASE_TRIGGERED" = "true" ];
	then
		echo "Promote repository"
		./gradlew closeAndReleaseRepository
	fi
fi
Workaround for deploying to Sonatype

Due to a Travis limitation (https://github.com/travis-ci/travis-ci/issues/9555), and until a better solution is available, add this to the build.gradle:

apply plugin: 'maven-publish'

def uploadAndRelease(def username, def password, def repoDir) {
    def proc = ['./uploadAndRelease.sh', username, password, repoDir].execute([], file("${rootDir.toString()}/gradle"))
    proc.waitForProcessOutput(System.out, System.err)
}

task publishToNexusAndClose(dependsOn: 'publish'){
    doLast {
        uploadAndRelease(System.getenv('SONATYPE_USERNAME'), System.getenv('SONATYPE_PASSWORD'), "$rootDir/build/deploy")
    }
}

For this to work, the <Project>/gradle folder needs to contain the following scripts, which talk to oss.sonatype.org directly to upload the artifacts:

uploadAndRelease.sh:

#!/usr/bin/env bash

export SONATYPE_USERNAME=$1
export SONATYPE_PASSWORD=$2
export REPOSITORY_DIR=$3

export API_ENDPOINT=https://oss.sonatype.org/service/local

function runWithRetry()
{
	n=0
	until [ $n -ge 100 ]
	do
		$1 && break
		n=$[$n+1]
        echo "Sleep 15 sec before retry"
		sleep 15
        echo "Retry $n"
	done
}

export ATLAS_PROFILE_ID=1442a4f451744
export DESCRIPTION_PAYLOAD="<promoteRequest>\
    <data>\
        <description>Atlas Release</description>\
    </data>\
</promoteRequest>"

# Create staging repo
export STAGING_ID=$(curl -s -u $SONATYPE_USERNAME:$SONATYPE_PASSWORD \
    -X POST \
    -H "Content-Type:application/xml" \
    -d "$DESCRIPTION_PAYLOAD" \
    "$API_ENDPOINT/staging/profiles/$ATLAS_PROFILE_ID/start" \
    | perl -nle 'print "$1" if ($_ =~ /.*<stagedRepositoryId>(.*)<\/stagedRepositoryId>.*/g);' \
    | awk '{$1=$1};1')
# Response parsed looks like this:
# <promoteResponse>  <data>    <stagedRepositoryId>orgopenstreetmapatlas-1147</stagedRepositoryId>    <description>Atlas Release</description>  </data></promoteResponse>

export CLOSE_PAYLOAD="<promoteRequest>\
    <data>\
        <stagedRepositoryId>$STAGING_ID</stagedRepositoryId>\
        <description>Close Atlas repo</description>\
    </data>\
</promoteRequest>"

# Upload
./uploadToNexus.sh $SONATYPE_USERNAME $SONATYPE_PASSWORD $REPOSITORY_DIR $STAGING_ID

echo "sleep 20 seconds before closing"
sleep 20

# Close
curl --fail -s -u $SONATYPE_USERNAME:$SONATYPE_PASSWORD \
    -X POST \
    -H "Content-Type:application/xml" \
    -d "$CLOSE_PAYLOAD" \
    "$API_ENDPOINT/staging/profiles/$ATLAS_PROFILE_ID/finish"

echo "sleep 120 before releasing (to let validation happen)"
sleep 120

# Release
runWithRetry ./releaseSonatype.sh

# Drop if needed. It is usually automatically cleaned up.
# runWithRetry ./dropSonatype.sh

uploadToNexus.sh:

#!/usr/bin/env bash

name=$1
password=$2
repodir=$3
stagingId=$4

find $repodir -type f | while read f; do
    suffix=$(echo $f | sed "s%^$repodir/%%")
    echo "Uploading to: ${stagingId}: ${suffix}"
    curl -s -u $name:$password -H "Content-type: application/x-rpm" --upload-file $f https://oss.sonatype.org/service/local/staging/deployByRepositoryId/${stagingId}/${suffix}
done

releaseSonatype.sh

export RELEASE_PAYLOAD='{'\
'   "data":{'\
'      "stagedRepositoryIds":['\
'         "'$STAGING_ID'"'\
'      ],'\
'      "description":"Releasing Atlas repo"'\
'   }'\
'}'

curl --fail -s -u $SONATYPE_USERNAME:$SONATYPE_PASSWORD \
    -X POST \
    -H "Content-Type:application/json" \
    -d "$RELEASE_PAYLOAD" \
    "$API_ENDPOINT/staging/bulk/promote" \

dropSonatype.sh

export DROP_PAYLOAD='{'\
'   "data":{'\
'      "stagedRepositoryIds":['\
'         "'$STAGING_ID'"'\
'      ],'\
'      "description":"Dropping Atlas repo"'\
'   }'\
'}'

curl --fail -u $SONATYPE_USERNAME:$SONATYPE_PASSWORD \
    -X POST \
    -H "Content-Type:application/json" \
    -d "$DROP_PAYLOAD" \
    "$API_ENDPOINT/staging/bulk/drop"
Git tag generation

In .travis, add a tag-master.sh script that will mark the tag from master, if the release succeeded. Make sure to update the GITHUB_REPO variable.

#!/usr/bin/env sh

GITHUB_REPO="osmlab/atlas"
RELEASE_BRANCH=master

FUNCTION_NAME="tag-$RELEASE_BRANCH"

echo "$FUNCTION_NAME: $GITHUB_REPO"
echo "$FUNCTION_NAME: TRAVIS_BRANCH = $TRAVIS_BRANCH"
echo "$FUNCTION_NAME: TRAVIS_PULL_REQUEST = $TRAVIS_PULL_REQUEST"

if [ "$TRAVIS_BRANCH" != "$RELEASE_BRANCH" ];
then
    echo "$FUNCTION_NAME: Exiting! Branch is not $RELEASE_BRANCH: ($TRAVIS_BRANCH)"
    exit 0;
fi

if [ "$TRAVIS_PULL_REQUEST" != "false" ];
then
    echo "$FUNCTION_NAME: Exiting! This is a Pull Request: $TRAVIS_PULL_REQUEST"
    exit 0;
fi

if [ "$MANUAL_RELEASE_TRIGGERED" != "true" ];
then
    echo "$FUNCTION_NAME: Exiting! This is not a release build."
    exit 0;
fi

: ${GITHUB_SECRET_TOKEN:?"GITHUB_SECRET_TOKEN needs to be set in .travis.yml!"}

export GIT_COMMITTER_EMAIL="[email protected]"
export GIT_COMMITTER_NAME="Travis CI"

TEMPORARY_REPOSITORY=$(mktemp -d)
git clone "https://github.com/$GITHUB_REPO" "$TEMPORARY_REPOSITORY"
cd $TEMPORARY_REPOSITORY

echo "Checking out $RELEASE_BRANCH"
git checkout $RELEASE_BRANCH

PROJECT_VERSION=$(cat gradle.properties | grep "\-SNAPSHOT" | awk -F '=' '{print $2}' | awk -F '-' '{print $1}')
: ${PROJECT_VERSION:?"PROJECT_VERSION could not be found."}

echo "Tagging $RELEASE_BRANCH at version $PROJECT_VERSION"
git tag -a $PROJECT_VERSION -m "Release $PROJECT_VERSION"

echo "Pushing tag $PROJECT_VERSION to $GITHUB_REPO"
# Redirect to /dev/null to avoid secret leakage
git push "https://$GITHUB_SECRET_TOKEN@github.com/$GITHUB_REPO" $PROJECT_VERSION > /dev/null 2>&1

To make sure it runs only when the build is successful, wrap in a gate script tag-master-gate.sh that runs it only accordingly:

#!/usr/bin/env sh

if [ $TRAVIS_TEST_RESULT -eq 0 ];
then
    .travis/tag-master.sh
    RETURN_VALUE=$?
    if [ "$RETURN_VALUE" != "0" ];
    then
        exit $RETURN_VALUE
    fi
fi
Trigger Script

In .travis/, add a script trigger-release.sh that can trigger builds. Make sure to update the GITHUB_ORGANIZATION and GITHUB_REPOSITORY_NAME below:

#!/usr/bin/env sh

# Use Travis to trigger a release from Master

GITHUB_ORGANIZATION=osmlab
GITHUB_REPOSITORY_NAME=atlas

# Assumptions
# - This is called from the root of the project
# - The travis client is installed: gem install travis
# - travis login --org has been called to authenticate

TRAVIS_PERSONAL_TOKEN=$(travis token)

:${TRAVIS_PERSONAL_TOKEN:?"TRAVIS_PERSONAL_TOKEN needs to be set to access the Travis API to trigger the build"}

body='
{
    "request":
    {
        "branch": "master",
        "config":
        {
            "before_script": "export MANUAL_RELEASE_TRIGGERED=true"
        }
    }
}'

curl -s -X POST \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -H "Travis-API-Version: 3" \
    -H "Authorization: token $TRAVIS_PERSONAL_TOKEN" \
    -d "$body" \
    https://api.travis-ci.org/repo/$GITHUB_ORGANIZATION%2F$GITHUB_REPOSITORY_NAME/requests

Here the "manual" part is tracked with an environment variable called MANUAL_RELEASE_TRIGGERED.

Call that script from the root of the repo, manually, and after travis login has been called.