| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF 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.apache.openejb.tools.release.cmd; |
| |
| import org.apache.http.client.methods.CloseableHttpResponse; |
| import org.apache.http.client.methods.HttpGet; |
| import org.apache.http.impl.client.CloseableHttpClient; |
| import org.apache.http.impl.client.HttpClientBuilder; |
| import org.apache.http.util.EntityUtils; |
| import org.tomitribe.crest.api.Command; |
| import org.tomitribe.crest.api.Default; |
| import org.tomitribe.crest.api.Option; |
| import org.tomitribe.crest.api.Out; |
| import org.tomitribe.swizzle.stream.StreamBuilder; |
| import org.tomitribe.util.Files; |
| import org.tomitribe.util.Hex; |
| import org.tomitribe.util.IO; |
| import org.tomitribe.util.dir.Dir; |
| import org.tomitribe.util.dir.Filter; |
| import org.tomitribe.util.dir.Walk; |
| |
| import java.io.File; |
| import java.io.FileFilter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.PrintStream; |
| import java.io.UncheckedIOException; |
| import java.net.URI; |
| import java.security.DigestInputStream; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.function.Consumer; |
| import java.util.function.Predicate; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| |
| import static java.lang.String.format; |
| import static org.apache.openejb.tools.release.util.Exec.exec; |
| import static org.apache.openejb.tools.release.util.Exec.read; |
| |
| @Command |
| public class Dist { |
| |
| /** |
| * Download binaries from a maven repo and commit them to dist.apache.org dev |
| * |
| * The org/apache/tomee/apache-tomee and org/apache/tomee/tomee-project sections |
| * of the maven repository will be scanned for the version specified and all zip |
| * and tar.gz files will be downloaded along with any associated asc and sha1 files. |
| * After download the sha1 files of each binary will be checked to ensure a complete |
| * download. The sha256 and sha512 file for each binary will be computed and written |
| * to disk. |
| * |
| * If the --dry-run flag is not enabled, the resulting zip, tar.gz, asc, sha256 and |
| * sha512 files will be uploaded to a directory in dist.apache.org dev or the specified |
| * svn repo. |
| * |
| * When ready, the dist.apache.org dev directory can be moved to dist.apache.org release |
| * via the `dist dev-to-release` command. |
| * |
| * @param version The TomEE version being published. Example: 8.0.7 |
| * @param tmp The directory under which files can be temporarily downloaded |
| * @param mavenRepoUri The root path of a Nexus staging repository or Maven Central |
| * @param svnRepo The svn directory for tomee where a subdirectory can be created and binaries uploaded |
| * @param dryRun Download the files to local disk, but do not commit them to svn |
| */ |
| @Command("maven-to-dev") |
| public void mavenToDev(final String version, |
| @Option("tmp") @Default("/tmp/") final File tmp, |
| @Option("maven-repo") @Default("https://repo1.maven.org/maven2/") final URI mavenRepoUri, |
| @Option("svn-repo") @Default("https://dist.apache.org/repos/dist/dev/tomee/") final URI svnRepo, |
| @Option("dry-run") @Default("false") final boolean dryRun, |
| final @Out PrintStream out) throws IOException { |
| |
| |
| final String build = buildId(mavenRepoUri); |
| final String tomeeVersionName = "tomee-" + version; |
| final String svnBinaryLocation = format("https://dist.apache.org/repos/dist/dev/tomee/staging-%s/%s", build, tomeeVersionName); |
| |
| final File dir = new File(tmp, "tomee-" + version + "-work"); |
| |
| { // Make and checkout the binaries dir in svn |
| if (!dir.exists()) { |
| Files.mkdirs(dir); |
| } |
| |
| if (!dryRun) { |
| exec("svn", "-m", format("[release-tools] staged binary dir for %s", tomeeVersionName), "mkdir", "--parents", svnBinaryLocation); |
| exec("svn", "co", svnBinaryLocation, dir.getAbsolutePath()); |
| } |
| } |
| |
| |
| final MavenRepo repo = new MavenRepo(mavenRepoUri, out); |
| |
| final List<URI> binaries = new ArrayList<>(); |
| binaries.addAll(repo.binaries("org/apache/tomee/apache-tomee/", version)); |
| binaries.addAll(repo.binaries("org/apache/tomee/tomee-project/", version)); |
| |
| binaries.forEach(repo.downloadTo(dir)); |
| out.printf("Downloaded %s binaries to %s%n", binaries.size(), dir.getAbsolutePath()); |
| |
| final Work work = Dir.of(Work.class, dir); |
| |
| final List<Binary> invalid = work.binaries() |
| .filter(((Predicate<Binary>) Binary::verifySha1).negate()) |
| .collect(Collectors.toList()); |
| |
| if (invalid.size() != 0) { |
| invalid.forEach(binary -> out.printf("SHA1 check failed %s%n", binary.get().getAbsolutePath())); |
| throw new CommandFailedException("Remove the invalid files and try again"); |
| } |
| |
| work.binaries() |
| .peek(Binary::createSha256) |
| .peek(Binary::createSha512) |
| .forEach(binary -> out.println("Hashed " + binary.get().getName())); |
| |
| if (!dryRun) { |
| |
| Consumer<File> svnAdd = file -> exec("svn", "add", file.getAbsolutePath()); |
| |
| work.binaries() |
| .peek(binary -> svnAdd.accept(binary.get())) |
| .peek(binary -> svnAdd.accept(binary.asc())) |
| .peek(binary -> svnAdd.accept(binary.sha256())) |
| .peek(binary -> svnAdd.accept(binary.sha512())) |
| .forEach(binary -> out.println("Added " + binary.get().getName())); |
| |
| exec("svn", "-m", format("[release-tools] staged binaries for %s", tomeeVersionName), "ci", dir.getAbsolutePath()); |
| |
| out.printf("Binaries published to %s%n", svnBinaryLocation); |
| } |
| } |
| |
| /** |
| * Move binaries dist.apache.org dev to dist.apache.org release |
| * |
| * Looks for directories under the specified stagedDir in dist.apache.org dev section and moves each into the mirror |
| * system under dist.apache.org release section. For example, given the following staged directory in svn: |
| * |
| * svn list https://dist.apache.org/repos/dist/dev/tomee/staging-1179/ |
| * tomee-8.0.7/ |
| * tomee-9.0.0.-M7/ |
| * |
| * The command below would move the "tomee-8.0.7" and "tomee-9.0.0.-M7" into dist.apache.org/repos/dist/release/tomee/ |
| * where they will become available on the Apache mirror system within 24 hours. Please note that it does take some |
| * time for things to propagate, so any updates to the download page should not be done till about 24 hours after this |
| * command is run. |
| * |
| * Once this command is run it is a good idea to use `dist remove-release` to remove any older releases from the mirror |
| * system that we no longer need. |
| * |
| * @param stagingDir The name of the staging directory to release. Example: staging-1179 |
| * @param dev The specific location in dist.apache.org dev where this project's binaries are staged |
| * @param release The specific location in dist.apache.org release where this project's binaries are promoted |
| */ |
| @Command("dev-to-release") |
| public void release(final String stagingDir, |
| @Option("dev-repo") @Default("https://dist.apache.org/repos/dist/dev/tomee/") final URI dev, |
| @Option("release-repo") @Default("https://dist.apache.org/repos/dist/release/tomee/") final URI release, |
| final @Out PrintStream out) throws IOException { |
| |
| final URI stagingUri = dev.resolve(stagingDir + "/"); |
| final String contents = IO.slurp(read("svn", "list", stagingUri.toASCIIString())); |
| final String[] dirs = contents.split("[\n /]+"); |
| |
| for (final String dir : dirs) { |
| final URI dirUri = stagingUri.resolve(dir); |
| out.printf("Promoting %s/%s%n", stagingDir, dir); |
| exec("svn", "-m", format("[release-tools] promote staged binaries for %s", dir), "mv", dirUri.toASCIIString(), release.toASCIIString()); |
| } |
| |
| out.printf("Removing %s%n", stagingUri); |
| exec("svn", "-m", format("[release-tools] remove staged directory %s", stagingDir), "rm", stagingUri.toASCIIString()); |
| |
| out.printf("Listing %s%n", release); |
| exec("svn", "list", release.toASCIIString()); |
| } |
| |
| /** |
| * Removes an older release from the mirror system. To view all existing releases simply execute `dist list-releases` |
| * If there are too many releases in our release directory, infra will ask us to remove the older binaries as they |
| * are available in archive.apache.org. After executing `dist dev-to-release` it is a good idea to clean up any |
| * previous releases that are no longer necessary. |
| * |
| * @param releaseDirectory The release directory to remove from the mirror system. Example: tomee-9.0.0-M3 |
| * @param releases The specific location in dist.apache.org release where this project's binaries are promoted |
| */ |
| @Command("remove-release") |
| public void removeRelease(final String releaseDirectory, |
| @Option("release-repo") @Default("https://dist.apache.org/repos/dist/release/tomee/") final URI releases, |
| final @Out PrintStream out) throws IOException { |
| |
| final URI releaseUri = releases.resolve(releaseDirectory); |
| exec("svn", "-m", format("[release-tools] remove release %s", releaseDirectory), "rm", releaseUri.toASCIIString()); |
| } |
| |
| /** |
| * Lists releases currently on the mirror system. |
| * |
| * @param releases The specific location in dist.apache.org release where this project's binaries are promoted |
| */ |
| @Command("list-releases") |
| public void listReleases(@Option("release-repo") @Default("https://dist.apache.org/repos/dist/release/tomee/") final URI releases, |
| final @Out PrintStream out) throws IOException { |
| |
| exec("svn", "list", releases.toASCIIString()); |
| } |
| |
| @Command("add-key") |
| public void addKey(final File publicKey, |
| @Option("release-repo") @Default("https://dist.apache.org/repos/dist/release/tomee/") final URI releases, |
| @Out final PrintStream out) throws IOException { |
| |
| final File tmpdir = Files.tmpdir(); |
| //svn checkout https://dist.apache.org/repos/dist/release/tomee/ --depth files |
| exec("svn", "checkout", releases.toASCIIString(), tmpdir.getAbsolutePath(), "--depth", "files"); |
| |
| final File keys = new File(tmpdir, "KEYS"); |
| final String contents = IO.slurp(publicKey); |
| try (final PrintStream keysStream = new PrintStream(IO.write(keys, true))) { |
| keysStream.println(); |
| keysStream.println(contents); |
| } |
| final URI keysUri = releases.resolve("KEYS"); |
| exec("svn", "-m", format("[release-tools] add key to %s", keysUri), "ci", keys.getAbsolutePath()); |
| out.printf("Key added to %s%n", keysUri); |
| } |
| |
| /** |
| * Return the last digits of a Nexus staging repo dir such as orgapachetomee-1136 or |
| * return the month and day as a default. |
| */ |
| private String buildId(final URI stagingRepoUri) { |
| final String id = stagingRepoUri.getPath().replaceAll(".*-([0-9]+)/?$", "$1"); |
| if (id.matches("^[0-9]+$")) { |
| return id; |
| } |
| |
| final SimpleDateFormat format = new SimpleDateFormat("MMdd"); |
| return format.format(new Date()); |
| } |
| |
| public static class MavenRepo { |
| private final CloseableHttpClient client = HttpClientBuilder.create().build(); |
| private final URI repo; |
| private final PrintStream out; |
| |
| public MavenRepo(final URI repo, final PrintStream out) { |
| this.repo = repo; |
| this.out = out; |
| } |
| |
| public List<URI> binaries(final String artifactPath, final String version) throws IOException { |
| final URI artifactDir = this.repo.resolve(artifactPath); |
| |
| final URI versionDir = artifactDir.resolve(version + "/"); |
| final CloseableHttpResponse response = get(versionDir); |
| |
| final List<String> hrefs = new ArrayList<>(); |
| StreamBuilder.create(response.getEntity().getContent()) |
| .watch("<a href=\"", "\"", hrefs::add) |
| .run(); |
| |
| final Predicate<String> acceptedExtensions = Pattern.compile("\\.(zip|tar\\.gz)(\\.(asc|sha1))?$").asPredicate(); |
| return hrefs.stream() |
| .filter(acceptedExtensions) |
| .map(versionDir::resolve) |
| .collect(Collectors.toList()); |
| } |
| |
| private CloseableHttpResponse get(final URI uri) throws IOException { |
| final CloseableHttpResponse response = client.execute(new HttpGet(uri)); |
| if (response.getStatusLine().getStatusCode() != 200) { |
| EntityUtils.consume(response.getEntity()); |
| throw new UnexpectedHttpResponseException("GET", uri, response.getStatusLine()); |
| } |
| return response; |
| } |
| |
| public Consumer<URI> downloadTo(final File directory) { |
| return downloadTo(directory, false); |
| } |
| |
| public Consumer<URI> downloadTo(final File directory, final boolean overwrite) { |
| return uri -> { |
| try { |
| final String name = uri.getPath().replaceAll(".*/", ""); |
| final File file = new File(directory, name); |
| |
| if (file.exists() && !overwrite) { |
| out.println("Downloaded " + uri); |
| } else { |
| out.println("Downloading " + uri); |
| final CloseableHttpResponse response = get(uri); |
| IO.copy(response.getEntity().getContent(), file); |
| } |
| } catch (IOException e) { |
| throw new UncheckedIOException(e); |
| } |
| }; |
| } |
| } |
| |
| public interface Work extends Dir { |
| @Walk(maxDepth = 1) |
| @Filter(Binary.Format.class) |
| Stream<Binary> binaries(); |
| } |
| |
| public interface Binary extends Dir { |
| |
| default boolean verifySha1() { |
| final String expectedSha1 = slurp(sha1()); |
| final String actualSha1 = hash("SHA-1"); |
| return expectedSha1.equals(actualSha1); |
| } |
| |
| default void createSha256() { |
| final String sha256 = hash("SHA-256"); |
| write(sha256, sha256()); |
| } |
| |
| default void createSha512() { |
| final String sha256 = hash("SHA-512"); |
| write(sha256, sha512()); |
| } |
| |
| default File asc() { |
| return get(get(), "asc"); |
| } |
| |
| default File sha1() { |
| return get(get(), "sha1"); |
| } |
| |
| default File sha256() { |
| return get(get(), "sha256"); |
| } |
| |
| default File sha512() { |
| return get(get(), "sha512"); |
| } |
| |
| static void write(String content, File file) { |
| try { |
| IO.copy(IO.read(content), file); |
| } catch (IOException e) { |
| throw new UncheckedIOException("Unable to write to file " + file.getAbsolutePath(), e); |
| } |
| } |
| |
| default File get(final File file, final String ext) { |
| return new File(file.getParentFile(), file.getName() + "." + ext); |
| } |
| |
| default String hash(final String type) { |
| try { |
| final MessageDigest digest = MessageDigest.getInstance(type); |
| try (final InputStream inputStream = IO.read(get())) { |
| final DigestInputStream digestInputStream = new DigestInputStream(inputStream, digest); |
| IO.copy(digestInputStream, IO.IGNORE_OUTPUT); |
| return Hex.toString(digest.digest()); |
| } |
| } catch (NoSuchAlgorithmException e) { |
| throw new IllegalStateException("Unknown algorithm " + type, e); |
| } catch (IOException e) { |
| throw new UncheckedIOException(e); |
| } |
| } |
| |
| static String slurp(final File file) { |
| try { |
| return IO.slurp(file); |
| } catch (IOException e) { |
| throw new UncheckedIOException("Cannot read file " + file.getAbsolutePath(), e); |
| } |
| } |
| |
| class Format implements FileFilter { |
| @Override |
| public boolean accept(final File file) { |
| final String name = file.getName(); |
| return name.endsWith(".zip") || name.endsWith(".tar.gz"); |
| } |
| } |
| } |
| |
| } |