Skip to content

Commit

Permalink
feat(version): custom tag-version-separator for independent projects (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesHenry committed Feb 5, 2024
1 parent ea3fb65 commit 43de79c
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 8 deletions.
92 changes: 92 additions & 0 deletions e2e/version/src/tag-version-separator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Fixture, normalizeCommitSHAs, normalizeEnvironment } from "@lerna/e2e-utils";

expect.addSnapshotSerializer({
serialize(str: string) {
return normalizeCommitSHAs(normalizeEnvironment(str));
},
test(val: string) {
return val != null && typeof val === "string";
},
});

describe("lerna-version-tag-version-separator qqqq", () => {
let fixture: Fixture;

beforeEach(async () => {
fixture = await Fixture.create({
e2eRoot: process.env.E2E_ROOT,
name: "lerna-version-tag-version-separator",
packageManager: "npm",
initializeGit: true,
lernaInit: { args: [`--packages="packages/*" --independent`] },
installDependencies: true,
});
await fixture.lerna("create package-a -y");
await fixture.lerna("create package-b -y");
await fixture.updateJson("lerna.json", (json) => ({
...json,
command: {
version: {
tagVersionSeparator: "__",
},
},
}));
await fixture.createInitialGitCommit();
await fixture.exec("git push origin test-main");
});
afterEach(() => fixture.destroy());

it("should create and read tags based on the custom tag-version-separator", async () => {
await fixture.lerna("version 3.3.3 -y");

// It should create one tag for each independently versioned package using the custom separator
const checkPackageTagsArePresentLocally = await fixture.exec("git describe --abbrev=0");
expect(checkPackageTagsArePresentLocally.combinedOutput).toMatchInlineSnapshot(`
package-a__3.3.3
`);

const checkPackageATagIsPresentOnRemote = await fixture.exec(
"git ls-remote origin refs/tags/package-a__3.3.3"
);
expect(checkPackageATagIsPresentOnRemote.combinedOutput).toMatchInlineSnapshot(`
{FULL_COMMIT_SHA} refs/tags/package-a__3.3.3
`);
const checkPackageBTagIsPresentOnRemote = await fixture.exec(
"git ls-remote origin refs/tags/package-b__3.3.3"
);
expect(checkPackageBTagIsPresentOnRemote.combinedOutput).toMatchInlineSnapshot(`
{FULL_COMMIT_SHA} refs/tags/package-b__3.3.3
`);

// Update package-a and version using conventional commits (to read from the tag)
await fixture.updateJson("packages/package-a/package.json", (json) => {
return {
...json,
description: json.description + "...with an update!",
};
});
await fixture.exec("git add packages/package-a/package.json");
await fixture.exec("git commit -m 'feat: update package-a'");
await fixture.exec("git push origin test-main");

const output = await fixture.lerna("version --conventional-commits -y", { silenceError: true });
expect(output.combinedOutput).toMatchInlineSnapshot(`
lerna notice cli v999.9.9-e2e.0
lerna info versioning independent
lerna info Looking for changed packages since package-a__3.3.3
lerna info getChangelogConfig Successfully resolved preset "conventional-changelog-angular"
Changes:
- package-a: 3.3.3 => 3.4.0
lerna info auto-confirmed
lerna info execute Skipping releases
lerna info git Pushing tags...
lerna success version finished
`);
});
});
6 changes: 6 additions & 0 deletions libs/commands/version/src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ const command: CommandModule = {
requiresArg: true,
defaultDescription: "v",
},
"tag-version-separator": {
describe: "Customize the tag version separator used when creating tags for independent versioning.",
type: "string",
requiresArg: true,
defaultDescription: "@",
},
"git-tag-command": {
describe:
"Allows users to specify a custom command to be used when applying git tags. For example, this may be useful for providing a wrapper command in CI/CD pipelines that have no direct write access.",
Expand Down
4 changes: 3 additions & 1 deletion libs/commands/version/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ interface VersionCommandConfigOptions extends CommandConfigOptions {
signGitTag?: boolean;
forceGitTag?: boolean;
tagVersionPrefix?: string;
tagVersionSeparator?: string;
createRelease?: "github" | "gitlab";
changelog?: boolean;
exact?: boolean;
Expand Down Expand Up @@ -864,9 +865,10 @@ class VersionCommand extends Command {
}

async gitCommitAndTagVersionForUpdates() {
const tagVersionSeparator = this.options.tagVersionSeparator || "@";
const tags = this.updates.map((node) => {
const pkg = getPackage(node);
return `${pkg.name}@${this.updatesVersions.get(node.name)}`;
return `${pkg.name}${tagVersionSeparator}${this.updatesVersions.get(node.name)}`;
});
const subject = this.options.message || "Publish";
const message = tags.reduce((msg, tag) => `${msg}${os.EOL} - ${tag}`, `${subject}${os.EOL}`);
Expand Down
8 changes: 7 additions & 1 deletion libs/core/src/lib/collect-updates/collect-project-updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export interface ProjectUpdateCollectorOptions {
conventionalGraduate?: string | boolean;
forceConventionalGraduate?: boolean;
excludeDependents?: boolean;
// Separator used within independent version tags, defaults to @
tagVersionSeparator?: string;
}

/**
Expand All @@ -46,6 +48,7 @@ export function collectProjectUpdates(
forceConventionalGraduate,
conventionalGraduate,
excludeDependents,
tagVersionSeparator,
} = commandOptions;

// If --conventional-commits and --conventional-graduate are both set, ignore --force-publish but consider --force-conventional-graduate
Expand All @@ -59,7 +62,10 @@ export function collectProjectUpdates(
// TODO: refactor to address type issues
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { sha, refCount, lastTagName } = describeRefSync(execOpts, commandOptions.includeMergedTags);
const { sha, refCount, lastTagName } = describeRefSync(
{ ...execOpts, separator: tagVersionSeparator },
commandOptions.includeMergedTags
);

if (refCount === "0" && forced.size === 0 && !committish) {
// no commits since previous release
Expand Down
37 changes: 37 additions & 0 deletions libs/core/src/lib/describe-ref.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,43 @@ describe("parser", () => {
expect(result.isDirty).toBe(true);
});

describe("custom tag-version-separator", () => {
it("matches independent tags using a custom tag-version-separator, CASE 1", () => {
childProcess.execSync.mockReturnValueOnce("pkg-name__1.2.3-4-g567890a");

const result = describeRefSync({ separator: "__" }) as DescribeRefDetailedResult;

expect(result.lastTagName).toBe("pkg-name__1.2.3");
expect(result.lastVersion).toBe("1.2.3");
});

it("matches independent tags using a custom tag-version-separator, CASE 2", () => {
childProcess.execSync.mockReturnValueOnce("pkg-name-1.2.3-4-g567890a");

const result = describeRefSync({ separator: "-" }) as DescribeRefDetailedResult;

expect(result.lastTagName).toBe("pkg-name-1.2.3");
expect(result.lastVersion).toBe("1.2.3");
});

it("matches independent tags for scoped packages", () => {
childProcess.execSync.mockReturnValueOnce("@scope/pkg-name_1.2.3-4-g567890a");

const result = describeRefSync({ separator: "_" }) as DescribeRefDetailedResult;

expect(result.lastTagName).toBe("@scope/pkg-name_1.2.3");
expect(result.lastVersion).toBe("1.2.3");
});

it("matches dirty annotations", () => {
childProcess.execSync.mockReturnValueOnce("pkg-name@@1.2.3-4-g567890a-dirty");

const result = describeRefSync({ separator: "@@" });

expect(result.isDirty).toBe(true);
});
});

it("handles non-matching strings safely", () => {
childProcess.execSync.mockReturnValueOnce("poopy-pants");

Expand Down
27 changes: 21 additions & 6 deletions libs/core/src/lib/describe-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ interface DescribeRefOptions {
cwd?: string | URL;
// Glob passed to `--match` flag
match?: string;
// Separator used within independent version tags, defaults to @
separator?: string;
}

// When annotated release tags are missing
Expand Down Expand Up @@ -60,7 +62,7 @@ export function describeRef(
// TODO: refactor based on TS feedback
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const result = parse(stdout, options.cwd);
const result = parse(stdout, options.cwd, options.separator);

log.verbose("git-describe", "%j => %j", options && options.match, stdout);
log.silly("git-describe", "parsed => %j", result);
Expand All @@ -69,12 +71,15 @@ export function describeRef(
});
}

export function describeRefSync(options = {}, includeMergedTags?: boolean) {
export function describeRefSync(
options: DescribeRefOptions = {},
includeMergedTags?: boolean
): DescribeRefFallbackResult | DescribeRefDetailedResult {
const stdout = childProcess.execSync("git", getArgs(options, includeMergedTags), options);
// TODO: refactor based on TS feedback
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const result = parse(stdout, options.cwd);
const result = parse(stdout, options.cwd, options.separator);

// only called by collect-updates with no matcher
log.silly("git-describe.sync", "%j => %j", stdout, result);
Expand All @@ -86,8 +91,15 @@ export function describeRefSync(options = {}, includeMergedTags?: boolean) {
* Parse git output and return relevant metadata.
* @param stdout Result of `git describe`
* @param [cwd] Defaults to `process.cwd()`
* @param [separator] Separator used within independent version tags, defaults to @
*/
function parse(stdout: string, cwd: string): DescribeRefFallbackResult | DescribeRefDetailedResult {
function parse(
stdout: string,
cwd: string,
separator: string
): DescribeRefFallbackResult | DescribeRefDetailedResult {
separator = separator || "@";

const minimalShaRegex = /^([0-9a-f]{7,40})(-dirty)?$/;
// when git describe fails to locate tags, it returns only the minimal sha
if (minimalShaRegex.test(stdout)) {
Expand All @@ -103,8 +115,11 @@ function parse(stdout: string, cwd: string): DescribeRefFallbackResult | Describ
return { refCount, sha, isDirty: Boolean(isDirty) };
}

const [, lastTagName, lastVersion, refCount, sha, isDirty] =
/^((?:.*@)?(.*))-(\d+)-g([0-9a-f]+)(-dirty)?$/.exec(stdout) || [];
// If the user has specified a custom separator, it may not be regex-safe, so escape it
const escapedSeparator = separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regexPattern = new RegExp(`^((?:.*${escapedSeparator})?(.*))-(\\d+)-g([0-9a-f]+)(-dirty)?$`);

const [, lastTagName, lastVersion, refCount, sha, isDirty] = regexPattern.exec(stdout) || [];

return { lastTagName, lastVersion, refCount, sha, isDirty: Boolean(isDirty) };
}

0 comments on commit 43de79c

Please sign in to comment.