-
Ryan Laurie authored
* Support manual release channels s3 tag fix check generate matrix generate matrix add git tagging channel observability version info update handle some edge cases * remove testing code * update unit tests * support rollout percentage for latest channel * update rollout onlyh * update unit tests * address review comments
Ryan Laurie authored* Support manual release channels s3 tag fix check generate matrix generate matrix add git tagging channel observability version info update handle some edge cases * remove testing code * update unit tests * support rollout percentage for latest channel * update rollout onlyh * update unit tests * address review comments
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
release-offline.ts 11.38 KiB
/**
* Note: This is a backup and failsafe if you cannot use the release github actions in CI for some reason.
* The environment consistency and observability of the release process is very important, so
* you should only use this script if absolutely necessary, and you cannot use CI for some very
* good reason.
*/
import "dotenv/config";
import { Octokit } from "@octokit/rest";
import "zx/globals";
import { $ } from "zx";
$.verbose = false;
import {
isValidVersionString,
hasBeenReleased,
isValidCommitHash,
isEnterpriseVersion,
getMajorVersion,
getVersionInfo,
publishRelease,
closeMilestone,
openNextMilestones,
versionRequirements,
} from "./src";
const {
GITHUB_TOKEN,
GITHUB_OWNER,
GITHUB_REPO,
AWS_S3_DOWNLOADS_BUCKET,
AWS_S3_STATIC_BUCKET,
AWS_CLOUDFRONT_DOWNLOADS_ID,
AWS_CLOUDFRONT_STATIC_ID,
AWS_SECRET_ACCESS_KEY,
AWS_ACCESS_KEY_ID,
DOCKERHUB_OWNER,
DOCKERHUB_RELEASE_USERNAME,
DOCKERHUB_RELEASE_TOKEN,
} = process.env;
const github = new Octokit({ auth: GITHUB_TOKEN });
const JAR_PATH = "../target/uberjar";
const version = process.argv?.[2]?.trim();
const commitHash = process.argv?.[3]?.trim();
const step = process.argv?.[4]?.trim().replace("--", "");
const isWithoutGithub = process.argv?.[5]?.trim() === "--without-github";
let latestFlag: string | boolean | null = process.argv?.[6]?.trim();
const log = (message, color = "blue") =>
// eslint-disable-next-line no-console
console.log(chalk[color](`\n${message}\n`));
function error(message) {
log(`⚠️ ${message}`, "red");
process.exit(1);
}
// need to force the user to set the latest tag from the command line if they are releasing without github
if (isWithoutGithub) {
if (!latestFlag) {
error('If you are releasing without github you must pass --latest or --not-latest as the last argument');
}
latestFlag = (latestFlag === "--latest") ? true : false;
}
const edition = isEnterpriseVersion(version) ? "ee" : "oss";
if (!isValidVersionString(version)) {
error(
"You must provide a valid version string as the first argument (e.g v0.45.6)",
);
}
if (!isValidCommitHash(commitHash)) {
error("You must provide a valid commit hash as the second argument");
}
if (!step) {
error("You must provide a step argument like --build or --publish");
}
/**************************************************
HELPERS
**************************************************/
// mostly for type checking
function getGithubCredentials() {
if (isWithoutGithub) {
return { GITHUB_TOKEN: '', GITHUB_OWNER: '', GITHUB_REPO: '' };
}
if (!GITHUB_TOKEN || !GITHUB_OWNER || !GITHUB_REPO) {
error("You must provide all github environment variables in .env-template");
process.exit(1);
}
return { GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO };
}
function getAWSCredentials() {
if (
!AWS_S3_DOWNLOADS_BUCKET ||
!AWS_S3_STATIC_BUCKET ||
!AWS_CLOUDFRONT_STATIC_ID ||
!AWS_CLOUDFRONT_DOWNLOADS_ID ||
!AWS_SECRET_ACCESS_KEY ||
!AWS_ACCESS_KEY_ID
) {
error("You must provide all AWS environment variables in .env-template");
process.exit(1);
}
return {
AWS_S3_DOWNLOADS_BUCKET,
AWS_S3_STATIC_BUCKET,
AWS_CLOUDFRONT_DOWNLOADS_ID,
AWS_CLOUDFRONT_STATIC_ID,
};
}
function getDockerCredentials() {
if (
!DOCKERHUB_RELEASE_USERNAME ||
!DOCKERHUB_RELEASE_TOKEN ||
!DOCKERHUB_OWNER
) {
error("You must provide all docker environment variables in .env-template");
process.exit(1);
}
return {
DOCKERHUB_RELEASE_USERNAME,
DOCKERHUB_RELEASE_TOKEN,
DOCKERHUB_OWNER,
};
}
async function checkReleased() {
const { GITHUB_OWNER, GITHUB_REPO } = getGithubCredentials();
const hasThisVersionBeenReleased = await hasBeenReleased({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version,
});
if (hasThisVersionBeenReleased) {
error(`Version ${version} has already been released`);
}
}
async function checkJar() {
// check if built jar exists
const requiredFiles = [
`${JAR_PATH}/metabase.jar`,
`${JAR_PATH}/COMMIT-ID`,
`${JAR_PATH}/SHA256.sum`,
];
requiredFiles.forEach(file => {
if (!fs.existsSync(file)) {
error(`You must build the jar first. ${file} does not exist`);
}
});
const versionProperties = (
await $`jar xf ${JAR_PATH}/metabase.jar version.properties && cat version.properties`
).toString();
log(versionProperties, "green");
if (!versionProperties.includes(`tag=${version}`)) {
error(`This jar does not match the input version: ${version}`);
}
}
/**************************************************
BUILD STEP
**************************************************/
async function build() {
log(`🚀 Building ${edition} jar for ${version} from commit ${commitHash}`);
const unstagedChanges = (await $`git status --porcelain`).toString().trim();
if (unstagedChanges) {
error(
`You have unstaged changes:\n\n ${unstagedChanges}\n\nPlease commit or stash them and try again`,
);
}
// check build environment
const majorVersion = Number(getMajorVersion(version));
const nodeVersion = (await $`node --version`).toString();
const javaVersion = (await $`java -version`).toString();
if (!nodeVersion.includes(`v${versionRequirements[majorVersion].node}`)) {
error(
`Node version must be v${versionRequirements[majorVersion].node}, you are running ${nodeVersion}`,
);
}
if (
!javaVersion.includes(
`openjdk version "${versionRequirements[majorVersion].java}`,
)
) {
error(
`Java version must be ${versionRequirements[majorVersion].java}, you are running ${javaVersion}`,
);
}
const currentBranch = (await $`git branch --show-current`).toString().trim();
try {
await $`git fetch --all`;
await $`git stash && git checkout ${commitHash}`;
// actually build jar
await $`../bin/build.sh :edition :${edition} :version ${version}`.pipe(
process.stdout,
);
await $`git checkout -`;
await $`echo ${commitHash} > ${JAR_PATH}/COMMIT-ID`;
await $`shasum -a 256 ${JAR_PATH}/metabase.jar > ${JAR_PATH}/SHA256.sum`;
log(`✅ Built ${edition} jar for ${version} in ${JAR_PATH}`, "green");
} catch (error) {
console.error(error);
} finally {
await $`git checkout ${currentBranch}`;
}
}
/**************************************************
PUBLISH STEPS
***************************************************/
async function s3() {
log(`⏳ Publishing ${version} to s3`);
const { GITHUB_OWNER, GITHUB_REPO } = getGithubCredentials();
const { AWS_S3_DOWNLOADS_BUCKET } = getAWSCredentials();
await checkJar();
const versionPath = edition === "ee" ? `enterprise/${version}` : version;
await $`aws s3 cp ${JAR_PATH}/metabase.jar s3://${AWS_S3_DOWNLOADS_BUCKET}/${versionPath}/metabase.jar`.pipe(
process.stdout,
);
await $`aws cloudfront create-invalidation \
--distribution-id ${AWS_CLOUDFRONT_DOWNLOADS_ID} \
--paths /${versionPath}/metabase.jar`.pipe(process.stdout);
log(`✅ Published ${version} to s3`);
}
async function docker() {
log(`⏳ Building docker image for ${version}`);
const { GITHUB_OWNER, GITHUB_REPO } = getGithubCredentials();
const {
DOCKERHUB_RELEASE_USERNAME,
DOCKERHUB_RELEASE_TOKEN,
DOCKERHUB_OWNER,
} = getDockerCredentials();
await checkJar();
await $`cp -r ${JAR_PATH}/metabase.jar ../bin/docker/`;
const dockerRepo = edition === "ee" ? "metabase-enterprise" : "metabase";
const dockerTag = `${DOCKERHUB_OWNER}/${dockerRepo}:${version}`;
await $`docker build --tag ${dockerTag} ../bin/docker/.`.pipe(process.stdout);
log(`⏳ Pushing docker image to dockerhub for ${version}`);
await $`docker login --username ${DOCKERHUB_RELEASE_USERNAME} -p ${DOCKERHUB_RELEASE_TOKEN}`;
await $`docker push ${dockerTag}`.pipe(process.stdout);
log(`✅ Published ${dockerTag} to DockerHub`);
}
async function versionInfo() {
log(`⏳ Building version-info.json`);
const { GITHUB_OWNER, GITHUB_REPO } = getGithubCredentials();
const { AWS_S3_STATIC_BUCKET, AWS_CLOUDFRONT_STATIC_ID } =
getAWSCredentials();
const newVersionInfo = await getVersionInfo({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version,
});
const versionInfoName =
edition === "ee" ? "version-info-ee.json" : "version-info.json";
fs.writeFileSync(versionInfoName, JSON.stringify(newVersionInfo, null, 2));
await $`aws s3 cp ${versionInfoName} s3://${AWS_S3_STATIC_BUCKET}/${versionInfoName}`.pipe(
process.stdout,
);
await $`aws cloudfront create-invalidation \
--distribution-id ${AWS_CLOUDFRONT_STATIC_ID} \
--paths /${versionInfoName}`.pipe(process.stdout);
log(`✅ Published ${versionInfoName} to s3`);
}
async function tag() {
// tag commit
await $`git tag ${version} ${commitHash}`;
await $`git push origin ${version}`.pipe(process.stdout);
log(`✅ Tagged ${version}`);
}
async function releaseNotes() {
const { GITHUB_OWNER, GITHUB_REPO } = getGithubCredentials();
log(`⏳ Building Release Notes for ${version}`);
const checksum = (await $`shasum -a 256 ${JAR_PATH}/metabase.jar`)
.toString()
.split(" ")[0];
await publishRelease({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version,
checksum,
});
log(`✅ Published release notes for ${version} to github\n`);
}
async function updateMilestones() {
const { GITHUB_OWNER, GITHUB_REPO } = getGithubCredentials();
await closeMilestone({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version,
});
await openNextMilestones({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version,
});
}
/**************************************************
MAIN CONTROL FUNCTION
**************************************************/
(async () => {
if (step === "build") {
await checkReleased();
await build();
}
if (step === "publish") {
log(`🚀 Publishing ${edition} ${version} 🚀`);
if ( isWithoutGithub ) {
log(`⚠️ Skipping github steps because --without-github was passed ⚠️`, "yellow");
await s3();
await docker();
const remainingSteps = [
"release notes were not published",
"version-info.json was not updated",
"no commit was tagged",
"no milestones were updated",
].join("\n ❌ ");
log(`Because you released without github the following steps were not completed:\n ❌ ${remainingSteps}`);
return;
}
await checkReleased();
await s3();
await docker();
await versionInfo();
await tag();
await releaseNotes();
await updateMilestones();
log(`✅ Published ${edition} ${version}`);
const remainingSteps = [
"Publish the release notes",
"Reorder commits to make sure OSS is first",
"Submit a pull request to build the docker image for cloud",
"Create an issue to update cloud instances",
"Update documentation on the website to reflect the newly released versions",
].join("\n ✔️ ");
log(`Don't forget, you still need to:\n ✔️ ${remainingSteps}`);
}
if (step === "check-jar") {
await checkJar();
}
if (step === "s3") {
await s3();
}
if (step === "docker") {
await docker();
}
if (step === "version-info") {
await versionInfo();
}
if (step === "tag") {
await tag();
}
if (step === "release-notes") {
await releaseNotes();
}
if (step === "update-milestones") {
await updateMilestones();
}
})();