Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 45 additions & 49 deletions packages/build/src/local/get-latest-tag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,61 +10,57 @@ describe('local get-latest-tag', () => {
});

describe('getLatestDraftOrReleaseTagFromLog', () => {
it('extracts the latest draft tag', () => {
spawnSync.onFirstCall().returns({
stdout: [
'v0.7.9',
'v0.8.0-draft.0',
'v0.8.0-draft.1',
'v0.8.0-draft.10',
'v0.8.0-draft.2'
].join('\n')
});
spawnSync.onSecondCall().returns({
stdout: 'tagHash'
});

const result = getLatestDraftOrReleaseTagFromLog(
'somePath',
spawnSync
);
expect(result).to.deep.equal({
commit: 'tagHash',
tag: {
semverName: '0.8.0-draft.10',
[
{
restriction: undefined,
expected: {
semverName: '0.8.1',
releaseVersion: '0.8.1',
draftVersion: undefined
}
},
{
restriction: { major: 0, minor: 8, patch: 0 },
expected: {
semverName: '0.8.0',
releaseVersion: '0.8.0',
draftVersion: 10
draftVersion: undefined
}
});
});

it('extracts the latest release tag', () => {
spawnSync.onFirstCall().returns({
stdout: [
'v0.8.0',
'v0.8.0-draft.0',
'v0.8.0-draft.1',
'v0.8.0-draft.10',
'v0.8.1',
'v0.8.0-draft.2',
'v0.8.1-draft.0',
].join('\n')
});
spawnSync.onSecondCall().returns({
stdout: 'tagHash'
});

const result = getLatestDraftOrReleaseTagFromLog(
'somePath',
spawnSync
);
expect(result).to.deep.equal({
commit: 'tagHash',
tag: {
},
{
restriction: { major: 0, minor: 8, patch: undefined },
expected: {
semverName: '0.8.1',
releaseVersion: '0.8.1',
draftVersion: undefined
}
}
].forEach(({ restriction, expected }) => {
it(`extracts the latest tag when restricted to ${JSON.stringify(restriction)}`, () => {
spawnSync.onFirstCall().returns({
stdout: [
'v0.8.0',
'v0.8.0-draft.0',
'v0.8.0-draft.1',
'v0.8.0-draft.10',
'v0.8.1',
'v0.8.0-draft.2',
'v0.8.1-draft.0',
].join('\n')
});
spawnSync.onSecondCall().returns({
stdout: 'tagHash'
});

const result = getLatestDraftOrReleaseTagFromLog(
'somePath',
restriction,
spawnSync
);
expect(result).to.deep.equal({
commit: 'tagHash',
tag: expected
});
});
});
});
Expand Down
31 changes: 28 additions & 3 deletions packages/build/src/local/get-latest-tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,23 @@ export interface TagDetails {
draftVersion: number | undefined;
}

export interface ReleaseVersion {
major: number | undefined;
minor: number | undefined;
patch: number | undefined;
}

export function getLatestDraftOrReleaseTagFromLog(
repositoryRoot: string,
versionRestriction: ReleaseVersion | undefined,
spawnSync: typeof spawnSyncFn = spawnSyncFn
): TaggedCommit | undefined {
const gitTags = spawnSync('git', ['tag'], {
cwd: repositoryRoot,
encoding: 'utf-8'
});

const tagDetails = extractTags(gitTags.stdout.split('\n'));
const tagDetails = extractTags(gitTags.stdout.split('\n'), versionRestriction);
const sortedTagsWithCommit = tagDetails.sort((t1, t2) => {
return -1 * semver.compare(t1.semverName, t2.semverName);
});
Expand All @@ -42,7 +49,7 @@ export function getLatestDraftOrReleaseTagFromLog(
};
}

function extractTags(gitTags: string[]): TagDetails[] {
function extractTags(gitTags: string[], versionRestriction: ReleaseVersion | undefined): TagDetails[] {
const validTags = gitTags
.map(tag => semver.valid(tag))
.filter(v => !!v) as string[];
Expand All @@ -53,9 +60,27 @@ function extractTags(gitTags: string[]): TagDetails[] {
return undefined;
}

const major = semver.major(semverTag);
const minor = semver.minor(semverTag);
const patch = semver.patch(semverTag);

if (versionRestriction?.major !== undefined) {
if (major !== versionRestriction.major) {
return undefined;
}
if (versionRestriction.minor !== undefined) {
if (minor !== versionRestriction.minor) {
return undefined;
}
if (versionRestriction.patch !== undefined && versionRestriction.patch !== patch) {
return undefined;
}
}
}

return {
semverName: semverTag,
releaseVersion: `${semver.major(semverTag)}.${semver.minor(semverTag)}.${semver.patch(semverTag)}`,
releaseVersion: `${major}.${minor}.${patch}`,
draftVersion: prerelease ? parseInt(prerelease[1], 10) : undefined
};
}).filter(t => !!t) as TagDetails[];
Expand Down
73 changes: 53 additions & 20 deletions packages/build/src/local/repository-status.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from 'chai';
import sinon from 'sinon';
import { getRepositoryStatus, RepositoryStatus, verifyGitStatus } from './repository-status';
import { getReleaseVersionFromBranch, getRepositoryStatus, RepositoryStatus, verifyGitStatus } from './repository-status';

describe('local repository-status', () => {
let spawnSync: sinon.SinonStub;
Expand All @@ -16,7 +16,7 @@ describe('local repository-status', () => {
getRepositoryStatus = sinon.stub();
});

[ 'master', 'main', 'v0.8.0', 'v0.8.x' ].forEach(branchName => {
[ 'master', 'main', 'release/v0.8.0', 'release/v0.8.x' ].forEach(branchName => {
it(`accepts a clean repository on ${branchName}`, () => {
const status: RepositoryStatus = {
branch: {
Expand All @@ -28,8 +28,8 @@ describe('local repository-status', () => {
hasUnpushedTags: false
};
getRepositoryStatus.returns(status);
verifyGitStatus('root', getRepositoryStatus);
expect(getRepositoryStatus).to.have.been.calledOnce;
const returnedStatus = verifyGitStatus('root', getRepositoryStatus);
expect(returnedStatus).to.equal(status);
});
});

Expand Down Expand Up @@ -150,23 +150,31 @@ describe('local repository-status', () => {
});

describe('getRepositoryStatus', () => {
it('parses a clean repository correctly', () => {
spawnSync.returns({
stdout: '## master...origin/master\n'
});
spawnSync.onSecondCall().returns({
stdout: 'Everything up-to-date'
});
[
'master',
'main',
'release/v0.7.x',
'release/another-branch',
'release/v0.7.9'
].forEach(branch => {
it('parses a clean repository correctly', () => {
spawnSync.returns({
stdout: `## ${branch}...origin/${branch}\n`
});
spawnSync.onSecondCall().returns({
stdout: 'Everything up-to-date'
});

const status = getRepositoryStatus('somePath', spawnSync);
expect(status).to.deep.equal({
branch: {
local: 'master',
tracking: 'origin/master',
diverged: false
},
clean: true,
hasUnpushedTags: false
const status = getRepositoryStatus('somePath', spawnSync);
expect(status).to.deep.equal({
branch: {
local: branch,
tracking: `origin/${branch}`,
diverged: false
},
clean: true,
hasUnpushedTags: false
});
});
});

Expand Down Expand Up @@ -267,4 +275,29 @@ describe('local repository-status', () => {
});
});
});

describe('getReleaseVersionFromDraft', () => {
it('parses the release branch properly', () => {
const version = getReleaseVersionFromBranch('release/v0.8.3');
expect(version).to.deep.equal({
major: 0,
minor: 8,
patch: 3
});
});

it('handles a release branch that is not fully numbered', () => {
const version = getReleaseVersionFromBranch('release/v0.8.x');
expect(version).to.deep.equal({
major: 0,
minor: 8,
patch: undefined
});
});

it('returns undefined for non-release branches', () => {
const version = getReleaseVersionFromBranch('master');
expect(version).to.be.undefined;
});
});
});
37 changes: 30 additions & 7 deletions packages/build/src/local/repository-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@ export interface RepositoryStatus {
hasUnpushedTags: boolean
}

const MAIN_OR_MASTER_BRANCH = /^(main|master)$/;
const RELEASE_BRANCH = /^release\/v([a-z0-9]+\.[a-z0-9]+\.[a-z0-9]+)$/;

export function verifyGitStatus(
repositoryRoot: string,
getRepositoryStatusFn: typeof getRepositoryStatus = getRepositoryStatus
): void {
): RepositoryStatus {
const repositoryStatus = getRepositoryStatusFn(repositoryRoot);
if (!repositoryStatus.branch?.local) {
throw new Error('Could not determine local repository information - please verify your repository is intact.');
}
if (!/^(master|main|v[a-z0-9]+\.[a-z0-9]+\.[a-z0-9]+)$/.test(repositoryStatus.branch.local)) {
throw new Error('The current branch does not match: master|main|vX.X.X');
if (!MAIN_OR_MASTER_BRANCH.test(repositoryStatus.branch.local) && !RELEASE_BRANCH.test(repositoryStatus.branch.local)) {
throw new Error('The current branch does not match: master|main|release/vX.X.X');
}
if (!repositoryStatus.branch.tracking) {
throw new Error('The branch you are on is not tracking any remote branch.');
Expand All @@ -31,6 +34,7 @@ export function verifyGitStatus(
if (repositoryStatus.hasUnpushedTags) {
throw new Error('You have local tags that are not pushed to the remote. Remove or push those tags to continue.');
}
return repositoryStatus;
}

export function getRepositoryStatus(
Expand All @@ -48,7 +52,7 @@ export function getRepositoryStatus(

const result: RepositoryStatus = {
clean: true,
hasUnpushedTags: tagStatus.stdout.trim() !== 'Everything up-to-date'
hasUnpushedTags: (tagStatus.stdout?.trim() ?? '' + tagStatus.stderr?.trim() ?? '') !== 'Everything up-to-date'
};

const output = gitStatus.stdout
Expand All @@ -57,13 +61,13 @@ export function getRepositoryStatus(
.filter(l => !!l);

const branchOutput = output.find(l => l.match(/^## /));
const branchInfo = branchOutput?.match(/^## ([^\s.]+)(\.\.\.([^\s]+)( \[[^\]]+])?)?/);
const branchInfo = branchOutput?.match(/^## ([^\s]+?((?=\.\.\.)|$))(\.\.\.([^\s]+)( \[[^\]]+])?)?/);

if (branchInfo) {
result.branch = {
local: branchInfo[1],
tracking: branchInfo[3],
diverged: !!branchInfo[4]
tracking: branchInfo[4],
diverged: !!branchInfo[5]
};
}

Expand All @@ -73,3 +77,22 @@ export function getRepositoryStatus(
return result;
}

export function getReleaseVersionFromBranch(branchName: string | undefined): { major: number | undefined, minor: number | undefined, patch: number | undefined} | undefined {
const match = branchName?.match(RELEASE_BRANCH);
if (!match) {
return undefined;
}

const versionParts = match[1].split('.');
const numOrUndefiend = (num: string): number | undefined => {
const value = parseInt(num, 10);
return isNaN(value) ? undefined : value;
};

return {
major: numOrUndefiend(versionParts[0]),
minor: numOrUndefiend(versionParts[1]),
patch: numOrUndefiend(versionParts[2]),
};
}

Loading