mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
Sync main branch to canary branch (#12267)
* 🔧 chore(release): bump version to v2.1.27 [skip ci] * chore: update sync main to canary workflow * 🐛 fix: update @lobehub/ui version and refactor dynamic import handling (#12260) * ✨ feat: add hotfix workflow and script for automated hotfix management Signed-off-by: Innei <tukon479@gmail.com> * 🔧 fix: refactor PR creation command to use execFileSync for improved reliability Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore: update @lobehub/ui version and refactor dynamic import handling - Bump @lobehub/ui dependency from ^4.35.0 to ^4.36.2 in package.json. - Refactor settingsContentToStatic.mts to simplify dynamic import processing by removing business feature checks. - Add initialize.ts to enable immer's map set functionality. - Correct import path in layout.tsx from 'initiallize' to 'initialize'. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore: update @types/react version in package.json - Bump @types/react dependency from ^19.2.9 to 19.2.14. - Add @types/react version to overrides section for consistency. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore: enhance auto-tag-release workflow for strict semver validation - Updated regex to match strict semantic versioning format, allowing for optional prerelease and build metadata. - Added validation step to ensure the version is a valid semver before proceeding with the release process. Signed-off-by: Innei <tukon479@gmail.com> * 🗑️ chore: remove defaultSecurityBlacklist test file - Deleted the test file for DEFAULT_SECURITY_BLACKLIST as it is no longer needed. - This cleanup helps maintain a more streamlined test suite. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore: update localization files for multiple languages - Improved translations in Arabic, Bulgarian, German, English, and Spanish for chat and tool-related strings. - Enhanced descriptions for various parameters and added new keys for file handling and security warnings. - Adjusted phrasing for clarity and consistency across languages. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore: update PR comment script to include Actions Artifacts link - Modified the PR comment generation script to accept an additional artifactsUrl parameter. - Updated the comment format to include both Release download and Actions Artifacts links for better accessibility. Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com> * 🐛 chore(hotfix): bump version to v2.1.28 [skip ci] * chore: update secrets token --------- Signed-off-by: Innei <tukon479@gmail.com> Co-authored-by: rdmclin2 <rdmclin2@gmail.com> Co-authored-by: Arvin Xu <arvinx@foxmail.com> Co-authored-by: Innei <i@innei.in>
This commit is contained in:
@@ -12,13 +12,6 @@ interface DynamicImportInfo {
|
||||
start: number;
|
||||
}
|
||||
|
||||
const isBusinessFeaturesEnabled = () => {
|
||||
const raw = process.env.ENABLE_BUSINESS_FEATURES;
|
||||
if (!raw) return false;
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
return normalized === 'true' || normalized === '1';
|
||||
};
|
||||
|
||||
const extractDynamicImportsFromMap = (code: string): DynamicImportInfo[] => {
|
||||
const results: DynamicImportInfo[] = [];
|
||||
|
||||
@@ -44,20 +37,12 @@ const extractDynamicImportsFromMap = (code: string): DynamicImportInfo[] => {
|
||||
return results;
|
||||
};
|
||||
|
||||
const generateStaticImports = (imports: DynamicImportInfo[], keepBusinessTabs: boolean): string => {
|
||||
return imports
|
||||
.filter((imp) => keepBusinessTabs || !imp.importPath.includes('@/business/'))
|
||||
.map((imp) => `import ${imp.componentName} from '${imp.importPath}';`)
|
||||
.join('\n');
|
||||
const generateStaticImports = (imports: DynamicImportInfo[]): string => {
|
||||
return imports.map((imp) => `import ${imp.componentName} from '${imp.importPath}';`).join('\n');
|
||||
};
|
||||
|
||||
const generateStaticComponentMap = (
|
||||
imports: DynamicImportInfo[],
|
||||
keepBusinessTabs: boolean,
|
||||
): string => {
|
||||
const entries = imports
|
||||
.filter((imp) => keepBusinessTabs || !imp.importPath.includes('@/business/'))
|
||||
.map((imp) => ` [SettingsTabs.${imp.key}]: ${imp.componentName},`);
|
||||
const generateStaticComponentMap = (imports: DynamicImportInfo[]): string => {
|
||||
const entries = imports.map((imp) => ` [SettingsTabs.${imp.key}]: ${imp.componentName},`);
|
||||
|
||||
return `const componentMap: Record<string, React.ComponentType<{ mobile?: boolean }>> = {\n${entries.join('\n')}\n}`;
|
||||
};
|
||||
@@ -79,13 +64,6 @@ export const convertSettingsContentToStatic = async (TEMP_DIR: string) => {
|
||||
filePath,
|
||||
name: 'convertSettingsContentToStatic',
|
||||
transformer: (code) => {
|
||||
const keepBusinessTabs = isBusinessFeaturesEnabled();
|
||||
if (keepBusinessTabs) {
|
||||
console.log(
|
||||
' ENABLE_BUSINESS_FEATURES is enabled, preserving business Settings tabs in componentMap',
|
||||
);
|
||||
}
|
||||
|
||||
const imports = extractDynamicImportsFromMap(code);
|
||||
|
||||
invariant(
|
||||
@@ -95,8 +73,8 @@ export const convertSettingsContentToStatic = async (TEMP_DIR: string) => {
|
||||
|
||||
console.log(` Found ${imports.length} dynamic imports in componentMap`);
|
||||
|
||||
const staticImports = generateStaticImports(imports, keepBusinessTabs);
|
||||
const staticComponentMap = generateStaticComponentMap(imports, keepBusinessTabs);
|
||||
const staticImports = generateStaticImports(imports);
|
||||
const staticComponentMap = generateStaticComponentMap(imports);
|
||||
|
||||
let result = code;
|
||||
|
||||
|
||||
266
scripts/hotfixWorkflow/index.ts
Normal file
266
scripts/hotfixWorkflow/index.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { execFileSync, execSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { confirm, input } from '@inquirer/prompts';
|
||||
import { consola } from 'consola';
|
||||
import * as semver from 'semver';
|
||||
|
||||
const ROOT_DIR = process.cwd();
|
||||
const PACKAGE_JSON_PATH = path.join(ROOT_DIR, 'package.json');
|
||||
|
||||
function checkGitRepo(): void {
|
||||
try {
|
||||
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
|
||||
} catch {
|
||||
consola.error('❌ Current directory is not a Git repository');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentBranch(): string {
|
||||
try {
|
||||
return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
||||
} catch {
|
||||
consola.error('❌ Unable to determine current branch');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentVersion(): string {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
|
||||
return pkg.version;
|
||||
} catch {
|
||||
consola.error('❌ Unable to read version from package.json');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function bumpPatchVersion(currentVersion: string): string {
|
||||
const parsed = semver.parse(currentVersion);
|
||||
if (!parsed) {
|
||||
consola.error(`❌ Invalid semver version in package.json: ${currentVersion}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// If current is a pre-release, hotfix should still be a stable patch (e.g. 2.0.0-beta.1 -> 2.0.1)
|
||||
const base = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
||||
const next = semver.inc(base, 'patch');
|
||||
if (!next) {
|
||||
consola.error(`❌ Unable to calculate patch version from: ${base}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function createHotfixBranchName(version: string): string {
|
||||
try {
|
||||
const hash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
|
||||
if (!hash) {
|
||||
consola.error('❌ Unable to determine current commit hash for branch suffix');
|
||||
process.exit(1);
|
||||
}
|
||||
return `hotfix/v${version}-${hash}`;
|
||||
} catch (error) {
|
||||
consola.error('❌ Failed to generate hotfix branch name');
|
||||
consola.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmHotfix(opts: {
|
||||
branchName: string;
|
||||
currentVersion: string;
|
||||
isExistingBranch: boolean;
|
||||
version: string;
|
||||
}): Promise<boolean> {
|
||||
const { version, currentVersion, branchName, isExistingBranch } = opts;
|
||||
|
||||
if (isExistingBranch) {
|
||||
consola.box(
|
||||
`
|
||||
🩹 Hotfix PR (existing branch)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Branch: ${branchName}
|
||||
Version: ${version}
|
||||
Target: main
|
||||
━━━━━━━━━━━━━━━━━━━━━━━
|
||||
`.trim(),
|
||||
);
|
||||
|
||||
return await confirm({
|
||||
default: true,
|
||||
message: 'Confirm to push and submit PR for this hotfix branch?',
|
||||
});
|
||||
}
|
||||
|
||||
consola.box(
|
||||
`
|
||||
🩹 Hotfix Confirmation
|
||||
━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Current: ${currentVersion}
|
||||
New: ${version}
|
||||
Branch: ${branchName}
|
||||
Target: main
|
||||
━━━━━━━━━━━━━━━━━━━━━━━
|
||||
`.trim(),
|
||||
);
|
||||
|
||||
return await confirm({
|
||||
default: true,
|
||||
message: 'Confirm to create hotfix branch and submit PR?',
|
||||
});
|
||||
}
|
||||
|
||||
function createHotfixBranch(branchName: string): void {
|
||||
try {
|
||||
consola.info(`🌿 Creating branch: ${branchName}...`);
|
||||
execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
|
||||
consola.success(`✅ Created and switched to branch: ${branchName}`);
|
||||
} catch (error) {
|
||||
consola.error(`❌ Failed to create branch: ${branchName}`);
|
||||
consola.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function pushBranch(branchName: string): void {
|
||||
try {
|
||||
consola.info('📤 Pushing branch to remote...');
|
||||
execSync(`git push -u origin ${branchName}`, { stdio: 'inherit' });
|
||||
consola.success(`✅ Pushed branch to remote: ${branchName}`);
|
||||
} catch (error) {
|
||||
consola.error('❌ Failed to push branch');
|
||||
consola.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function createPullRequest(version: string, branchName: string): Promise<void> {
|
||||
const title = await input({
|
||||
default: `🐛 fix: hotfix v${version}`,
|
||||
message: 'PR title:',
|
||||
});
|
||||
const body = `## 🩹 Hotfix v${version}
|
||||
|
||||
This PR starts a hotfix release from \`main\`.
|
||||
|
||||
### Release Process
|
||||
1. ✅ Hotfix branch created from main
|
||||
2. ✅ Pushed to remote
|
||||
3. 🔄 Waiting for PR review and merge
|
||||
4. ⏳ Auto tag + GitHub Release will be created after merge
|
||||
|
||||
---
|
||||
Created by hotfix script`;
|
||||
|
||||
try {
|
||||
consola.info('🔀 Creating Pull Request...');
|
||||
execFileSync(
|
||||
'gh',
|
||||
['pr', 'create', '--title', title, '--body', body, '--base', 'main', '--head', branchName],
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
consola.success('✅ PR created successfully!');
|
||||
} catch (error) {
|
||||
consola.error('❌ Failed to create PR');
|
||||
consola.error(error instanceof Error ? error.message : String(error));
|
||||
consola.info('\n💡 Tip: Make sure GitHub CLI (gh) is installed and logged in');
|
||||
consola.info(' Install: https://cli.github.com/');
|
||||
consola.info(' Login: gh auth login');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function showCompletion(version: string, branchName: string): void {
|
||||
consola.box(
|
||||
`
|
||||
🎉 Hotfix process started!
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
✅ Branch created: ${branchName}
|
||||
✅ Pushed to remote
|
||||
✅ PR created targeting main branch
|
||||
|
||||
📋 PR Title: 🐛 hotfix: v${version}
|
||||
|
||||
Next steps:
|
||||
1. Open the PR link to view details
|
||||
2. Complete code review
|
||||
3. Merge PR to main branch
|
||||
4. Wait for release workflows to complete
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
`.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
function extractVersionFromBranch(branchName: string): string | null {
|
||||
const match = branchName.match(/^hotfix\/v(.+?)(?:-[a-f0-9]+)?$/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
consola.info('🩹 LobeChat Hotfix Script\n');
|
||||
|
||||
checkGitRepo();
|
||||
|
||||
const currentBranch = getCurrentBranch();
|
||||
const isOnMain = currentBranch === 'main';
|
||||
const isOnHotfix = currentBranch.startsWith('hotfix/');
|
||||
|
||||
if (!isOnMain && !isOnHotfix) {
|
||||
consola.error(`❌ Current branch "${currentBranch}" is neither main nor a hotfix branch`);
|
||||
consola.info('💡 Please switch to main or an existing hotfix branch first');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (isOnHotfix) {
|
||||
consola.info(`🔍 Detected existing hotfix branch: ${currentBranch}`);
|
||||
|
||||
const currentVersion = getCurrentVersion();
|
||||
const version = extractVersionFromBranch(currentBranch) ?? bumpPatchVersion(currentVersion);
|
||||
|
||||
const confirmed = await confirmHotfix({
|
||||
branchName: currentBranch,
|
||||
currentVersion,
|
||||
isExistingBranch: true,
|
||||
version,
|
||||
});
|
||||
if (!confirmed) {
|
||||
consola.info('❌ Hotfix process cancelled');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
pushBranch(currentBranch);
|
||||
await createPullRequest(version, currentBranch);
|
||||
showCompletion(version, currentBranch);
|
||||
} else {
|
||||
consola.info('📥 Pulling latest main branch...');
|
||||
execSync('git pull --rebase origin main', { stdio: 'inherit' });
|
||||
|
||||
const currentVersion = getCurrentVersion();
|
||||
const newVersion = bumpPatchVersion(currentVersion);
|
||||
const branchName = createHotfixBranchName(newVersion);
|
||||
|
||||
const confirmed = await confirmHotfix({
|
||||
branchName,
|
||||
currentVersion,
|
||||
isExistingBranch: false,
|
||||
version: newVersion,
|
||||
});
|
||||
if (!confirmed) {
|
||||
consola.info('❌ Hotfix process cancelled');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
createHotfixBranch(branchName);
|
||||
pushBranch(branchName);
|
||||
await createPullRequest(newVersion, branchName);
|
||||
showCompletion(newVersion, branchName);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
consola.error('❌ Error occurred:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import path from 'node:path';
|
||||
|
||||
import { confirm, select } from '@inquirer/prompts';
|
||||
import { consola } from 'consola';
|
||||
@@ -110,7 +110,7 @@ Target: main
|
||||
function checkoutAndPullDev(): void {
|
||||
try {
|
||||
// Check for dev branch
|
||||
const branches = execSync('git branch -a', { encoding: 'utf-8' });
|
||||
const branches = execSync('git branch -a', { encoding: 'utf8' });
|
||||
const hasLocalDev = branches.includes(' dev\n') || branches.startsWith('* dev\n');
|
||||
const hasRemoteDev = branches.includes('remotes/origin/dev');
|
||||
|
||||
@@ -144,20 +144,14 @@ function checkoutAndPullDev(): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Create release branch with version marker commit
|
||||
function createReleaseBranch(version: string, versionType: VersionType): void {
|
||||
// Create release branch
|
||||
function createReleaseBranch(version: string): void {
|
||||
const branchName = `release/v${version}`;
|
||||
|
||||
try {
|
||||
consola.info(`🌿 Creating branch: ${branchName}...`);
|
||||
execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
|
||||
consola.success(`✅ Created and switched to branch: ${branchName}`);
|
||||
|
||||
// Create empty commit to mark the release
|
||||
const markerMessage = getReleaseMarkerMessage(versionType, version);
|
||||
consola.info(`📝 Creating version marker commit...`);
|
||||
execSync(`git commit --allow-empty -m "${markerMessage}"`, { stdio: 'inherit' });
|
||||
consola.success(`✅ Created version marker commit: ${markerMessage}`);
|
||||
} catch (error) {
|
||||
consola.error(`❌ Failed to create branch or commit: ${branchName}`);
|
||||
consola.error(error instanceof Error ? error.message : String(error));
|
||||
@@ -165,19 +159,6 @@ function createReleaseBranch(version: string, versionType: VersionType): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Get release marker commit message
|
||||
function getReleaseMarkerMessage(versionType: VersionType, version: string): string {
|
||||
// Use gitmoji format for commit message
|
||||
const gitmojiMap = {
|
||||
major: '🚀',
|
||||
minor: '✨',
|
||||
patch: '🔧',
|
||||
};
|
||||
|
||||
const emoji = gitmojiMap[versionType];
|
||||
return `${emoji} chore(release): prepare release v${version}`;
|
||||
}
|
||||
|
||||
// Push branch to remote
|
||||
function pushBranch(version: string): void {
|
||||
const branchName = `release/v${version}`;
|
||||
@@ -289,8 +270,8 @@ async function main(): Promise<void> {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 6. Create release branch (with version marker commit)
|
||||
createReleaseBranch(newVersion, versionType);
|
||||
// 6. Create release branch
|
||||
createReleaseBranch(newVersion);
|
||||
|
||||
// 7. Push to remote
|
||||
pushBranch(newVersion);
|
||||
|
||||
Reference in New Issue
Block a user