// Append the changelog if we should and can
  private String createBuildNotes(String buildNotes, final ChangeLogSet<?> changeSet) {
    if (appendChangelog) {
      StringBuilder stringBuilder = new StringBuilder();

      // Show the build notes first
      stringBuilder.append(buildNotes);

      // Then append the changelog
      stringBuilder
          .append("\n\n")
          .append(
              changeSet.isEmptySet()
                  ? Messages.TestflightRecorder_EmptyChangeSet()
                  : Messages.TestflightRecorder_Changelog())
          .append("\n");

      int entryNumber = 1;

      for (Entry entry : changeSet) {
        stringBuilder.append("\n").append(entryNumber).append(". ");
        stringBuilder.append(entry.getMsg()).append(" \u2014 ").append(entry.getAuthor());

        entryNumber++;
      }
      buildNotes = stringBuilder.toString();
    }
    return buildNotes;
  }
  private void addTestflightLinks(
      AbstractBuild<?, ?> build, BuildListener listener, Map parsedMap) {
    TestflightBuildAction installAction = new TestflightBuildAction();
    String installUrl = (String) parsedMap.get("install_url");
    installAction.displayName = Messages.TestflightRecorder_InstallLinkText();
    installAction.iconFileName = "package.gif";
    installAction.urlName = installUrl;
    build.addAction(installAction);
    listener.getLogger().println(Messages.TestflightRecorder_InfoInstallLink(installUrl));

    TestflightBuildAction configureAction = new TestflightBuildAction();
    String configUrl = (String) parsedMap.get("config_url");
    configureAction.displayName = Messages.TestflightRecorder_ConfigurationLinkText();
    configureAction.iconFileName = "gear2.gif";
    configureAction.urlName = configUrl;
    build.addAction(configureAction);
    listener.getLogger().println(Messages.TestflightRecorder_InfoConfigurationLink(configUrl));

    build.addAction(new EnvAction());

    // Add info about the selected build into the environment
    EnvAction envData = build.getAction(EnvAction.class);
    if (envData != null) {
      envData.add("TESTFLIGHT_INSTALL_URL", installUrl);
      envData.add("TESTFLIGHT_CONFIG_URL", configUrl);
    }
  }
  @Override
  public boolean perform(
      AbstractBuild<?, ?> build, Launcher launcher, final BuildListener listener) {
    if (build.getResult().isWorseOrEqualTo(Result.FAILURE)) return false;

    listener.getLogger().println(Messages.TestflightRecorder_InfoUploading());

    try {
      EnvVars vars = build.getEnvironment(listener);

      String workspace = vars.expand("$WORKSPACE");

      List<TestflightUploader.UploadRequest> urList =
          new ArrayList<TestflightUploader.UploadRequest>();

      for (TestflightTeam team : createDefaultPlusAdditionalTeams()) {
        try {
          TestflightUploader.UploadRequest ur = createPartialUploadRequest(team, vars, build);
          urList.add(ur);
        } catch (MisconfiguredJobException mje) {
          listener.getLogger().println(mje.getConfigurationMessage());
          return false;
        }
      }

      for (TestflightUploader.UploadRequest ur : urList) {
        TestflightRemoteRecorder remoteRecorder =
            new TestflightRemoteRecorder(workspace, ur, listener);

        final List<Map> parsedMaps;

        try {
          Object result = launcher.getChannel().call(remoteRecorder);
          parsedMaps = (List<Map>) result;
        } catch (UploadException ue) {
          listener
              .getLogger()
              .println(Messages.TestflightRecorder_IncorrectResponseCode(ue.getStatusCode()));
          listener.getLogger().println(ue.getResponseBody());
          return false;
        }

        if (parsedMaps.size() == 0) {
          listener.getLogger().println(Messages.TestflightRecorder_NoUploadedFile(ur.filePaths));
          return false;
        }
        for (Map parsedMap : parsedMaps) {
          addTestflightLinks(build, listener, parsedMap);
        }
      }
    } catch (Throwable e) {
      listener.getLogger().println(e);
      e.printStackTrace(listener.getLogger());
      return false;
    }

    return true;
  }
  private TokenPair getTokenPair(String tokenPairName) {
    for (TokenPair tokenPair : getDescriptor().getTokenPairs()) {
      if (tokenPair.getTokenPairName().equals(tokenPairName)) return tokenPair;
    }

    if (getApiToken() != null && getTeamToken() != null)
      return new TokenPair("", getApiToken(), getTeamToken());

    String tokenPairNameForMessage = tokenPairName != null ? tokenPairName : "(null)";
    throw new MisconfiguredJobException(
        Messages._TestflightRecorder_TokenPairNotFound(tokenPairNameForMessage));
  }
    public FormValidation doCheck(
        @AncestorInPath AbstractProject project, @QueryParameter String value) {
      // Require CONFIGURE permission on this project
      if (!project.hasPermission(Item.CONFIGURE)) return FormValidation.ok();

      for (String name : Util.tokenize(fixNull(value), ",")) {
        name = name.trim();
        if (Jenkins.getInstance().getItem(name, project) == null)
          return FormValidation.error(
              hudson.tasks.Messages.BuildTrigger_NoSuchProject(
                  name, AbstractProject.findNearest(name).getName()));
      }

      return FormValidation.ok();
    }
 /** This human readable name is used in the configuration screen. */
 public String getDisplayName() {
   return Messages.TestflightRecorder_UploadLinkText();
 }
 public String getDisplayName() {
   return Messages.MavenModuleSet_DiplayName();
 }