From 918c5896621d8346ce17d1e089bebf77f56c0e46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 10:28:53 +0000 Subject: [PATCH 1/5] Initial plan From 46d39b016507b6ff7ebe616ff13c537a6f0672e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 10:37:40 +0000 Subject: [PATCH 2/5] Add branch pattern check to git plugin Co-authored-by: AlCalzone <17641229+AlCalzone@users.noreply.github.com> --- packages/plugin-git/src/index.test.ts | 147 ++++++++++++++++++++++++++ packages/plugin-git/src/index.ts | 77 ++++++++++++++ 2 files changed, 224 insertions(+) diff --git a/packages/plugin-git/src/index.test.ts b/packages/plugin-git/src/index.test.ts index 22cd62f..46e1339 100644 --- a/packages/plugin-git/src/index.test.ts +++ b/packages/plugin-git/src/index.test.ts @@ -15,6 +15,7 @@ describe("Git plugin", () => { context.sys.mockExec({ "git config --get user.name": "", "git config --get user.email": "", + "git rev-parse --abbrev-ref HEAD": "main", }); await assertReleaseError(() => gitPlugin.executeStage(context, DefaultStages.check), { @@ -31,6 +32,7 @@ describe("Git plugin", () => { context.sys.mockExec({ "git config --get user.name": "henlo", "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "main", "git rev-list --left-right --count HEAD...origin": "0\t2", }); @@ -48,6 +50,7 @@ describe("Git plugin", () => { context.sys.mockExec({ "git config --get user.name": "henlo", "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "main", "git rev-list --left-right --count HEAD...origin": "1\t1", }); @@ -66,6 +69,7 @@ describe("Git plugin", () => { context.sys.mockExec({ "git config --get user.name": "henlo", "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "main", "git rev-list --left-right --count HEAD...origin": "1\t0", "git status --porcelain": "whatever", }); @@ -84,6 +88,7 @@ describe("Git plugin", () => { context.sys.mockExec({ "git config --get user.name": "henlo", "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "main", "git rev-list --left-right --count HEAD...origin": "1\t0", "git status --porcelain": "whatever", }); @@ -100,6 +105,7 @@ describe("Git plugin", () => { context.sys.mockExec({ "git config --get user.name": "henlo", "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "main", "git rev-list --left-right --count HEAD...origin": "1\t0", "git status --porcelain": "", }); @@ -107,6 +113,147 @@ describe("Git plugin", () => { await gitPlugin.executeStage(context, DefaultStages.check); expect(context.errors).toHaveLength(0); }); + + it("succeeds when on the 'main' branch (default pattern)", async () => { + const gitPlugin = new GitPlugin(); + const context = createMockContext({ + plugins: [gitPlugin], + }); + context.sys.mockExec({ + "git config --get user.name": "henlo", + "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "main", + "git rev-list --left-right --count HEAD...origin": "1\t0", + "git status --porcelain": "", + }); + + await gitPlugin.executeStage(context, DefaultStages.check); + expect(context.errors).toHaveLength(0); + }); + + it("succeeds when on the 'master' branch (default pattern)", async () => { + const gitPlugin = new GitPlugin(); + const context = createMockContext({ + plugins: [gitPlugin], + }); + context.sys.mockExec({ + "git config --get user.name": "henlo", + "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "master", + "git rev-list --left-right --count HEAD...origin": "1\t0", + "git status --porcelain": "", + }); + + await gitPlugin.executeStage(context, DefaultStages.check); + expect(context.errors).toHaveLength(0); + }); + + it("raises a fatal error when not on an allowed branch", async () => { + const gitPlugin = new GitPlugin(); + const context = createMockContext({ + plugins: [gitPlugin], + }); + context.sys.mockExec({ + "git config --get user.name": "henlo", + "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "feature-branch", + }); + + await assertReleaseError(() => gitPlugin.executeStage(context, DefaultStages.check), { + fatal: true, + messageMatches: /Release can only be triggered from/i, + }); + }); + + it("succeeds with custom branch pattern (single string)", async () => { + const gitPlugin = new GitPlugin(); + const context = createMockContext({ + plugins: [gitPlugin], + argv: { branchPattern: ["develop"] }, + }); + context.sys.mockExec({ + "git config --get user.name": "henlo", + "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "develop", + "git rev-list --left-right --count HEAD...origin": "1\t0", + "git status --porcelain": "", + }); + + await gitPlugin.executeStage(context, DefaultStages.check); + expect(context.errors).toHaveLength(0); + }); + + it("succeeds with wildcard pattern matching release branches", async () => { + const gitPlugin = new GitPlugin(); + const context = createMockContext({ + plugins: [gitPlugin], + argv: { branchPattern: ["main", "release/*"] }, + }); + context.sys.mockExec({ + "git config --get user.name": "henlo", + "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "release/1.0.0", + "git rev-list --left-right --count HEAD...origin": "1\t0", + "git status --porcelain": "", + }); + + await gitPlugin.executeStage(context, DefaultStages.check); + expect(context.errors).toHaveLength(0); + }); + + it("fails with wildcard pattern when branch doesn't match", async () => { + const gitPlugin = new GitPlugin(); + const context = createMockContext({ + plugins: [gitPlugin], + argv: { branchPattern: ["main", "release/*"] }, + }); + context.sys.mockExec({ + "git config --get user.name": "henlo", + "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "feature/my-feature", + }); + + await assertReleaseError(() => gitPlugin.executeStage(context, DefaultStages.check), { + fatal: true, + messageMatches: /Release can only be triggered from/i, + }); + }); + + it("succeeds with regex pattern", async () => { + const gitPlugin = new GitPlugin(); + const context = createMockContext({ + plugins: [gitPlugin], + argv: { branchPattern: ["^(main|master|release/.+)$"] }, + }); + context.sys.mockExec({ + "git config --get user.name": "henlo", + "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "release/v1.2.3", + "git rev-list --left-right --count HEAD...origin": "1\t0", + "git status --porcelain": "", + }); + + await gitPlugin.executeStage(context, DefaultStages.check); + expect(context.errors).toHaveLength(0); + }); + + it("fails with regex pattern when branch doesn't match", async () => { + const gitPlugin = new GitPlugin(); + const context = createMockContext({ + plugins: [gitPlugin], + argv: { branchPattern: ["^(main|master)$"] }, + }); + context.sys.mockExec({ + "git config --get user.name": "henlo", + "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "develop", + }); + + await assertReleaseError(() => gitPlugin.executeStage(context, DefaultStages.check), { + fatal: true, + messageMatches: /Release can only be triggered from/i, + }); + }); }); describe("commit stage", () => { diff --git a/packages/plugin-git/src/index.ts b/packages/plugin-git/src/index.ts index 238e304..5738fb1 100644 --- a/packages/plugin-git/src/index.ts +++ b/packages/plugin-git/src/index.ts @@ -21,6 +21,13 @@ async function hasGitIdentity(context: Context): Promise { } } +async function getCurrentBranch(context: Context): Promise { + const { stdout: branch } = await context.sys.execRaw("git rev-parse --abbrev-ref HEAD", { + cwd: context.cwd, + }); + return branch; +} + async function getUpstream(context: Context): Promise { const { stdout: upstream } = await context.sys.execRaw( "git rev-parse --abbrev-ref --symbolic-full-name @{u}", @@ -67,6 +74,65 @@ async function gitStatus(context: Context): Promise { } } +function matchesBranchPattern(branch: string, pattern: string): boolean { + // Convert simplified wildcard to regex pattern + // * without leading dot expands to .* + let regexPattern = pattern; + + // Check if it's already a valid regex pattern + // If pattern starts with ^ or ends with $ or contains regex special chars (not just *), treat as regex + const isRegex = + /^\/.*\/[gimuy]*$/.test(pattern) || + /[[\]{}()+?.|\\]/.test(pattern) || + pattern.startsWith("^") || + pattern.endsWith("$"); + + if (!isRegex) { + // Escape special regex characters except * + regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&"); + // Replace * with .* + regexPattern = regexPattern.replace(/\*/g, ".*"); + // Anchor the pattern + regexPattern = `^${regexPattern}$`; + } else if (/^\/(.*)\/([gimuy]*)$/.test(pattern)) { + // Handle /pattern/flags format + const match = pattern.match(/^\/(.*)\/([gimuy]*)$/); + if (match) { + return new RegExp(match[1], match[2]).test(branch); + } + } + + try { + return new RegExp(regexPattern).test(branch); + } catch { + // If regex is invalid, treat as literal string match + return branch === pattern; + } +} + +function checkBranchPattern(context: Context, currentBranch: string): void { + const patterns = context.argv.branchPattern as string | string[] | undefined; + // Default to ["main", "master"] if not provided + const defaultPatterns = ["main", "master"]; + const branchPatterns = patterns + ? Array.isArray(patterns) + ? patterns + : [patterns] + : defaultPatterns; + + const matches = branchPatterns.some((pattern) => matchesBranchPattern(currentBranch, pattern)); + + if (!matches) { + const colors = context.cli.colors; + const patternsStr = + branchPatterns.length === 1 ? branchPatterns[0] : branchPatterns.join(", "); + const message = `Release can only be triggered from branches matching: ${colors.bold( + colors.blue(patternsStr), + )}. Current branch: ${colors.bold(colors.red(currentBranch))}`; + context.cli.fatal(message); + } +} + class GitPlugin implements Plugin { public readonly id = "git"; public readonly stages = [ @@ -103,6 +169,13 @@ class GitPlugin implements Plugin { description: "Do not push anything to the remote", default: false, }, + branchPattern: { + type: "string", + array: true, + description: + "Branch name patterns (supports wildcards and regex) that releases can be triggered from", + default: ["main", "master"], + }, }); } @@ -128,6 +201,10 @@ Note: If the current folder belongs to a different user than ${colors.bold( context.cli.fatal(message); } + // Check that we're on the correct branch + const currentBranch = await getCurrentBranch(context); + checkBranchPattern(context, currentBranch); + const lerna = context.hasData("lerna") && !!context.getData("lerna"); // check if there are untracked changes From 136863b3edd3fd88a2ab9b6a90735e9965400612 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:28:10 +0000 Subject: [PATCH 3/5] Use dynamic default branch from git symbolic-ref instead of hardcoded main/master Co-authored-by: AlCalzone <17641229+AlCalzone@users.noreply.github.com> --- packages/plugin-git/src/index.test.ts | 55 +++++++++++++++++++++++++-- packages/plugin-git/src/index.ts | 44 ++++++++++++++++----- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/packages/plugin-git/src/index.test.ts b/packages/plugin-git/src/index.test.ts index 46e1339..5c01baf 100644 --- a/packages/plugin-git/src/index.test.ts +++ b/packages/plugin-git/src/index.test.ts @@ -106,6 +106,7 @@ describe("Git plugin", () => { "git config --get user.name": "henlo", "git config --get user.email": "this.is@dog", "git rev-parse --abbrev-ref HEAD": "main", + "git symbolic-ref --short refs/remotes/origin/HEAD": "origin/main", "git rev-list --left-right --count HEAD...origin": "1\t0", "git status --porcelain": "", }); @@ -123,6 +124,7 @@ describe("Git plugin", () => { "git config --get user.name": "henlo", "git config --get user.email": "this.is@dog", "git rev-parse --abbrev-ref HEAD": "main", + "git symbolic-ref --short refs/remotes/origin/HEAD": "origin/main", "git rev-list --left-right --count HEAD...origin": "1\t0", "git status --porcelain": "", }); @@ -140,6 +142,7 @@ describe("Git plugin", () => { "git config --get user.name": "henlo", "git config --get user.email": "this.is@dog", "git rev-parse --abbrev-ref HEAD": "master", + "git symbolic-ref --short refs/remotes/origin/HEAD": "origin/master", "git rev-list --left-right --count HEAD...origin": "1\t0", "git status --porcelain": "", }); @@ -153,10 +156,15 @@ describe("Git plugin", () => { const context = createMockContext({ plugins: [gitPlugin], }); - context.sys.mockExec({ - "git config --get user.name": "henlo", - "git config --get user.email": "this.is@dog", - "git rev-parse --abbrev-ref HEAD": "feature-branch", + context.sys.mockExec((cmd) => { + if (cmd === "git config --get user.name") return "henlo"; + if (cmd === "git config --get user.email") return "this.is@dog"; + if (cmd === "git rev-parse --abbrev-ref HEAD") return "feature-branch"; + if (cmd === "git symbolic-ref --short refs/remotes/origin/HEAD") { + // Simulate failure - fall back to ["main", "master"] + throw new Error("not a symbolic ref"); + } + throw new Error(`mock missing for command "${cmd}"!`); }); await assertReleaseError(() => gitPlugin.executeStage(context, DefaultStages.check), { @@ -165,6 +173,45 @@ describe("Git plugin", () => { }); }); + it("uses dynamic default branch from git symbolic-ref", async () => { + const gitPlugin = new GitPlugin(); + const context = createMockContext({ + plugins: [gitPlugin], + }); + context.sys.mockExec({ + "git config --get user.name": "henlo", + "git config --get user.email": "this.is@dog", + "git rev-parse --abbrev-ref HEAD": "develop", + "git symbolic-ref --short refs/remotes/origin/HEAD": "origin/develop", + "git rev-list --left-right --count HEAD...origin": "1\t0", + "git status --porcelain": "", + }); + + await gitPlugin.executeStage(context, DefaultStages.check); + expect(context.errors).toHaveLength(0); + }); + + it("falls back to main/master when git symbolic-ref fails", async () => { + const gitPlugin = new GitPlugin(); + const context = createMockContext({ + plugins: [gitPlugin], + }); + context.sys.mockExec((cmd) => { + if (cmd === "git config --get user.name") return "henlo"; + if (cmd === "git config --get user.email") return "this.is@dog"; + if (cmd === "git rev-parse --abbrev-ref HEAD") return "main"; + if (cmd === "git symbolic-ref --short refs/remotes/origin/HEAD") { + throw new Error("not a symbolic ref"); + } + if (cmd === "git rev-list --left-right --count HEAD...origin") return "1\t0"; + if (cmd === "git status --porcelain") return ""; + throw new Error(`mock missing for command "${cmd}"!`); + }); + + await gitPlugin.executeStage(context, DefaultStages.check); + expect(context.errors).toHaveLength(0); + }); + it("succeeds with custom branch pattern (single string)", async () => { const gitPlugin = new GitPlugin(); const context = createMockContext({ diff --git a/packages/plugin-git/src/index.ts b/packages/plugin-git/src/index.ts index 5738fb1..63ce404 100644 --- a/packages/plugin-git/src/index.ts +++ b/packages/plugin-git/src/index.ts @@ -28,6 +28,21 @@ async function getCurrentBranch(context: Context): Promise { return branch; } +async function getDefaultBranch(context: Context): Promise { + try { + const { stdout } = await context.sys.execRaw( + "git symbolic-ref --short refs/remotes/origin/HEAD", + { cwd: context.cwd }, + ); + // Output is like "origin/main", extract the branch name + const match = stdout.match(/^origin\/(.+)$/); + return match ? match[1] : undefined; + } catch { + // If the command fails (e.g., symbolic ref not set), return undefined + return undefined; + } +} + async function getUpstream(context: Context): Promise { const { stdout: upstream } = await context.sys.execRaw( "git rev-parse --abbrev-ref --symbolic-full-name @{u}", @@ -110,15 +125,23 @@ function matchesBranchPattern(branch: string, pattern: string): boolean { } } -function checkBranchPattern(context: Context, currentBranch: string): void { +async function checkBranchPattern(context: Context, currentBranch: string): Promise { const patterns = context.argv.branchPattern as string | string[] | undefined; - // Default to ["main", "master"] if not provided - const defaultPatterns = ["main", "master"]; - const branchPatterns = patterns - ? Array.isArray(patterns) - ? patterns - : [patterns] - : defaultPatterns; + + let branchPatterns: string[]; + if (patterns) { + // User provided patterns + branchPatterns = Array.isArray(patterns) ? patterns : [patterns]; + } else { + // Try to get the default branch dynamically + const defaultBranch = await getDefaultBranch(context); + if (defaultBranch) { + branchPatterns = [defaultBranch]; + } else { + // Fallback to common defaults if we can't determine the default branch + branchPatterns = ["main", "master"]; + } + } const matches = branchPatterns.some((pattern) => matchesBranchPattern(currentBranch, pattern)); @@ -174,7 +197,8 @@ class GitPlugin implements Plugin { array: true, description: "Branch name patterns (supports wildcards and regex) that releases can be triggered from", - default: ["main", "master"], + defaultDescription: + "The repository's default branch (from git symbolic-ref), or 'main'/'master' if that fails", }, }); } @@ -203,7 +227,7 @@ Note: If the current folder belongs to a different user than ${colors.bold( // Check that we're on the correct branch const currentBranch = await getCurrentBranch(context); - checkBranchPattern(context, currentBranch); + await checkBranchPattern(context, currentBranch); const lerna = context.hasData("lerna") && !!context.getData("lerna"); From d20c7304f61dfeb608141f01927a38e3097fbebf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:44:37 +0000 Subject: [PATCH 4/5] Simplify pattern matching to only support * wildcard, remove regex support Co-authored-by: AlCalzone <17641229+AlCalzone@users.noreply.github.com> --- packages/plugin-git/src/index.test.ts | 36 --------------------------- packages/plugin-git/src/index.ts | 36 ++++++--------------------- 2 files changed, 8 insertions(+), 64 deletions(-) diff --git a/packages/plugin-git/src/index.test.ts b/packages/plugin-git/src/index.test.ts index 5c01baf..0d9a22a 100644 --- a/packages/plugin-git/src/index.test.ts +++ b/packages/plugin-git/src/index.test.ts @@ -265,42 +265,6 @@ describe("Git plugin", () => { messageMatches: /Release can only be triggered from/i, }); }); - - it("succeeds with regex pattern", async () => { - const gitPlugin = new GitPlugin(); - const context = createMockContext({ - plugins: [gitPlugin], - argv: { branchPattern: ["^(main|master|release/.+)$"] }, - }); - context.sys.mockExec({ - "git config --get user.name": "henlo", - "git config --get user.email": "this.is@dog", - "git rev-parse --abbrev-ref HEAD": "release/v1.2.3", - "git rev-list --left-right --count HEAD...origin": "1\t0", - "git status --porcelain": "", - }); - - await gitPlugin.executeStage(context, DefaultStages.check); - expect(context.errors).toHaveLength(0); - }); - - it("fails with regex pattern when branch doesn't match", async () => { - const gitPlugin = new GitPlugin(); - const context = createMockContext({ - plugins: [gitPlugin], - argv: { branchPattern: ["^(main|master)$"] }, - }); - context.sys.mockExec({ - "git config --get user.name": "henlo", - "git config --get user.email": "this.is@dog", - "git rev-parse --abbrev-ref HEAD": "develop", - }); - - await assertReleaseError(() => gitPlugin.executeStage(context, DefaultStages.check), { - fatal: true, - messageMatches: /Release can only be triggered from/i, - }); - }); }); describe("commit stage", () => { diff --git a/packages/plugin-git/src/index.ts b/packages/plugin-git/src/index.ts index 63ce404..680a211 100644 --- a/packages/plugin-git/src/index.ts +++ b/packages/plugin-git/src/index.ts @@ -90,35 +90,15 @@ async function gitStatus(context: Context): Promise { } function matchesBranchPattern(branch: string, pattern: string): boolean { - // Convert simplified wildcard to regex pattern - // * without leading dot expands to .* - let regexPattern = pattern; - - // Check if it's already a valid regex pattern - // If pattern starts with ^ or ends with $ or contains regex special chars (not just *), treat as regex - const isRegex = - /^\/.*\/[gimuy]*$/.test(pattern) || - /[[\]{}()+?.|\\]/.test(pattern) || - pattern.startsWith("^") || - pattern.endsWith("$"); - - if (!isRegex) { - // Escape special regex characters except * - regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&"); - // Replace * with .* - regexPattern = regexPattern.replace(/\*/g, ".*"); - // Anchor the pattern - regexPattern = `^${regexPattern}$`; - } else if (/^\/(.*)\/([gimuy]*)$/.test(pattern)) { - // Handle /pattern/flags format - const match = pattern.match(/^\/(.*)\/([gimuy]*)$/); - if (match) { - return new RegExp(match[1], match[2]).test(branch); - } - } + // Support only * as a wildcard that matches 1 or more characters + // Escape all regex special characters except * + const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".+"); + + // Anchor the pattern to match the entire branch name + const anchored = `^${regexPattern}$`; try { - return new RegExp(regexPattern).test(branch); + return new RegExp(anchored).test(branch); } catch { // If regex is invalid, treat as literal string match return branch === pattern; @@ -196,7 +176,7 @@ class GitPlugin implements Plugin { type: "string", array: true, description: - "Branch name patterns (supports wildcards and regex) that releases can be triggered from", + "Branch name patterns (supports * wildcard) that releases can be triggered from", defaultDescription: "The repository's default branch (from git symbolic-ref), or 'main'/'master' if that fails", }, From 2837ea51d88c36aa5618f81dd02334a73815cc0f Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Mon, 20 Oct 2025 19:40:49 +0200 Subject: [PATCH 5/5] chore: cleanup, documentation --- README.md | 46 +++++++++++++++++++------------- packages/plugin-git/src/index.ts | 21 ++++++--------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index b5f8773..b806d47 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,18 @@ Automate the monotonous tasks when it comes to releasing new versions of your project. The features include: -- Bump the version in `package.json` (and `package-lock.json`) -- Update the changelog headline with the new version and release date whenever a release is made -- Move old changelog entries into another file -- Add the changelog to the release commit and create a tag for it. -- Support for monorepos managed with `lerna` or `yarn workspaces` (using additional `yarn` plugins) -- Support for custom scripts during the release lifecycle -- Check licenses for outdated copyrights -- Dry runs and checking for errors -- **ioBroker only**: - - Bump the version in `io-package.json` - - Update the `news` in `io-package.json` and auto-translate the changelog to the other languages - - Remove old `news` +- Bump the version in `package.json` (and `package-lock.json`) +- Update the changelog headline with the new version and release date whenever a release is made +- Move old changelog entries into another file +- Add the changelog to the release commit and create a tag for it. +- Support for monorepos managed with `lerna` or `yarn workspaces` (using additional `yarn` plugins) +- Support for custom scripts during the release lifecycle +- Check licenses for outdated copyrights +- Dry runs and checking for errors +- **ioBroker only**: + - Bump the version in `io-package.json` + - Update the `news` in `io-package.json` and auto-translate the changelog to the other languages + - Remove old `news` Together with the corresponding **Github Actions** workflow (more on that below) this enables auto-publishing on `npm` and `Github Releases` if the build was successful. @@ -72,8 +72,8 @@ In order to use this script, you need to maintain the changelog in either `READM ## **WORK IN PROGRESS** -- Did some changes -- Did some more changes +- Did some changes +- Did some more changes ## v0.0.1 (2020-01-01) @@ -166,8 +166,8 @@ Instead of manually providing all options, you can configure the release process "plugins": ["iobroker", "lerna"], // Objects "exec": { - "before_commit": "echo Hello World!" - } + "before_commit": "echo Hello World!", + }, } ``` @@ -291,7 +291,7 @@ or to `.releaseconfig.json` if you're using that: ```jsonc { // ... other options - "plugins": ["iobroker", "license"] + "plugins": ["iobroker", "license"], } ``` @@ -342,6 +342,14 @@ When this option is set, nothing will be pushed to the remote. This option can b npm run release patch -- --noPush ``` +#### Configure branches from which releases can be created (`--branchPattern`) + +By default, the release script only allows creating releases from the repository's default branch (usually `main` or `master`). If you want to allow releases from other branches, you can use this option. It supports `*` as a wildcard, e.g. `release/*` to allow releases from all branches starting with `release/`. Can be specified multiple times. + +```bash +npm run release patch -- --branchPattern main --branchPattern release/* +``` + ### `changelog` plugin options #### Limit the number of entries in README.md (`--numChangelogEntries` or `-n`) @@ -402,8 +410,8 @@ To run custom scripts, you can use these options. They accept an object defining // Run "echo 1", "echo 2", "echo 3" after the "push" stage "after_push": ["echo 1", "echo 2", "echo 3"], // Run "sudo shutdown" before the "check" stage - "before_check": "sudo shutdown" - } + "before_check": "sudo shutdown", + }, } ``` diff --git a/packages/plugin-git/src/index.ts b/packages/plugin-git/src/index.ts index 680a211..a6e0046 100644 --- a/packages/plugin-git/src/index.ts +++ b/packages/plugin-git/src/index.ts @@ -36,9 +36,8 @@ async function getDefaultBranch(context: Context): Promise { ); // Output is like "origin/main", extract the branch name const match = stdout.match(/^origin\/(.+)$/); - return match ? match[1] : undefined; + return match?.[1]; } catch { - // If the command fails (e.g., symbolic ref not set), return undefined return undefined; } } @@ -110,10 +109,9 @@ async function checkBranchPattern(context: Context, currentBranch: string): Prom let branchPatterns: string[]; if (patterns) { - // User provided patterns branchPatterns = Array.isArray(patterns) ? patterns : [patterns]; } else { - // Try to get the default branch dynamically + // If the user did not specify any patterns, default to the repository's default branch const defaultBranch = await getDefaultBranch(context); if (defaultBranch) { branchPatterns = [defaultBranch]; @@ -126,12 +124,11 @@ async function checkBranchPattern(context: Context, currentBranch: string): Prom const matches = branchPatterns.some((pattern) => matchesBranchPattern(currentBranch, pattern)); if (!matches) { - const colors = context.cli.colors; - const patternsStr = - branchPatterns.length === 1 ? branchPatterns[0] : branchPatterns.join(", "); - const message = `Release can only be triggered from branches matching: ${colors.bold( - colors.blue(patternsStr), - )}. Current branch: ${colors.bold(colors.red(currentBranch))}`; + const message = `Release can only be triggered from branches matching: +${context.cli.colors.blue(branchPatterns.map((p) => `ยท ${p}`).join("\n"))} +Current branch: ${context.cli.colors.red(currentBranch)} + +Note: Use the --branch-pattern option to customize this behavior.`; context.cli.fatal(message); } } @@ -176,9 +173,7 @@ class GitPlugin implements Plugin { type: "string", array: true, description: - "Branch name patterns (supports * wildcard) that releases can be triggered from", - defaultDescription: - "The repository's default branch (from git symbolic-ref), or 'main'/'master' if that fails", + "On which git branches a release may be created. Supports '*' as a wildcard. Defaults to the repository's default branch, or ['main', 'master'] if it cannot be determined.", }, }); }