Skip to content
Snippets Groups Projects
Unverified Commit f8a5a30d authored by Ryan Laurie's avatar Ryan Laurie Committed by GitHub
Browse files

Separate Releasing from "Latest" release determination (#47868)

* 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
parent 13d8dabd
Branches
Tags
No related merge requests found
Showing
with 801 additions and 363 deletions
......@@ -10,13 +10,19 @@ on:
required: true
schedule:
- cron: '45 * * * *' # hourly
workflow_call:
inputs:
version:
description: 'Major Metabase version (e.g. 45, 52, 68)'
type: number
required: true
jobs:
update-release-log:
runs-on: ubuntu-latest
timeout-minutes: 5
env:
VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || 50 }} # Update this for the next major release
VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || vars.CURRENT_VERSION }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
......@@ -31,11 +37,19 @@ jobs:
run: yarn --cwd release --frozen-lockfile && npm i -g tsx
- name: generate release Log
run: cd release && tsx ./src/release-log.ts $VERSION > v$VERSION.html
- name: generate release channel log
run: cd release && tsx ./src/release-channel-log.ts > channels.html
- name: upload release log to the web
run: |
aws s3 cp \
release/v$VERSION.html \
s3://${{ vars.AWS_S3_STATIC_BUCKET }}/release-log/v$VERSION.html
- name: upload release channel log to the web
run: |
aws s3 cp \
release/channels.html \
s3://${{ vars.AWS_S3_STATIC_BUCKET }}/release-log/channels.html
- name: Create cloudfront invalidation
run: |
aws cloudfront create-invalidation \
......
......@@ -31,8 +31,9 @@ jobs:
hasCommitBeenReleased,
} = require('${{ github.workspace }}/release/dist/index.cjs');
// Don't forget to update this for the next major release 🤞
const AUTO_RELEASE_VERSIONS = [49, 50];
const currentRelease = Number('${{ vars.CURRENT_VERSION }}');
const lastRelease = currentRelease - 1;
const AUTO_RELEASE_VERSIONS = [lastRelease, currentRelease];
async function releasePatchFor(majorVersion) {
const nextPatch = await getNextPatchVersion({
......
name: Release 4 - Set Release Channel
run-name: Set ${{ inputs.version }} as ${{ inputs.tag_name }} ( ${{ inputs.tag_ee && 'EE' || '' }} ${{ inputs.tag_oss && 'OSS' || '' }} )
on:
workflow_dispatch:
inputs:
version:
description: Metabase version (e.g. v0.46.3)
type: string
required: true
tag_name:
description: Tag name to apply to this release
type: choice
options:
- nightly
- beta
- latest
required: true
tag_rollout:
description: Rollout % (0-100)
type: number
default: 100
tag_ee:
description: Apply to EE
type: boolean
default: true
tag_oss:
description: Apply to OSS
type: boolean
default: true
jobs:
check-version:
runs-on: ubuntu-22.04
timeout-minutes: 5
outputs:
ee: ${{ fromJson(steps.canonical_version.outputs.result).ee }}
oss: ${{ fromJson(steps.canonical_version.outputs.result).oss }}
edition_matrix: ${{ steps.edition_matrix.outputs.result }}
steps:
- name: Fail early on the incorrect version format
if: ${{ !(startsWith(inputs.version,'v0.') || startsWith(inputs.version,'v1.')) }}
run: |
echo "The version format is invalid!"
echo "It must start with either 'v0.' or 'v1.'."
echo "Please, try again."
exit 1
- uses: actions/checkout@v4
with:
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Get Release Version
uses: actions/github-script@v7
id: canonical_version
with:
script: | # js
const { isValidVersionString, getCanonicalVersion, hasBeenReleased } = require('${{ github.workspace }}/release/dist/index.cjs');
const version = '${{ inputs.version }}';
if (!isValidVersionString(version)) {
throw new Error("The version format is invalid!");
}
const versions = {
ee: getCanonicalVersion(version, 'ee'),
oss: getCanonicalVersion(version, 'oss'),
};
const ossReleased = await hasBeenReleased({
github,
owner: context.repo.owner,
repo: context.repo.repo,
version: versions.oss,
});
const eeReleased = await hasBeenReleased({
github,
owner: context.repo.owner,
repo: context.repo.repo,
version: versions.ee,
});
if (!ossReleased || !eeReleased) {
throw new Error("This version has not been released yet!", version);
}
return versions;
- name: Get Edition matrix
uses: actions/github-script@v7
id: edition_matrix
with:
script: | # js
const tag_oss = ${{ inputs.tag_oss }};
const tag_ee = ${{ inputs.tag_ee }};
if (tag_oss && tag_ee) {
return ["oss", "ee"];
}
if (tag_oss) {
return ["oss"];
}
if (tag_ee) {
return ["ee"];
}
throw new Error("No edition selected to tag");
copy-to-s3:
runs-on: ubuntu-22.04
needs: check-version
timeout-minutes: 5
strategy:
matrix:
edition: ${{ fromJson(needs.check-version.outputs.edition_matrix) }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_S3_RELEASE_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_S3_RELEASE_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
- name: Determine the source path
uses: actions/github-script@v7
id: source_path
with:
result-encoding: string
script: | # js
const version = '${{ inputs.version }}';
const edition = '${{ matrix.edition }}';
const source_path = edition === 'ee'
? 'enterprise/' + version.replace(/^v0\./, "v1.") // always e.g. v1.47.2
: version.replace(/^v1\./, "v0."); // always e.g. v0.45.6;
console.log("The source path for this", edition, "edition is", source_path);
return source_path;
- name: Determine upload path
uses: actions/github-script@v7
id: upload_path
with:
result-encoding: string
script: | # js
const edition = '${{ matrix.edition }}';
const tagName = '${{ inputs.tag_name }}';
const OSSversion = '${{ needs.check-version.outputs.oss }}';
const EEversion = '${{ needs.check-version.outputs.ee }}';
const upload_path = edition === 'ee'
? `enterprise/${tagName}`
: tagName;
console.log("The upload path for this", edition, "edition is", upload_path);
return upload_path;
- name: Upload to s3 latest path
run: | # sh
aws s3 cp \
s3://${{ vars.AWS_S3_DOWNLOADS_BUCKET }}/${{ steps.source_path.outputs.result }}/metabase.jar \
s3://${{ vars.AWS_S3_DOWNLOADS_BUCKET }}/${{ steps.upload_path.outputs.result }}/metabase.jar
- name: Create cloudfront invalidation
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ vars.AWS_CLOUDFRONT_DOWNLOADS_ID }} \
--paths /${{ steps.upload_path.outputs.result }}/metabase.jar
- name: Download the jars
run: | # sh
curl -L http://${{ vars.AWS_S3_DOWNLOADS_BUCKET }}.s3.${{ vars.AWS_REGION }}.amazonaws.com/${{ steps.source_path.outputs.result }}/metabase.jar -o metabase-source.jar
curl -L http://${{ vars.AWS_S3_DOWNLOADS_BUCKET }}.s3.${{ vars.AWS_REGION }}.amazonaws.com/${{ steps.upload_path.outputs.result }}/metabase.jar -o metabase-tagged.jar
- name: Verify Checksums match
run: | # sh
SOURCE_CHECKSUM=$(sha256sum ./metabase-source.jar | awk '{print $1}')
TAGGED_CHECKSUM=$(sha256sum ./metabase-tagged.jar | awk '{print $1}')
echo "Source Checksum: $SOURCE_CHECKSUM"
echo "Tagged Checksum: $TAGGED_CHECKSUM"
if [[ "$SOURCE_CHECKSUM" != "$TAGGED_CHECKSUM" ]]; then
echo "jar Checksums do not match!"
exit 1
fi
tag-docker-image:
runs-on: ubuntu-22.04
needs: check-version
timeout-minutes: 5
strategy:
matrix:
edition: ${{ fromJson(needs.check-version.outputs.edition_matrix) }}
env:
TAG_NAME: ${{ inputs.tag_name }}
steps:
- name: Determine the Docker Hub repository
run: | # sh
if [[ "${{ matrix.edition }}" == "ee" ]]; then
echo "Metabase EE: image is going to be pushed to ${{ github.repository_owner }}/metabase-enterprise"
echo "DOCKERHUB_REPO=${{ github.repository_owner }}/metabase-enterprise" >> $GITHUB_ENV
echo "DOCKERHUB_VERSION=${{ needs.check-version.outputs.ee }}" >> $GITHUB_ENV
else
echo "Metabase OSS: image is going to be pushed to ${{ github.repository_owner }}/metabase"
echo "DOCKERHUB_REPO=${{ github.repository_owner }}/metabase" >> $GITHUB_ENV
echo "DOCKERHUB_VERSION=${{ needs.check-version.outputs.oss }}" >> $GITHUB_ENV
fi
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_RELEASE_USERNAME }}
password: ${{ secrets.DOCKERHUB_RELEASE_TOKEN }}
- name: Tag the docker image
run: | # sh
docker pull $DOCKERHUB_REPO\:$DOCKERHUB_VERSION
docker tag $DOCKERHUB_REPO\:$DOCKERHUB_VERSION $DOCKERHUB_REPO\:$TAG_NAME
docker image ls $DOCKERHUB_REPO
SOURCE_ID=$(docker image ls $DOCKERHUB_REPO\:$DOCKERHUB_VERSION -q)
TAGGED_ID=$(docker image ls $DOCKERHUB_REPO\:$TAG_NAME -q)
if [[ $SOURCE_ID != $TAGGED_ID ]]; then
echo "Error tagging docker image"
exit 1
fi
docker push $DOCKERHUB_REPO\:$TAG_NAME
push-git-tags:
permissions: write-all
needs: check-version
runs-on: ubuntu-22.04
timeout-minutes: 5
strategy:
matrix:
edition: ${{ fromJson(needs.check-version.outputs.edition_matrix) }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # fetch all history
- name: Add and push git tag
run: | # sh
if [[ "${{ matrix.edition }}" == "ee" ]]; then
git tag -f ${{ inputs.tag_name }}-ee ${{ needs.check-version.outputs.ee }}
git push origin -f ${{ inputs.tag_name }}-ee
elif [[ "${{ matrix.edition }}" == "oss" ]]; then
git tag -f ${{ inputs.tag_name }}-oss ${{ needs.check-version.outputs.oss }}
git push origin -f ${{ inputs.tag_name }}-oss
fi
update-release-log:
needs: check-version
uses: ./.github/workflows/release-log.yml
with:
version: ${{ vars.CURRENT_VERSION }}
secrets: inherit
update-version-info:
runs-on: ubuntu-22.04
needs: check-version
timeout-minutes: 5
strategy:
matrix:
edition: ${{ fromJson(needs.check-version.outputs.edition_matrix) }}
env:
AWS_S3_STATIC_BUCKET: ${{ vars.AWS_S3_STATIC_BUCKET }}
AWS_REGION: ${{ vars.AWS_REGION }}
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_S3_RELEASE_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_S3_RELEASE_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION }}
- uses: actions/checkout@v4
with:
sparse-checkout: release
- name: Prepare build scripts
run: cd ${{ github.workspace }}/release && yarn && yarn build
- name: Publish release notes
uses: actions/github-script@v7
id: new_version_info
with:
result-encoding: string
script: | # js
const { updateVersionInfoLatest } = require('${{ github.workspace }}/release/dist/index.cjs');
const fs = require('fs');
const edition = '${{ matrix.edition }}';
const canonical_version = edition === 'ee'
? '${{ needs.check-version.outputs.ee }}'
: '${{ needs.check-version.outputs.oss }}';
const newVersionInfo = await updateVersionInfoLatest({
newLatestVersion: canonical_version,
rollout: ${{ inputs.tag_rollout }},
});
fs.writeFileSync('version-info.json', JSON.stringify(newVersionInfo));
- name: Upload new version-info.json to S3
if: ${{ inputs.tag_name }} == "latest"
run: |
if [[ "${{ matrix.edition }}" == "ee" ]]; then
aws s3 cp version-info.json s3://${{ vars.AWS_S3_STATIC_BUCKET }}/version-info-ee.json
else
aws s3 cp version-info.json s3://${{ vars.AWS_S3_STATIC_BUCKET }}/version-info.json
fi
- name: Create cloudfront invalidation for version-info.json and version-info-ee.json
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ vars.AWS_CLOUDFRONT_STATIC_ID }} \
--paths "/version-info.json" "/version-info-ee.json"
......@@ -230,38 +230,10 @@ jobs:
console.log("The download path for this", edition, "edition is", version_path);
return version_path;
- name: Determine if this is a latest version
uses: actions/github-script@v7
id: latest_path
with:
result-encoding: string
script: | # js
const { isLatestRelease, getCanonicalVersion } = require('${{ github.workspace }}/release/dist/index.cjs');
const version = '${{ inputs.version }}';
const edition = '${{ matrix.edition }}';
const canonical_version = getCanonicalVersion(version, edition);
const isLatest = await isLatestRelease({
github,
owner: context.repo.owner,
repo: context.repo.repo,
version: canonical_version,
});
console.log("Latest version?", isLatest);
if (isLatest) {
return edition === 'ee' ? 'enterprise/latest' : 'latest';
}
return "false";
- name: Upload to S3
run: aws s3 cp ./metabase.jar s3://${{ vars.AWS_S3_DOWNLOADS_BUCKET }}/${{ steps.version_path.outputs.result }}/metabase.jar
- name: Upload to s3 latest path
if: ${{ !inputs.auto && steps.latest_path.outputs.result != 'false' }}
run: aws s3 cp ./metabase.jar s3://${{ vars.AWS_S3_DOWNLOADS_BUCKET }}/${{ steps.latest_path.outputs.result }}/metabase.jar
- name: Create cloudfront invalidation
run: |
aws cloudfront create-invalidation \
......@@ -287,7 +259,7 @@ jobs:
id: version_path
with:
result-encoding: string
script: |
script: | # js
const version = '${{ inputs.version }}';
const edition = '${{ matrix.edition }}';
......@@ -338,23 +310,6 @@ jobs:
console.log("The canonical version of this Metabase", edition, "edition is", canonical_version);
return canonical_version;
- name: Check if the container image should be tagged as latest
uses: actions/github-script@v7
id: latest_version_check
with:
result-encoding: string
script: |
const { execSync } = require("child_process");
const { isLatestVersion } = require('${{ github.workspace }}/release/dist/index.cjs');
const currentTag = '${{ inputs.version }}';
const allTags = execSync("git tag -l").toString("utf-8").split("\n");
const isLatest = isLatestVersion(currentTag, allTags);
console.log("Latest version?", isLatest);
return isLatest ? "latest" : "not-latest";
- uses: actions/download-artifact@v4
name: Retrieve previously downloaded Uberjar
with:
......@@ -405,14 +360,6 @@ jobs:
docker push ${{ env.DOCKERHUB_REPO }}:${{ steps.canonical_version.outputs.result }}
echo "Finished!"
- name: Tag the container image as latest
if: ${{ !inputs.auto && steps.latest_version_check.outputs.result == 'latest' }}
run: |
echo "Pushing ${{ env.DOCKERHUB_REPO }}:latest ..."
docker tag localhost:5000/local-metabase:${{ steps.canonical_version.outputs.result }} ${{ env.DOCKERHUB_REPO }}:latest
docker push ${{ env.DOCKERHUB_REPO }}:latest
echo "Finished!"
verify-docker-pull:
if: ${{ inputs.skip-docker != true }}
runs-on: ubuntu-22.04
......@@ -709,7 +656,7 @@ jobs:
id: new_version_info
with:
result-encoding: string
script: |
script: | # js
const { getVersionInfo } = require('${{ github.workspace }}/release/dist/index.cjs');
const fs = require('fs');
......
......@@ -6,3 +6,4 @@ dist/
version.properties
version-info.json
v*.html
channels.html
\ No newline at end of file
......@@ -17,7 +17,6 @@ import {
isValidCommitHash,
isEnterpriseVersion,
getMajorVersion,
isLatestRelease,
getVersionInfo,
publishRelease,
closeMilestone,
......@@ -250,26 +249,12 @@ async function s3() {
await checkJar();
const isLatest = isWithoutGithub ? latestFlag : await isLatestRelease({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version,
});
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,
);
if (isLatest === 'true') {
const latestPath = edition === "ee" ? `enterprise/latest` : `latest`;
await $`aws s3 cp ${JAR_PATH}/metabase.jar s3://${AWS_S3_DOWNLOADS_BUCKET}/${latestPath}/metabase.jar`.pipe(
process.stdout,
);
}
await $`aws cloudfront create-invalidation \
--distribution-id ${AWS_CLOUDFRONT_DOWNLOADS_ID} \
--paths /${versionPath}/metabase.jar`.pipe(process.stdout);
......@@ -300,21 +285,6 @@ async function docker() {
await $`docker push ${dockerTag}`.pipe(process.stdout);
log(`✅ Published ${dockerTag} to DockerHub`);
const isLatest = isWithoutGithub ? latestFlag :await isLatestRelease({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version,
});
if (isLatest === 'true') {
const latestTag = `${DOCKERHUB_OWNER}/${dockerRepo}:latest`;
await $`docker tag ${dockerTag} ${latestTag}`.pipe(process.stdout);
await $`docker push ${latestTag}`.pipe(process.stdout);
log(`✅ Published ${latestTag} to DockerHub`);
}
}
async function versionInfo() {
......
import "dotenv/config";
import { Octokit } from "@octokit/rest";
import { isLatestRelease } from "./github";
const { GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO } = process.env as any;
const github = new Octokit({ auth: GITHUB_TOKEN });
describe("github release helpers", () => {
beforeAll(() => {
expect(GITHUB_TOKEN).toBeDefined();
expect(GITHUB_OWNER).toBeDefined();
expect(GITHUB_REPO).toBeDefined();
});
describe("isLatestRelease", () => {
// Note: if we've gotten to metabase v99 🥳 you'll need to update this test
it("should always tell you v0.99 is the latest release", async () => {
const isLatest = await isLatestRelease({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version: "v0.99.0",
});
expect(isLatest).toBe(true);
});
it("should always tell you v1.99 is the latest release", async () => {
const isLatest = await isLatestRelease({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version: "v1.99.0",
});
expect(isLatest).toBe(true);
});
it("should never tell you that v0.1 is the latest release", async () => {
const isLatest = await isLatestRelease({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version: "v0.1.0",
});
expect(isLatest).toBe(false);
});
it("should never tell you that v1.1 is the latest release", async () => {
const isLatest = await isLatestRelease({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version: "v1.1.0",
});
expect(isLatest).toBe(false);
});
it("should never tell you that an RC is a latest release", async () => {
const isLatestEE = await isLatestRelease({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version: "v1.99.0-RC9",
});
expect(isLatestEE).toBe(false);
const isLatestOSS = await isLatestRelease({
github,
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
version: "v0.99.0-RC9",
});
expect(isLatestOSS).toBe(false);
});
});
});
......@@ -3,8 +3,6 @@ import {
getLastReleaseTag,
getMilestoneName,
getNextVersions,
isLatestVersion,
isValidVersionString,
} from "./version-helpers";
export const getMilestones = async ({
......@@ -105,28 +103,6 @@ export const getMilestoneIssues = async ({
return (issues ?? []) as Issue[];
};
export const isLatestRelease = async ({
version,
github,
owner,
repo,
}: ReleaseProps): Promise<boolean> => {
if (!isValidVersionString(version)) {
console.warn(`Invalid version string: ${version}`);
return false;
}
const releases = await github.rest.repos.listReleases({
owner,
repo,
});
const releaseNames = releases.data.map(
(r: { tag_name: string }) => r.tag_name,
);
return isLatestVersion(version, releaseNames);
};
export const hasBeenReleased = async ({
github,
owner,
......
import fs from 'fs';
import { $ } from 'zx';
const releaseChannels = [
"nightly",
"beta",
"latest",
] as const;
const editions = ["oss", "ee"] as const;
type CommitInfo = {
version: string,
message: string,
hash: string,
date: string,
}
type ReleaseChannel = typeof releaseChannels[number];
type Edition = typeof editions[number];
type ChannelInfo = Record<Edition, CommitInfo & { edition: Edition, channel: ReleaseChannel }>;
type TagInfo = Record<ReleaseChannel, ChannelInfo>;
const tablePageTemplate = fs.readFileSync('./src/releaseChannelPageTemplate.html', 'utf8');
const format = "--pretty='format:%(decorate:prefix=,suffix=)||%s||%H||%ah'";
export async function gitLog(channel: ReleaseChannel, edition: Edition): Promise<CommitInfo> {
const { stdout } = await $`git log -1 ${format} refs/tags/${channel}-${edition}`.catch(() => ({ stdout: '' }));
const commitInfo = processCommit(stdout.trim(), edition);
return commitInfo;
}
function processCommit(commitLine: string, edition: Edition): CommitInfo {
const [refs, message, hash, date] = commitLine.split('||');
const version = edition === "ee"
? refs?.match(/(v1\.[\d\.\-RCrc]+)/)?.[1] ?? ''
: refs?.match(/(v0\.[\d\.\-RCrc]+)/)?.[1] ?? '';
return { version, message, hash, date};
}
const commitLink = (hash: string) => `https://github.com/metabase/metabase/commit/${hash}`;
function linkifyCommit(commit: CommitInfo) {
return `<a href="${commitLink(commit.hash)}" target="_blank">${commit.version}</a>`;
}
function tableRow(channelInfo: ChannelInfo) {
return `<tr>
<td><strong>${channelInfo.ee.channel}</strong></td>
<td>${linkifyCommit(channelInfo.oss)}</td>
<td>${linkifyCommit(channelInfo.ee)}</td>
</tr>`;
}
function buildTable(tagInfo: TagInfo) {
const rows = Object.values(tagInfo).map(tableRow).join('\n');
const currentTime = new Date().toLocaleString();
const tableHtml = `
<table>
<thead>
<tr>
<th>Channel</th>
<th>OSS</th>
<th>EE</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>`;
return tablePageTemplate
.replace(/{{release-table}}/, tableHtml)
.replace(/{{current-time}}/, currentTime);
}
export async function releaseChannelLog() {
// @ts-expect-error - TS is too stupid to infer from Object.fromEntries
const tagInfo: TagInfo = Object.fromEntries(releaseChannels.map((
channel: ReleaseChannel) => [channel, { ee: {}, oss: {} }]
));
for (const edition of editions) {
for (const channel of releaseChannels) {
const commitInfo = await gitLog(channel, edition);
tagInfo[channel][edition] = {
channel,
edition,
...commitInfo,
};
}
}
return buildTable(tagInfo);
}
console.log(await releaseChannelLog());
......@@ -5,7 +5,7 @@ import { $ } from 'zx';
import { issueNumberRegex } from './linked-issues';
type CommitInfo = {
version: string,
versions: string[],
message: string,
hash: string,
date: string,
......@@ -19,11 +19,15 @@ export async function gitLog(majorVersion: number) {
return buildTable(processedCommits, majorVersion);
}
function processCommit(commitLine: string): CommitInfo {
export function processCommit(commitLine: string): CommitInfo {
const [refs, message, hash, date] = commitLine.split('||');
const version = refs?.match(/(v[\d\.\-RCrc]+)/)?.[1] ?? '';
const tags = refs?.match(/tag: ([\w\d-_\.]+)/g) ?? '';
return { version, message, hash, date};
const versions = tags
? tags.map((v) => v.replace('tag: ', ''))
: [''];
return { versions, message, hash, date};
}
const issueLink = (issueNumber: string) => `https://github.com/metabase/metabase/issues/${issueNumber}`;
......@@ -36,7 +40,7 @@ function linkifyIssueNumbers(message: string) {
function tableRow(commit: CommitInfo) {
return `<tr>
<td><strong>${commit.version}</strong></td>
<td><strong>${commit.versions.join('<br>')}</strong></td>
<td>${linkifyIssueNumbers(commit.message)}</td>
<td>${commit.date}</td>
</tr>`;
......@@ -64,11 +68,15 @@ function buildTable(commits: CommitInfo[], majorVersion: number) {
.replace(/{{current-time}}/, currentTime);
}
const version = Number(process.argv[2]);
if (!version) {
console.error('Please provide a version number (e.g. 35, 57)');
process.exit(1);
export async function generateReleaseLog() {
const version = Number(process.argv[2]);
if (!version) {
console.error('Please provide a version number (e.g. 35, 57)');
process.exit(1);
}
console.log(await gitLog(version));
}
console.log(await gitLog(version));
import { processCommit } from './release-log';
describe('Release Log', () => {
describe('processCommit', () => {
it('should return an array of commit info objects with versions', () => {
const commitLine = 'tag: v1.50.0-RC2, tag: v0.50.0-RC2||Show the columns for the correct stage when using combine/extract in the presence of an aggregation (#43226) (#43450)||7a8b73e298e0d658e2fcd6b1fbcac3e0d0770288||Mon Jun 3 03:31';
const result = processCommit(commitLine);
expect(result).toEqual({
versions: ['v1.50.0-RC2', 'v0.50.0-RC2'],
message: 'Show the columns for the correct stage when using combine/extract in the presence of an aggregation (#43226) (#43450)',
hash: '7a8b73e298e0d658e2fcd6b1fbcac3e0d0770288',
date: 'Mon Jun 3 03:31',
});
});
it('should return an empty string in an array when there are no tags', () => {
const commitLine = '||fix stacked data labels on ordinal charts (#43469) (#43508)||37c901325cdf9cdb96091e4c159c849fd65df9f5||Mon Jun 3 11:03';
const result = processCommit(commitLine);
expect(result).toEqual({
versions: [''],
message: 'fix stacked data labels on ordinal charts (#43469) (#43508)',
hash: '37c901325cdf9cdb96091e4c159c849fd65df9f5',
date: 'Mon Jun 3 11:03',
});
});
it('should gracefully handle malformed logs', () => {
const commitLine = ' foo bar baz | la dee da pikachu;\r\n$20 ';
const result = processCommit(commitLine);
expect(result).toEqual({
versions: [''],
message: undefined,
hash: undefined,
date: undefined,
});
});
it('should gracefully handle empty logs', () => {
const commitLine = '';
const result = processCommit(commitLine);
expect(result).toEqual({
versions: [''],
message: undefined,
hash: undefined,
date: undefined,
});
});
});
});
import { match } from "ts-pattern";
import { nonUserFacingLabels, hiddenLabels } from "./constants";
import { getMilestoneIssues, isLatestRelease, hasBeenReleased } from "./github";
import { getMilestoneIssues, hasBeenReleased } from "./github";
import type { Issue, ReleaseProps } from "./types";
import {
isEnterpriseVersion,
......@@ -260,12 +260,6 @@ export async function publishRelease({
const issues = await getMilestoneIssues({ version, github, owner, repo });
const isLatest: "true" | "false" =
!isEnterpriseVersion(version) &&
(await isLatestRelease({ version, github, owner, repo }))
? "true"
: "false";
const payload = {
owner,
repo,
......@@ -274,7 +268,6 @@ export async function publishRelease({
body: generateReleaseNotes({ version, checksum, issues }),
draft: true,
prerelease: isRCVersion(version),
make_latest: isLatest,
};
return github.rest.repos.createRelease(payload);
......
<html>
<head>
<title>Metabase Release Channels </title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: Lato, sans-serif;
background-color: #282c34;
color: #ddd;
padding: 40px;
}
table {
border-collapse: collapse;
}
th, td {
padding: 8px;
text-align: left;
}
tr:nth-child(even) {
background-color: #2e323a;
}
tbody > tr:hover{
color: #8fbdf6;
}
a {
text-decoration: underline;
color:#2987f9;
}
</style>
</head>
<body class="p-5 text-gray-200 bg-gray-800">
<h1>Metabase Release Channels</h1>
<p>as of {{current-time}}</p>
<div>
{{release-table}}
</div>
</body>
</html>
......@@ -30,6 +30,7 @@ export interface VersionInfo {
released: string;
patch: boolean;
highlights: string[];
rollout?: number;
}
export interface VersionInfoFile {
......
......@@ -97,31 +97,6 @@ export const getVersionFromReleaseBranch = (branch: string) => {
return `v0.${majorVersion}.0`;
}
export const isLatestVersion = (thisVersion: string, allVersions: string[]) => {
if (isRCVersion(thisVersion)) {
return false;
}
const normalizedVersions = allVersions
.filter(isValidVersionString)
.filter(version => !isRCVersion(version))
.map(version => String(coerce(version.replace(/(v1|v0)\./, ""))))
.sort(compareVersions);
if (!normalizedVersions.length) {
return true;
}
const lastVersion = normalizedVersions[normalizedVersions.length - 1];
return (
compareVersions(
String(coerce(thisVersion.replace(/(v1|v0)\./, ""))),
lastVersion,
) > -1
);
};
export const versionRequirements: Record<
number,
{ java: number; node: number }
......
......@@ -12,7 +12,6 @@ import {
getVersionFromReleaseBranch,
getVersionType,
isEnterpriseVersion,
isLatestVersion,
isRCVersion,
isValidVersionString,
versionSort,
......@@ -221,102 +220,6 @@ describe("version-helpers", () => {
});
});
describe("isLatestVersion", () => {
it(`should return true for latest releases`, () => {
const cases: [string, string[]][] = [
["v0.25.2.1", ["v0.24.0", "v0.25.1", "v0.25.2", "v0.9.0"]],
["v0.25.3", ["v0.24.0", "v0.25.1", "v0.25.2"]],
["v0.26.0", ["v0.24.0", "v0.25.1", "v0.25.2"]],
["v0.26.0", ["v0.24.0", "v0.25.1", "v0.25.2"]],
];
cases.forEach(([input, releases]) => {
expect(isLatestVersion(input, releases)).toEqual(true);
expect(isLatestVersion(input, releases.reverse())).toEqual(true);
});
});
it(`should return false for non-latest releases`, () => {
const cases: [string, string[]][] = [
["v0.21.2.1", ["v0.24.0", "v0.25.1", "v0.25.2", "v0.9.9.9"]],
["v0.25.1.2", ["v0.24.0", "v0.25.1", "v0.25.2", "v0.9.9.9"]],
["v0.25.0", ["v0.24.0", "v0.25.1", "v0.25.2", "v0.9.0"]],
["v0.25.1.99", ["v0.24.0", "v0.25.1", "v0.25.2", "v0.9.0"]],
["v0.71", ["v0.24", "v0.25.1", "v0.25.2", "v0.80.0"]],
];
cases.forEach(([input, releases]) => {
expect(isLatestVersion(input, releases)).toEqual(false);
expect(isLatestVersion(input, releases.reverse())).toEqual(false);
});
});
it("should ignore EE vs OSS version", () => {
const falseCases: [string, string[]][] = [
["v0.21.2.1", ["v1.24", "v1.25.1", "v0.25.2"]],
["v1.25.1.2", ["v0.24", "v1.25.1", "v0.25.2"]],
["v0.25", ["v1.24", "v0.25.1", "v1.25.2"]],
["v1.25.1.99", ["v0.24", "v0.25.1", "v1.25.2"]],
];
falseCases.forEach(([input, releases]) => {
expect(isLatestVersion(input, releases)).toEqual(false);
expect(isLatestVersion(input, releases.reverse())).toEqual(false);
});
const trueCases: [string, string[]][] = [
["v0.25.2.1", ["v0.24", "v0.25.1", "v0.25.2"]],
["v0.25.3", ["v0.24", "v0.25.1", "v0.25.2"]],
["v0.26", ["v0.24", "v0.25.1", "v0.25.2"]],
["v0.26.0", ["v0.24", "v0.25.1", "v0.25.2"]],
];
trueCases.forEach(([input, releases]) => {
expect(isLatestVersion(input, releases)).toEqual(true);
expect(isLatestVersion(input, releases.reverse())).toEqual(true);
});
});
it("should return true for an equal release", () => {
expect(isLatestVersion("v0.25.2", ["v0.25.2", "v0.25.1"])).toEqual(true);
});
it("should return true for an equal release of ee/oss", () => {
// this is important because if we release 0.25.2 and 1.25.2 at the same time,
// they should both be "latest" - and one of them will always be tagged first
expect(isLatestVersion("v0.25.2", ["v1.25.2", "v0.25.1"])).toEqual(true);
expect(isLatestVersion("v1.25.2", ["v0.25.2", "v0.25.1"])).toEqual(true);
});
it("should filter out invalid versions", () => {
const trueCases: [string, string[]][] = [
["v0.25.2.1", ["v0.24", "v0.25.1", "99"]],
["v0.25.3", ["v0.24", "v0.25.1", "xyz"]],
["v0.26", ["v0.24", "v0.25.1", "v99.99.99"]],
["v0.26.0", ["-1", "000", ""]],
];
trueCases.forEach(([input, releases]) => {
expect(isLatestVersion(input, releases)).toEqual(true);
expect(isLatestVersion(input, releases.reverse())).toEqual(true);
});
});
it("should never mark an RC as latest", () => {
const cases: [string, string[]][] = [
["v0.25.2.1-rc1", ["v0.24.0", "v0.25.1", "v0.25.2-rc1"]],
["v0.25.3-rc2", ["v0.24.0", "v0.25.1", "v0.25.2-rc2"]],
["v0.26.0-RC2", ["v0.24.0", "v0.25.1", "v0.25.2-rc3"]],
["v0.24.5-rc1", ["v0.24.0", "v0.25.1", "v0.25.2-rc4"]],
["v0.26.0-rc99", ["v0.24.0", "v0.25.1", "v0.25.2-rc4"]],
["v0.99.0-rc99", ["v0.24.0", "v0.25.1", "v0.99-rc4", "v0.9.9.9"]],
];
cases.forEach(([input, releases]) => {
expect(isLatestVersion(input, releases)).toEqual(false);
expect(isLatestVersion(input, releases.reverse())).toEqual(false);
});
});
});
describe("getBuildRequirements", () => {
it("should return the correct build requirements for provided ee version", () => {
expect(getBuildRequirements("v1.47.2.1")).toEqual({
......
import fetch from "node-fetch";
import { getMilestoneIssues } from "./github";
import {
getVersionType,
isEnterpriseVersion,
isLatestVersion,
} from "./version-helpers";
import _ from "underscore";
import { getMilestoneIssues } from "./github";
import type {
Issue,
ReleaseProps,
VersionInfoFile,
VersionInfo,
VersionInfoFile,
} from "./types";
import {
getVersionType,
isEnterpriseVersion,
} from "./version-helpers";
const generateVersionInfo = ({
version,
......@@ -37,15 +37,6 @@ export const generateVersionInfoJson = ({
milestoneIssues: Issue[];
existingVersionInfo: VersionInfoFile;
}) => {
const isLatest = isLatestVersion(version, [
existingVersionInfo.latest.version,
]);
if (!isLatest) {
console.warn(`Version ${version} is not the latest`);
return existingVersionInfo;
}
const isAlreadyReleased =
existingVersionInfo?.latest?.version === version ||
existingVersionInfo?.older?.some(
......@@ -56,13 +47,56 @@ export const generateVersionInfoJson = ({
console.warn(`Version ${version} already released`);
return existingVersionInfo;
}
const newVersionInfo = generateVersionInfo({ version, milestoneIssues });
return {
latest: isLatest ? newVersionInfo : existingVersionInfo.latest,
older: isLatest
? [existingVersionInfo.latest, ...existingVersionInfo.older]
: [newVersionInfo, ...existingVersionInfo.older],
latest: existingVersionInfo.latest,
older: [newVersionInfo, ...existingVersionInfo.older],
};
};
export const updateVersionInfoLatestJson = ({
newLatestVersion,
existingVersionInfo,
rollout,
}: {
newLatestVersion: string;
existingVersionInfo: VersionInfoFile;
rollout?: number;
}) => {
if (existingVersionInfo.latest.version === newLatestVersion) {
console.warn(`Version ${newLatestVersion} already latest, updating rollout % only`);
return {
...existingVersionInfo,
latest: {
...existingVersionInfo.latest,
rollout,
},
};
}
const newLatestVersionInfo = existingVersionInfo.older
.find((info: VersionInfo) => info.version === newLatestVersion);
if (!newLatestVersionInfo) {
throw new Error(`${newLatestVersion} not found version-info.json`);
}
// remove the new latest version from the older versions
const oldLatestVersionInfo = existingVersionInfo.latest;
const newOldVersionInfo = existingVersionInfo.older.filter(
(info: VersionInfo) => info.version !== newLatestVersion,
);
oldLatestVersionInfo.rollout = undefined;
return {
latest: {
...newLatestVersionInfo,
rollout,
},
older: [oldLatestVersionInfo, ...newOldVersionInfo],
};
};
......@@ -72,6 +106,7 @@ export const getVersionInfoUrl = (version: string) => {
: `http://${process.env.AWS_S3_STATIC_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/version-info.json`;
};
// for adding a new release to version info
export async function getVersionInfo({
version,
github,
......@@ -98,3 +133,25 @@ export async function getVersionInfo({
return newVersionJson;
}
// for updating the latest version in version info
export const updateVersionInfoLatest = async ({
newLatestVersion,
rollout = 100,
}: {
newLatestVersion: string;
rollout?: number;
}) => {
const url = getVersionInfoUrl(newLatestVersion);
const existingFile = (await fetch(url).then(r =>
r.json(),
)) as VersionInfoFile;
const newVersionJson = updateVersionInfoLatestJson({
newLatestVersion,
existingVersionInfo: existingFile,
rollout,
});
return newVersionJson;
};
\ No newline at end of file
import { generateVersionInfoJson, getVersionInfoUrl } from "./version-info";
import type { Issue, VersionInfoFile } from "./types";
import { generateVersionInfoJson, getVersionInfoUrl, updateVersionInfoLatestJson } from "./version-info";
describe("version-info", () => {
describe("generateVersionInfoJson", () => {
......@@ -39,29 +39,29 @@ describe("version-info", () => {
older: [],
} as VersionInfoFile;
it("should add new latest version to version info json", () => {
it("should add new version to version info json", () => {
const generatedJson = generateVersionInfoJson({
milestoneIssues: issues,
version: "v0.3.0",
existingVersionInfo: oldJson,
});
expect(generatedJson.latest).toEqual({
expect(generatedJson.older).toEqual([{
version: "v0.3.0",
released: expect.any(String),
patch: false,
highlights: ["New Issue 1", "New Issue 2"],
});
}]);
});
it("should move old latest version to older array", () => {
it("should leave old latest version intact", () => {
const generatedJson = generateVersionInfoJson({
milestoneIssues: issues,
version: "v0.3.0",
existingVersionInfo: oldJson,
});
expect(generatedJson.older).toEqual([oldJson.latest]);
expect(generatedJson.latest).toEqual(oldJson.latest);
});
it("properly records patch releases", () => {
......@@ -71,7 +71,7 @@ describe("version-info", () => {
existingVersionInfo: oldJson,
});
expect(generatedJson.latest.patch).toEqual(true);
expect(generatedJson.older[0].patch).toEqual(true);
});
it("properly recognizes major releases", () => {
......@@ -81,17 +81,22 @@ describe("version-info", () => {
existingVersionInfo: oldJson,
});
expect(generatedJson.latest.patch).toEqual(false);
expect(generatedJson.older[0].patch).toEqual(false);
});
it("should ignore a non-latest release", () => {
it("should always record releases in older array", () => {
const generatedJson = generateVersionInfoJson({
milestoneIssues: issues,
version: "v0.1.9",
existingVersionInfo: oldJson,
});
expect(generatedJson).toEqual(oldJson);
expect(generatedJson.older[0]).toEqual({
version: "v0.1.9",
released: expect.any(String),
patch: true,
highlights: ["New Issue 1", "New Issue 2"],
});
});
it("should ignore an already released version", () => {
......@@ -105,6 +110,161 @@ describe("version-info", () => {
});
});
describe("updateVersionInfoLatestJson", () => {
const oldJson = {
latest: {
version: "v0.2.4",
released: "2022-01-01",
patch: true,
highlights: ["Old Issue 1", "Old Issue 2"],
},
older: [
{
version: "v0.2.5",
released: "2023-01-01",
patch: true,
highlights: ["New Issue 31", "New Issue 41"],
},
{
version: "v0.2.3",
released: "2021-01-01",
patch: true,
highlights: ["Old Issue 3", "Old Issue 4"],
},
{
version: "v0.2.2",
released: "2020-01-01",
patch: true,
highlights: ["Old Issue 5", "Old Issue 6"],
},
],
} as VersionInfoFile;
it("should update latest version", () => {
const updatedJson = updateVersionInfoLatestJson({
newLatestVersion: "v0.2.5",
existingVersionInfo: oldJson,
});
expect(updatedJson.latest.version).toEqual("v0.2.5");
});
it("should ignore if version is already latest", () => {
const updatedJson = updateVersionInfoLatestJson({
newLatestVersion: "v0.2.4",
existingVersionInfo: oldJson,
});
expect(updatedJson).toEqual(oldJson);
});
it("should throw if new version is not in older versions", () => {
expect(() => updateVersionInfoLatestJson({
newLatestVersion: "v0.2.1",
existingVersionInfo: oldJson,
})).toThrow();
});
it("should remove the new latest version from the older versions", () => {
const updatedJson = updateVersionInfoLatestJson({
newLatestVersion: "v0.2.5",
existingVersionInfo: oldJson,
});
expect(updatedJson.older).not.toContainEqual({
version: "v0.2.5",
released: "2023-01-01",
patch: true,
highlights: ["New Issue 31", "New Issue 41"],
});
});
it("should keep the old latest version in the older versions", () => {
const updatedJson = updateVersionInfoLatestJson({
newLatestVersion: "v0.2.5",
existingVersionInfo: oldJson,
});
expect(updatedJson.older).toContainEqual({
version: "v0.2.4",
released: "2022-01-01",
patch: true,
highlights: ["Old Issue 1", "Old Issue 2"],
});
});
it("should update rollout % on new latest version", () => {
const updatedJson = updateVersionInfoLatestJson({
newLatestVersion: "v0.2.5",
existingVersionInfo: oldJson,
rollout: 51,
});
expect(updatedJson.latest.rollout).toEqual(51);
});
it("should update rollout % on old latest version", () => {
const updatedJson = updateVersionInfoLatestJson({
newLatestVersion: "v0.2.4",
existingVersionInfo: oldJson,
rollout: 51,
});
expect(updatedJson.latest).toEqual({
version: "v0.2.4",
released: "2022-01-01",
patch: true,
highlights: ["Old Issue 1", "Old Issue 2"],
rollout: 51,
});
const updatedJson2 = updateVersionInfoLatestJson({
newLatestVersion: "v0.2.4",
existingVersionInfo: updatedJson,
rollout: 59,
});
expect(updatedJson2.latest).toEqual({
version: "v0.2.4",
released: "2022-01-01",
patch: true,
highlights: ["Old Issue 1", "Old Issue 2"],
rollout: 59,
});
});
it("should remove rollout % on old latest version", () => {
const updatedJson = updateVersionInfoLatestJson({
newLatestVersion: "v0.2.4",
existingVersionInfo: oldJson,
rollout: 51,
});
const updatedJson2 = updateVersionInfoLatestJson({
newLatestVersion: "v0.2.5",
existingVersionInfo: updatedJson,
rollout: 100,
});
expect(updatedJson2.latest).toEqual({
version: "v0.2.5",
released: "2023-01-01",
patch: true,
highlights: ["New Issue 31", "New Issue 41"],
rollout: 100,
});
expect(updatedJson2.older[0]).toEqual({
version: "v0.2.4",
released: "2022-01-01",
patch: true,
highlights: ["Old Issue 1", "Old Issue 2"],
// no rollout
});
});
});
describe("getVersionInfoUrl", () => {
beforeEach(() => {
jest.resetModules();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment