Skip to content

Commit

Permalink
refactor: add azure devops detection
Browse files Browse the repository at this point in the history
  • Loading branch information
azurebot committed Jun 11, 2024
1 parent f7cd242 commit 56b8034
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 14 deletions.
79 changes: 67 additions & 12 deletions src/config/__tests__/changelog-preset-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getChangelogPresetConfig } from "../changelog-preset-config";

describe("changelog-preset-config", () => {
it("should return the default config", () => {
const config = getChangelogPresetConfig({}, {} as never);
const config = getChangelogPresetConfig({}, {} as never, null);

expect(config).toMatchObject({
commitUrlFormat: "{{host}}/{{owner}}/{{repository}}/commit/{{hash}}",
Expand Down Expand Up @@ -69,6 +69,7 @@ describe("changelog-preset-config", () => {
},
},
{} as never,
null,
);

expect(config).toMatchObject({
Expand Down Expand Up @@ -98,14 +99,18 @@ describe("changelog-preset-config", () => {
});

it("should be able to override from CLI arguments", () => {
const config = getChangelogPresetConfig({}, {
commitUrlFormat: "{{host}}/fork-version/commit/{{hash}}",
compareUrlFormat:
"{{host}}/fork-version/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}",
issueUrlFormat: "{{host}}/fork-version/issues/{{id}}",
userUrlFormat: "{{host}}/fork-version/user/{{user}}",
releaseCommitMessageFormat: "chore(release): {{currentTag}} [skip ci]",
} as never);
const config = getChangelogPresetConfig(
{},
{
commitUrlFormat: "{{host}}/fork-version/commit/{{hash}}",
compareUrlFormat:
"{{host}}/fork-version/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}",
issueUrlFormat: "{{host}}/fork-version/issues/{{id}}",
userUrlFormat: "{{host}}/fork-version/user/{{user}}",
releaseCommitMessageFormat: "chore(release): {{currentTag}} [skip ci]",
} as never,
null,
);

expect(config.commitUrlFormat).toBe("{{host}}/fork-version/commit/{{hash}}");
expect(config.compareUrlFormat).toBe(
Expand All @@ -122,16 +127,66 @@ describe("changelog-preset-config", () => {
releaseMessageSuffix: "[skip ci]",
},
{} as never,
null,
);

expect(config.releaseCommitMessageFormat).toBe("chore(release): {{currentTag}} [skip ci]");
});

it("should be able to append a releaseMessageSuffix to the releaseCommitMessageFormat from CLI arguments", () => {
const config = getChangelogPresetConfig({}, {
releaseMessageSuffix: "[no ci]",
} as never);
const config = getChangelogPresetConfig(
{},
{
releaseMessageSuffix: "[no ci]",
} as never,
null,
);

expect(config.releaseCommitMessageFormat).toBe("chore(release): {{currentTag}} [no ci]");
});

it("should be able to detect the git host", () => {
const config = getChangelogPresetConfig({}, {} as never, {
detectedGitHost: "Azure",
commitUrlFormat:
"{{host}}/ORGANISATION/PROJECT/_git/REPOSITORY/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}",
compareUrlFormat: "{{host}}/ORGANISATION/PROJECT/_git/REPOSITORY/commit/{{hash}}",
issueUrlFormat: "{{host}}/ORGANISATION/PROJECT/_workitems/edit/{{id}}",
});

expect(config.commitUrlFormat).toBe(
"{{host}}/ORGANISATION/PROJECT/_git/REPOSITORY/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}",
);
expect(config.compareUrlFormat).toBe(
"{{host}}/ORGANISATION/PROJECT/_git/REPOSITORY/commit/{{hash}}",
);
expect(config.issueUrlFormat).toBe("{{host}}/ORGANISATION/PROJECT/_workitems/edit/{{id}}");
});

it("should still be able to override the detected git host from configs", () => {
const config = getChangelogPresetConfig(
{
changelogPresetConfig: {
commitUrlFormat: "{{host}}/fork-version/commit/{{hash}}",
},
},
{
compareUrlFormat:
"{{host}}/fork-version/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}",
} as never,
{
detectedGitHost: "Azure",
commitUrlFormat:
"{{host}}/ORGANISATION/PROJECT/_git/REPOSITORY/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}",
compareUrlFormat: "{{host}}/ORGANISATION/PROJECT/_git/REPOSITORY/commit/{{hash}}",
issueUrlFormat: "{{host}}/ORGANISATION/PROJECT/_workitems/edit/{{id}}",
},
);

expect(config.commitUrlFormat).toBe("{{host}}/fork-version/commit/{{hash}}");
expect(config.compareUrlFormat).toBe(
"{{host}}/fork-version/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}",
);
expect(config.issueUrlFormat).toBe("{{host}}/ORGANISATION/PROJECT/_workitems/edit/{{id}}");
});
});
52 changes: 52 additions & 0 deletions src/config/__tests__/detect-git-host.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { execFileSync } from "child_process";
import { createTestDir } from "../../../tests/create-test-directory";
import { detectGitHost } from "../detect-git-host";

describe("detect-git-host", () => {
it("should detect a https azure git host", async () => {
const { testFolder } = await createTestDir("detect-git-host");

execFileSync(
"git",
[
"remote",
"add",
"origin",
"https://ORGANISATION@dev.azure.com/ORGANISATION/PROJECT/_git/REPOSITORY",
],
{ cwd: testFolder },
);

const gitHost = await detectGitHost(testFolder);

expect(gitHost?.detectedGitHost).toBe("Azure");
expect(gitHost?.commitUrlFormat).toBe(
"{{host}}/ORGANISATION/PROJECT/_git/REPOSITORY/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}",
);
expect(gitHost?.compareUrlFormat).toBe(
"{{host}}/ORGANISATION/PROJECT/_git/REPOSITORY/commit/{{hash}}",
);
expect(gitHost?.issueUrlFormat).toBe("{{host}}/ORGANISATION/PROJECT/_workitems/edit/{{id}}");
});

it("should detect a ssh azure git host", async () => {
const { testFolder } = await createTestDir("detect-git-host");

execFileSync(
"git",
["remote", "add", "origin", "git@ssh.dev.azure.com:v3/ORGANISATION/PROJECT/REPOSITORY"],
{ cwd: testFolder },
);

const gitHost = await detectGitHost(testFolder);

expect(gitHost?.detectedGitHost).toBe("Azure");
expect(gitHost?.commitUrlFormat).toBe(
"{{host}}/ORGANISATION/PROJECT/_git/REPOSITORY/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}",
);
expect(gitHost?.compareUrlFormat).toBe(
"{{host}}/ORGANISATION/PROJECT/_git/REPOSITORY/commit/{{hash}}",
);
expect(gitHost?.issueUrlFormat).toBe("{{host}}/ORGANISATION/PROJECT/_workitems/edit/{{id}}");
});
});
12 changes: 12 additions & 0 deletions src/config/changelog-preset-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import conventionalChangelogConfigSpec from "conventional-changelog-config-spec"

import { ChangelogPresetConfigSchema, type ForkConfig } from "./schema";
import type { getCliArguments } from "./cli-arguments";
import type { DetectedGitHost } from "./detect-git-host";

export function getChangelogPresetConfig(
mergedConfig: Partial<ForkConfig> | undefined,
cliArgumentsFlags: ReturnType<typeof getCliArguments>["flags"],
detectedGitHost: DetectedGitHost | null,
) {
const preset: { name: string; [_: string]: unknown } = {
name: "conventionalcommits",
Expand All @@ -20,6 +22,16 @@ export function getChangelogPresetConfig(
});
}

// If we've detected a git host, use the values from the detected host now so that they can
// be overwritten by the users config later
if (detectedGitHost) {
Object.entries(detectedGitHost).forEach(([key, value]) => {
if (value !== undefined) {
preset[key] = value;
}
});
}

// Then overwrite with any values from the users config
if (
mergedConfig?.changelogPresetConfig &&
Expand Down
1 change: 1 addition & 0 deletions src/config/cli-arguments.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export function getCliArguments() {
nextVersion: { type: "string" },

// Flags
allowMultipleVersions: { type: "boolean" },
commitAll: { type: "boolean" },
debug: { type: "boolean" },
dryRun: { type: "boolean" },
Expand Down
78 changes: 78 additions & 0 deletions src/config/detect-git-host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { execFile } from "node:child_process";

export interface DetectedGitHost {
detectedGitHost: string;
commitUrlFormat: string;
compareUrlFormat: string;
issueUrlFormat: string;
}

/**
* Conventional-Changelog already supports the following git hosts:
* - Github
* - Gitlab
* - Bitbucket
*
* We want to detect if the user is using another host such as Azure DevOps,
* if so we need to create the correct URLs so the changelog is generated
* correctly.
*/
export async function detectGitHost(cwd: string): Promise<DetectedGitHost | null> {
const remoteUrl = (await new Promise((onResolve, onReject) => {
execFile("git", ["config", "--get", "remote.origin.url"], { cwd }, (error, stdout, stderr) => {
if (error) {
onReject(error);
}
onResolve(stdout ? stdout.trim() : stderr);
});
})) as string;

// A checked out Azure DevOps remote URL looks like one of these:
//
// | Checkout Type | Remote URL |
// | ------------- | --------------------------------------------------------------------------------------- |
// | HTTPS | https://{{ORGANISATION}}@dev.azure.com/{{ORGANISATION}}/{{PROJECT}}/_git/{{REPOSITORY}} |
// | SSH | git@ssh.dev.azure.com:v3/{{ORGANISATION}}/{{PROJECT}}/{{REPOSITORY}} |
//
if (remoteUrl.startsWith("https://") && remoteUrl.includes("@dev.azure.com/")) {
/**
* [Regex101.com](https://regex101.com/r/fF7HUc/1)
*/
const match =
/^https:\/\/(?<atorganisation>.*?)@dev.azure.com\/(?<organisation>.*?)\/(?<project>.*?)\/_git\/(?<repository>.*?)(?:\.git)?$/.exec(
remoteUrl,
);

if (match?.groups) {
const { organisation = "", project = "", repository = "" } = match.groups;

return {
detectedGitHost: "Azure",
commitUrlFormat: `{{host}}/${organisation}/${project}/_git/${repository}/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}`,
compareUrlFormat: `{{host}}/${organisation}/${project}/_git/${repository}/commit/{{hash}}`,
issueUrlFormat: `{{host}}/${organisation}/${project}/_workitems/edit/{{id}}`,
};
}
} else if (remoteUrl.startsWith("git@ssh.dev.azure.com:")) {
/**
* [Regex101.com](https://regex101.com/r/VhNxWr/1)
*/
const match =
/^git@ssh.dev.azure.com:v\d\/(?<organisation>.*?)\/(?<project>.*?)\/(?<repository>.*?)(?:\.git)?$/.exec(
remoteUrl,
);

if (match?.groups) {
const { organisation = "", project = "", repository = "" } = match.groups;

return {
detectedGitHost: "Azure",
commitUrlFormat: `{{host}}/${organisation}/${project}/_git/${repository}/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}`,
compareUrlFormat: `{{host}}/${organisation}/${project}/_git/${repository}/commit/{{hash}}`,
issueUrlFormat: `{{host}}/${organisation}/${project}/_workitems/edit/{{id}}`,
};
}
}

return null;
}
9 changes: 8 additions & 1 deletion src/config/user-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ForkConfigSchema, type ForkConfig } from "./schema";
import { DEFAULT_CONFIG } from "./defaults";
import { getCliArguments } from "./cli-arguments";
import { getChangelogPresetConfig } from "./changelog-preset-config";
import { detectGitHost } from "./detect-git-host";

/**
* Name of the key in the package.json file that contains the users configuration.
Expand Down Expand Up @@ -51,6 +52,8 @@ export async function getUserConfig(): Promise<ForkConfig> {
});
}

const detectedGitHost = await detectGitHost(cwd);

return {
...mergedConfig,

Expand All @@ -62,7 +65,11 @@ export async function getUserConfig(): Promise<ForkConfig> {
preRelease:
// Meow doesn't support multiple flags with the same name, so we need to check both.
cliArguments.flags.preReleaseTag ?? cliArguments.flags.preRelease ?? configFile.preRelease,
changelogPresetConfig: getChangelogPresetConfig(mergedConfig, cliArguments.flags),
changelogPresetConfig: getChangelogPresetConfig(
mergedConfig,
cliArguments.flags,
detectedGitHost,
),
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/process/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function getNewReleaseContent(
...config.changelogPresetConfig,
},
tagPrefix: config.tagPrefix,
warn: (...message: string[]) => logger.error("[conventional-changelog] ", ...message),
warn: (...message: string[]) => logger.debug("[conventional-changelog] ", ...message),
cwd: config.path,
},
{
Expand Down

0 comments on commit 56b8034

Please sign in to comment.