org.jvnet.hudson.plugins.m2release.ReleaseEnvironment.java Source code

Java tutorial

Introduction

Here is the source code for org.jvnet.hudson.plugins.m2release.ReleaseEnvironment.java

Source

/*
*  Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
*  WSO2 Inc. licenses this file to you under the Apache License,
*  Version 2.0 (the "License"); you may not use this file except
*  in compliance with the License.
*  You may obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
*  Unless required by applicable law or agreed to in writing,
*  software distributed under the License is distributed on an
*  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
*  KIND, either express or implied.  See the License for the
*  specific language governing permissions and limitations
*  under the License.
*/
package org.jvnet.hudson.plugins.m2release;

import hudson.EnvVars;
import hudson.Launcher;
import hudson.maven.MavenModule;
import hudson.maven.MavenModuleSet;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.plugins.git.GitException;
import hudson.plugins.git.GitSCM;
import hudson.plugins.git.UserRemoteConfig;
import hudson.plugins.git.util.GitUtils;
import hudson.scm.SCM;
import hudson.tasks.BuildWrapper;
import hudson.util.RunList;
import hudson.util.TextFile;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.URIish;
import org.jenkinsci.plugins.gitclient.GitClient;
import org.jvnet.hudson.plugins.m2release.nexus.Stage;
import org.jvnet.hudson.plugins.m2release.nexus.StageClient;
import org.jvnet.hudson.plugins.m2release.nexus.StageException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;

public class ReleaseEnvironment extends BuildWrapper.Environment {

    private transient Logger log = LoggerFactory.getLogger(ReleaseEnvironment.class);

    private M2ReleaseBuildWrapper m2ReleaseBuildWrapper;
    private final String releaseBranch;
    private final String remoteBranch;
    private final String remoteRevision;
    private Launcher launcher;

    public ReleaseEnvironment(BuildWrapper enclosing, String releaseBranch, String remoteBranch,
            String remoteRevision, Launcher launcher) {
        enclosing.super();
        this.m2ReleaseBuildWrapper = (M2ReleaseBuildWrapper) enclosing;
        this.releaseBranch = releaseBranch;
        this.remoteBranch = remoteBranch;
        this.remoteRevision = remoteRevision;
        this.launcher = launcher;
    }

    @Override
    public void buildEnvVars(Map<String, String> env) {
        if (StringUtils.isNotBlank(m2ReleaseBuildWrapper.getReleaseEnvVar())) {
            // inform others that we are doing a release build
            env.put(m2ReleaseBuildWrapper.getReleaseEnvVar(), "true");
        }
    }

    /** Do Post-Build actions
     */
    @Override
    public boolean tearDown(@SuppressWarnings("rawtypes") AbstractBuild bld, BuildListener lstnr)
            throws IOException, InterruptedException {
        boolean retVal;

        M2ReleaseArgumentsAction args = bld.getAction(M2ReleaseArgumentsAction.class);

        if (args.isDryRun()) {
            lstnr.getLogger().println("[M2Release] its only a dryRun, we should not do any writable operation.");
            return true;
        }

        // 1) write latest commit hash
        writeLatestReleaseRevisionNumber(bld, lstnr);

        // 2) merge release branch into main branch and finalize the git repo
        finalizeSCMRepo(bld, lstnr);

        // 3) close and release nexus staging repo
        retVal = closeNexusStagingRepo(bld, lstnr);

        // 4) keep this build for later reference?
        keepThisBuild(bld, lstnr);

        return retVal;
    }

    private boolean closeNexusStagingRepo(AbstractBuild bld, BuildListener lstnr) {
        final MavenModuleSet mmSet = ReleaseUtils.getModuleSet(bld);
        M2ReleaseArgumentsAction args = bld.getAction(M2ReleaseArgumentsAction.class);

        try {
            StageClient client = new StageClient(new URL(m2ReleaseBuildWrapper.getDescriptor().getNexusURL()),
                    m2ReleaseBuildWrapper.getDescriptor().getNexusUser(),
                    m2ReleaseBuildWrapper.getDescriptor().getNexusPassword());
            MavenModule rootModule = mmSet.getRootModule();
            Stage stage = client.getOpenStageID(rootModule.getModuleName().groupId,
                    rootModule.getModuleName().artifactId, args.getReleaseVersion());

            if (stage == null) {
                lstnr.fatalError("[M2Release] Could not find nexus stage repository for project.\n");
                return false;
            }
            if (bld.getResult() == null || !bld.getResult().isBetterOrEqualTo(Result.SUCCESS)) {
                lstnr.getLogger().println(
                        "[M2Release] Dropping repository " + stage + ". Reason: " + bld.getResult() + " build.");
                client.dropStage(stage);
                lstnr.getLogger().println("[M2Release] Dropped staging repository.");

                return false;
            }

            //close the nexus repo.. with retrying.
            if (args.isCloseNexusStage()) {
                boolean isSuccess = false;
                for (int tries = 5; tries > 0; tries--) {
                    try {
                        lstnr.getLogger().println("[M2Release] Closing Nexus staging repository " + stage);
                        client.closeStage(stage, args.getRepoDescription());
                        lstnr.getLogger().println("[M2Release] Closed Nexus staging repository.");
                        isSuccess = true;
                        break;
                    } catch (StageException ex) {
                        lstnr.fatalError("[M2Release] Could not close repository , %1$s\n", ex.getMessage());
                        ex.printStackTrace(lstnr.getLogger());
                        log.error("[M2Release] Could not close repository " + stage, ex);

                        try {
                            Thread.sleep(15000);
                        } catch (InterruptedException e) {
                            /* ignore */ }
                        lstnr.getLogger().println("Retrying...");
                    }
                }
                if (!isSuccess) {
                    return false;
                }

            }

            //release the nexus staging repository
            if (args.isReleaseNexusStage()) {
                lstnr.getLogger().println("[WSO2 Maven Release] Releasing Nexus repository " + stage);
                client.releaseStage(stage, args.getRepoDescription());
                lstnr.getLogger().println("[WSO2 Maven Release] Released Nexus repository.");
                ReleaseUtils.printSeparator(lstnr);
            }

        } catch (StageException ex) {
            lstnr.fatalError("[M2Release] Could not close/release repository , %1$s\n", ex.getMessage());
            ex.printStackTrace(lstnr.getLogger());
            log.error("[M2Release] Could not close repository ", ex);
            return false;
        } catch (MalformedURLException ex) {
            ex.printStackTrace(lstnr.getLogger());
            return false;
        }
        return true;
    }

    private void finalizeSCMRepo(AbstractBuild bld, BuildListener buildListener)
            throws IOException, InterruptedException {
        //merge the release branch into master
        if (bld.getProject().getScm() instanceof GitSCM) {
            GitSCM gitSCM = (GitSCM) bld.getProject().getScm();
            AbstractProject project = bld.getProject();

            final EnvVars environment = GitUtils.getPollEnvironment(project, bld.getWorkspace(), launcher,
                    buildListener);
            GitClient gitClient = gitSCM.createClient(buildListener, environment, bld, bld.getWorkspace());
            M2ReleaseArgumentsAction args = bld.getAction(M2ReleaseArgumentsAction.class);

            List<UserRemoteConfig> userRemoteConfigs = gitSCM.getUserRemoteConfigs();
            if (userRemoteConfigs.isEmpty()) {
                buildListener.fatalError(
                        "[WSO2 Maven Release] " + "Could not find the git remote URL for the project. \n");
            } else if (userRemoteConfigs.get(0).getCredentialsId() == null) {
                ReleaseUtils.printInfoIntoBuildLog("Credentials are not present in the git configuration",
                        buildListener.getLogger());
            }

            //get release branch head commit
            String remoteUrl = userRemoteConfigs.get(0).getUrl();
            String releaseBranchHeadCommit = gitClient.revParse(M2ReleaseBuildWrapper.DEFAULT_REF).name();
            //local branch name that will be pushed to #remoteBranch
            String localBranchToPush = UUID.randomUUID().toString();

            try {
                // 1) handle build failures
                //we delete the git tag, but keep the release branch as it is in case it needs to be reviewed later
                ReleaseUtils.printSeparator(buildListener);
                ReleaseUtils.printInfoIntoBuildLog("[WSO2 Maven Release] Build Result: " + bld.getResult(),
                        buildListener.getLogger());
                if (bld.getResult() == null || !bld.getResult().isBetterOrEqualTo(Result.SUCCESS)) {
                    String scmTag = args.getScmTagName();
                    buildListener.getLogger().println("[WSO2 Maven Release] Dropping Git Tag: " + scmTag
                            + ". Reason: " + bld.getResult() + " build.");

                    //if exceptions, then remove the remote release tag
                    String refspec = ":" + "refs/tags/" + scmTag; // :refs/tags/v4.4.10
                    buildListener.getLogger().println();
                    ReleaseUtils.printInfoIntoBuildLog("Deleting release tag from remote.",
                            buildListener.getLogger());
                    gitClient.push().to(new URIish(remoteUrl)).ref(refspec).execute();

                    buildListener.getLogger().println("[WSO2 Maven Release] Dropped git tag - " + scmTag);
                    return;
                }
            } catch (URISyntaxException e) {
                buildListener.fatalError(
                        "[WSO2 Maven Release] " + "Could not parse the git remote URL for project: " + remoteUrl);
                bld.keepLog();
                throw new IllegalArgumentException(e.getMessage(), e);
            } catch (GitException e) {
                //this could fail if merging the release commits lead to a conflict.
                ReleaseUtils.printExceptionIntoBuildLog("[ERROR] [WSO2 Maven Release] merging the changes. ", e,
                        buildListener);
                bld.keepLog();
                throw e;
            }

            try {
                // 2) get latest commits from remote to a local branch

                // 2.1) checkout a temporary release branch for git push to remote
                ReleaseUtils.printInfoIntoBuildLog("Checking out a temp local branch named " + localBranchToPush
                        + " at revision " + remoteRevision, buildListener.getLogger());
                gitClient.checkoutBranch(localBranchToPush, remoteRevision);
                ReleaseUtils.printSeparator(buildListener);

                String localFetchBranch = "refs/remotes/origin/" + localBranchToPush;
                String fetchRefspec = remoteBranch + ":" + localFetchBranch;
                ReleaseUtils.printInfoIntoBuildLog(
                        "Fetching latest changes with refspec: " + fetchRefspec
                                + " before merging the release commits into " + localBranchToPush,
                        buildListener.getLogger());

                // 2.2) get latest commits from the remote branch before pushing to avoid outdated wc error
                gitClient.fetch_().from(new URIish(remoteUrl), Collections.singletonList(new RefSpec(fetchRefspec)))
                        .execute();
                String latestRemoteCommit = gitClient.revParse(localFetchBranch).getName();
                ObjectId latestRemoteCommitObject = ObjectId.fromString(latestRemoteCommit);
                ReleaseUtils.printInfoIntoBuildLog("Merging fetched upstream changes into " + localBranchToPush,
                        buildListener.getLogger());
                gitClient.merge().setRevisionToMerge(latestRemoteCommitObject).execute();
                ReleaseUtils.printSeparator(buildListener);
            } catch (URISyntaxException e) {
                buildListener.fatalError(
                        "[WSO2 Maven Release] " + "Could not parse the git remote URL for project: " + remoteUrl);
                throw new IllegalArgumentException(e);
            } catch (GitException e) {
                ReleaseUtils.printExceptionIntoBuildLog("[ERROR] [WSO2 Maven Release] merging the changes. ", e,
                        buildListener);
                gitClient.checkoutBranch(localBranchToPush, remoteRevision);
                //todo kasung does this work?
            }

            try {
                // 3) merge release commits into that local branch
                ReleaseUtils.printInfoIntoBuildLog("Merging release branch HEAD commit, " + releaseBranchHeadCommit
                        + ", into branch " + localBranchToPush, buildListener.getLogger());
                gitClient.merge().setRevisionToMerge(ObjectId.fromString(releaseBranchHeadCommit)).execute();

                // 3.1) push the whole thing into the original remote branch
                String refspec = localBranchToPush + ":" + remoteBranch;
                ReleaseUtils.printInfoIntoBuildLog("Pushing the whole thing into remote.",
                        buildListener.getLogger());
                gitClient.push().to(new URIish(remoteUrl)).ref(refspec).execute();

                // 3.2) if no exceptions, then remove the remote release branch
                refspec = ":" + releaseBranch;
                buildListener.getLogger().println();
                ReleaseUtils.printInfoIntoBuildLog("Deleting release branch from remote.",
                        buildListener.getLogger());
                gitClient.push().to(new URIish(remoteUrl)).ref(refspec).execute();

                String headCommitHashAfterMerge = writeLatestReleaseRevisionNumber(bld, buildListener);
                log.debug("[WSO2 Maven Release] {}-{} : Written the revision {} ", bld.getProject(),
                        bld.getDisplayName(), headCommitHashAfterMerge);
                ReleaseUtils.printInfoIntoBuildLog("Stored last release commit hash : " + headCommitHashAfterMerge,
                        buildListener.getLogger());

                ReleaseUtils.printSeparator(buildListener);

            } catch (URISyntaxException e) {
                buildListener.fatalError(
                        "[WSO2 Maven Release] " + "Could not parse the git remote URL for project: " + remoteUrl);
                throw new IllegalArgumentException(e.getMessage(), e);
            } catch (GitException e) {
                //this could fail if merging the release commits lead to a conflict.
                ReleaseUtils.printExceptionIntoBuildLog("[ERROR] [WSO2 Maven Release] merging the changes. ", e,
                        buildListener);
                bld.keepLog();
                throw e;
                //todo handle kasung
            }
        }
    }

    private void keepThisBuild(AbstractBuild bld, BuildListener lstnr) throws IOException {
        int buildsKept = 0;
        if (bld.getResult() != null && bld.getResult().isBetterOrEqualTo(Result.SUCCESS)) {
            if (m2ReleaseBuildWrapper.numberOfReleaseBuildsToKeep > 0
                    || m2ReleaseBuildWrapper.numberOfReleaseBuildsToKeep == -1) {
                // keep this build.
                lstnr.getLogger().println("[M2Release] assigning keep build to current build.");
                bld.keepLog();
                buildsKept++;
            }

            // the value may have changed since a previous release so go searching...
            log.debug("looking for extra release builds to lock/unlock.");
            for (Run run : (RunList<? extends Run>) (bld.getProject().getBuilds())) {
                log.debug("checking build #{}", run.getNumber());
                if (isSuccessfulReleaseBuild(run)) {
                    log.info("[WSO2 Maven Release] {} release build #{} was successful.",
                            bld.getProject().getName(), run.getNumber());
                    if (bld.getNumber() != run.getNumber()) { // not sure we still need this check..
                        if (shouldKeepBuildNumber(m2ReleaseBuildWrapper.numberOfReleaseBuildsToKeep, buildsKept)) {
                            buildsKept++;
                            if (!run.isKeepLog()) {
                                lstnr.getLogger()
                                        .println("[M2Release] assigning keep build to build " + run.getNumber());
                                run.keepLog(true);
                            }
                        } else {
                            if (run.isKeepLog()) {
                                lstnr.getLogger()
                                        .println("[M2Release] removing keep build from build " + run.getNumber());
                                run.keepLog(false);
                            }
                        }
                    }
                } else {
                    log.debug(
                            "[WSO2 Maven Release] {} build #{} was either a snapshot build or it was not successful.",
                            bld.getProject().getName(), run.getNumber());
                }
            }
        }
    }

    /**
     * Writes the current head commit hash into jenkins-home/jobs/job-name/lastReleaseRevisionNumber
     *
     * @return the commit hash that was written
     */
    private String writeLatestReleaseRevisionNumber(AbstractBuild bld, TaskListener lstnr) {
        try {
            //write the latest release revision number
            SCM scm = bld.getProject().getScm();
            if (scm instanceof GitSCM) {
                GitSCM gitSCM = (GitSCM) scm;
                AbstractProject project = bld.getProject();
                final EnvVars environment = GitUtils.getPollEnvironment(project, bld.getWorkspace(), launcher,
                        lstnr);
                GitClient gitClient = gitSCM.createClient(lstnr, environment, bld, bld.getWorkspace());
                ObjectId objectId = gitClient.revParse(M2ReleaseBuildWrapper.DEFAULT_REF);
                StringWriter writer = new StringWriter();
                objectId.copyTo(writer);
                String headHash = writer.toString();

                TextFile file = ReleaseUtils.getLastReleaseRevisionNumberFile(project);
                file.write(headHash);
                return headHash;
            }
        } catch (IOException e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw)); //todo
            lstnr.getLogger().println("[WSO2 Maven Release] Error " + e.getMessage() + " " + sw.toString());
        } catch (Throwable e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw)); //todo
            lstnr.getLogger().println("[WSO2 Maven Release] Error " + e.getMessage() + " " + sw.toString());
        }

        return "-";
    }

    /**
     * evaluate if the specified build is a successful release build (not including dry runs)
     * @param run the run to check
     * @return <code>true</code> if this is a successful release build that is not a dry run.
     */
    private boolean isSuccessfulReleaseBuild(Run run) {
        M2ReleaseBadgeAction a = run.getAction(M2ReleaseBadgeAction.class);
        if (a != null && !run.isBuilding() && run.getResult().isBetterOrEqualTo(Result.SUCCESS)) {
            return true;
        }
        return false;
    }

    private boolean shouldKeepBuildNumber(int numToKeep, int numKept) {
        if (numToKeep == -1) {
            return true;
        }
        return numKept < numToKeep;
    }

}