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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@coze-arch/rush-publish-plugin",
"comment": "support glob and regex patterns for branch validation",
"type": "minor"
}
],
"packageName": "@coze-arch/rush-publish-plugin",
"email": "tecvan.fe@qq.com"
}
3 changes: 3 additions & 0 deletions common/config/subspaces/default/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { logger } from '@coze-arch/logger';

import { randomHash } from '@/utils/random';
import { ensureNotUncommittedChanges, isMainBranch } from '@/utils/git';
import { ensureNotUncommittedChanges } from '@/utils/git';
import { getRushConfiguration } from '@/utils/get-rush-config';
import { release } from '@/action/release/action';
import { generatePublishManifest } from '@/action/publish/version';
Expand Down Expand Up @@ -105,7 +105,6 @@ describe('publish action', () => {
vi.mocked(confirmForPublish).mockResolvedValue(true);
vi.mocked(applyPublishManifest).mockResolvedValue(['package.json']);
vi.mocked(generateChangelog).mockResolvedValue(['CHANGELOG.md']);
vi.mocked(isMainBranch).mockResolvedValue(true);
});

it('should publish packages successfully', async () => {
Expand Down Expand Up @@ -177,68 +176,6 @@ describe('publish action', () => {
expect(generatePublishManifest).not.toHaveBeenCalled();
});

it('should stop if not in main branch for production release', async () => {
vi.mocked(isMainBranch).mockResolvedValue(false);
delete process.env.SKIP_BRANCH_CHECK;
await publish({
to: ['package-1'],
repoUrl: 'git@github.com:example/repo.git',
});
expect(logger.error).toHaveBeenCalledWith(
'You are not in main branch, please switch to main branch and try again.',
);
expect(applyPublishManifest).not.toHaveBeenCalled();
});

it('should skip branch check when SKIP_BRANCH_CHECK is true', async () => {
vi.mocked(isMainBranch).mockResolvedValue(false);
process.env.SKIP_BRANCH_CHECK = 'true';

await publish({
to: ['package-1'],
repoUrl: 'git@github.com:example/repo.git',
});

expect(logger.error).not.toHaveBeenCalledWith(
'You are not in main branch, please switch to main branch and try again.',
);
expect(applyPublishManifest).toHaveBeenCalled();

delete process.env.SKIP_BRANCH_CHECK;
});

it('should allow non-main branch for beta releases', async () => {
vi.mocked(isMainBranch).mockResolvedValue(false);
vi.mocked(generatePublishManifest).mockResolvedValue({
manifests: mockPublishManifests,
bumpPolicy: BumpType.BETA,
});

await publish({
to: ['package-1'],
repoUrl: 'git@github.com:example/repo.git',
});

expect(logger.error).not.toHaveBeenCalled();
expect(applyPublishManifest).toHaveBeenCalled();
});

it('should allow non-main branch for alpha releases', async () => {
vi.mocked(isMainBranch).mockResolvedValue(false);
vi.mocked(generatePublishManifest).mockResolvedValue({
manifests: mockPublishManifests,
bumpPolicy: BumpType.ALPHA,
});

await publish({
to: ['package-1'],
repoUrl: 'git@github.com:example/repo.git',
});

expect(logger.error).not.toHaveBeenCalled();
expect(applyPublishManifest).toHaveBeenCalled();
});

it('should stop if user does not confirm', async () => {
vi.mocked(confirmForPublish).mockResolvedValue(false);
await publish({
Expand Down
276 changes: 276 additions & 0 deletions packages/rush-plugins/publish/__tests__/actions/release/plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,5 +229,281 @@ describe('plan', () => {
checkReleasePlan(releaseManifests, 'any-branch', []),
).toThrow('For LATEST release, should be on one of these branches: .');
});

describe('glob pattern matching', () => {
const releaseManifests: ReleaseManifest[] = [
{
project: mockProject1,
version: '1.0.0',
},
];

it('should match branches with wildcard pattern', () => {
const allowBranches = [
'chore/*',
'release/*',
'integration/*',
'master',
];

// Should match
expect(() =>
checkReleasePlan(
releaseManifests,
'chore/upgrade-version',
allowBranches,
),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'chore/fix-bug', allowBranches),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'release/v1.0.0', allowBranches),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'integration/test', allowBranches),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'master', allowBranches),
).not.toThrow();

// Should not match
expect(() =>
checkReleasePlan(releaseManifests, 'feature/new', allowBranches),
).toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'main', allowBranches),
).toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'chore', allowBranches),
).toThrow();
});

it('should match branches with double-star pattern', () => {
const allowBranches = ['release/**'];

// Should match
expect(() =>
checkReleasePlan(releaseManifests, 'release/v1.0.0', allowBranches),
).not.toThrow();
expect(() =>
checkReleasePlan(
releaseManifests,
'release/2024/v1.0.0',
allowBranches,
),
).not.toThrow();
expect(() =>
checkReleasePlan(
releaseManifests,
'release/prod/v1.0.0',
allowBranches,
),
).not.toThrow();

// Should not match
expect(() =>
checkReleasePlan(releaseManifests, 'chore/fix', allowBranches),
).toThrow();
});

it('should match branches with prefix pattern', () => {
const allowBranches = ['feat-*', 'hotfix-*'];

// Should match
expect(() =>
checkReleasePlan(releaseManifests, 'feat-123', allowBranches),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'hotfix-urgent', allowBranches),
).not.toThrow();

// Should not match
expect(() =>
checkReleasePlan(releaseManifests, 'feat/123', allowBranches),
).toThrow();
});

it('should match branches with suffix pattern', () => {
const allowBranches = ['*-prod', '*-staging'];

// Should match
expect(() =>
checkReleasePlan(releaseManifests, 'release-prod', allowBranches),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'deploy-staging', allowBranches),
).not.toThrow();

// Should not match
expect(() =>
checkReleasePlan(releaseManifests, 'release-dev', allowBranches),
).toThrow();
});
});

describe('regex pattern matching', () => {
const releaseManifests: ReleaseManifest[] = [
{
project: mockProject1,
version: '1.0.0',
},
];

it('should match branches with regex pattern', () => {
const allowBranches = ['/^(main|master|develop)$/'];

// Should match
expect(() =>
checkReleasePlan(releaseManifests, 'main', allowBranches),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'master', allowBranches),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'develop', allowBranches),
).not.toThrow();

// Should not match
expect(() =>
checkReleasePlan(releaseManifests, 'main-backup', allowBranches),
).toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'feature/test', allowBranches),
).toThrow();
});

it('should match branches with version number regex', () => {
const allowBranches = ['/^release\\/v\\d+\\.\\d+\\.\\d+$/'];

// Should match
expect(() =>
checkReleasePlan(releaseManifests, 'release/v1.0.0', allowBranches),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'release/v2.10.5', allowBranches),
).not.toThrow();

// Should not match
expect(() =>
checkReleasePlan(releaseManifests, 'release/v1', allowBranches),
).toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'release/v1.0', allowBranches),
).toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'chore/v1.0.0', allowBranches),
).toThrow();
});

it('should handle invalid regex gracefully', () => {
const allowBranches = ['/^(invalid/'];

// Should fall back to exact match
expect(() =>
checkReleasePlan(releaseManifests, '/^(invalid/', allowBranches),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'main', allowBranches),
).toThrow();
});
});

describe('mixed pattern matching', () => {
const releaseManifests: ReleaseManifest[] = [
{
project: mockProject1,
version: '1.0.0',
},
];

it('should support mix of exact, glob, and regex patterns', () => {
const allowBranches = [
'main', // exact match
'chore/*', // glob pattern
'/^release\\/v\\d+\\.\\d+\\.\\d+$/', // regex pattern
];

// Should match exact
expect(() =>
checkReleasePlan(releaseManifests, 'main', allowBranches),
).not.toThrow();

// Should match glob
expect(() =>
checkReleasePlan(releaseManifests, 'chore/fix-bug', allowBranches),
).not.toThrow();

// Should match regex
expect(() =>
checkReleasePlan(releaseManifests, 'release/v1.0.0', allowBranches),
).not.toThrow();

// Should not match
expect(() =>
checkReleasePlan(releaseManifests, 'feature/test', allowBranches),
).toThrow();
});
});

describe('edge cases', () => {
const releaseManifests: ReleaseManifest[] = [
{
project: mockProject1,
version: '1.0.0',
},
];

it('should handle branches with special characters', () => {
const allowBranches = ['feat/*'];

expect(() =>
checkReleasePlan(
releaseManifests,
'feat/user-profile',
allowBranches,
),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'feat/issue-#123', allowBranches),
).not.toThrow();
});

it('should handle branches with multiple slashes', () => {
const allowBranches = ['team/**'];

expect(() =>
checkReleasePlan(
releaseManifests,
'team/frontend/feature',
allowBranches,
),
).not.toThrow();
expect(() =>
checkReleasePlan(
releaseManifests,
'team/backend/fix/urgent',
allowBranches,
),
).not.toThrow();
});

it('should be case-sensitive by default', () => {
const allowBranches = ['Main', 'CHORE/*'];

expect(() =>
checkReleasePlan(releaseManifests, 'Main', allowBranches),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'main', allowBranches),
).toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'CHORE/fix', allowBranches),
).not.toThrow();
expect(() =>
checkReleasePlan(releaseManifests, 'chore/fix', allowBranches),
).toThrow();
});
});
});
});
Loading
Loading