[core] make release
This commit is contained in:
1 parent
84f111e2c1
commit
e290fd545f
2 files changed
+229
No files matched your search
@@ -61,6 +61,17 @@ gows:
|
||||
(export PATH=${HOME}/go/bin:${PATH} || echo failed) && \
|
||||
make all
|
||||
|
||||
ORIGIN ?= waha-plus
|
||||
CORE_REMOTE ?= waha
|
||||
|
||||
release:
|
||||
node scripts/release.js
|
||||
|
||||
release-push: release
|
||||
git push $(ORIGIN) core plus
|
||||
git push --force-with-lease $(ORIGIN) dev
|
||||
git push $(CORE_REMOTE) core
|
||||
|
||||
up-dashboard:
|
||||
node scripts/up-dashboard.js
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { execFileSync } = require('child_process');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const readline = require('readline');
|
||||
|
||||
const DEV_BRANCH = process.env.WAHA_DEV_BRANCH || 'dev';
|
||||
const CORE_BRANCH = process.env.WAHA_CORE_BRANCH || 'core';
|
||||
const PLUS_BRANCH = process.env.WAHA_PLUS_BRANCH || 'plus';
|
||||
|
||||
const CORE_PREFIX = '[core]';
|
||||
const PLUS_PREFIX = '[PLUS]';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const assumeYes = args.includes('--yes') || args.includes('-y');
|
||||
|
||||
function git(gitArgs, options) {
|
||||
const opts = options || {};
|
||||
return execFileSync('git', gitArgs, {
|
||||
encoding: 'utf8',
|
||||
stdio: opts.inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
}
|
||||
|
||||
function gitOut(gitArgs) {
|
||||
return git(gitArgs, { inherit: false }).trim();
|
||||
}
|
||||
|
||||
function log(message) {
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(`\n✗ ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function ensureCleanTree() {
|
||||
const status = gitOut(['status', '--porcelain']);
|
||||
if (status) {
|
||||
fail(
|
||||
'Working tree is not clean. Commit or stash your changes before releasing.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureBranchExists(branch) {
|
||||
try {
|
||||
git(['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`]);
|
||||
} catch {
|
||||
fail(`Branch "${branch}" does not exist locally.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Non-merge commits in `boundary..head`, oldest first, whose subject starts
|
||||
// with prefix. The boundary is the last-released plus tip: dev is rebased onto
|
||||
// plus every release, so `plus..dev` is exactly the new, unreleased work.
|
||||
// We intentionally do not use `git cherry` (patch-id matching) because core is
|
||||
// a rewritten history whose old commits share no patch-ids with dev.
|
||||
function commitsToCherryPick(boundary, head, prefix) {
|
||||
const format = '%H%x09%s';
|
||||
const output = gitOut([
|
||||
'rev-list',
|
||||
'--reverse',
|
||||
'--no-merges',
|
||||
`--format=${format}`,
|
||||
`${boundary}..${head}`,
|
||||
]);
|
||||
if (!output) {
|
||||
return [];
|
||||
}
|
||||
const picks = [];
|
||||
for (const line of output.split('\n')) {
|
||||
// rev-list --format prefixes each entry with a "commit <sha>" header line.
|
||||
if (!line || line.startsWith('commit ')) {
|
||||
continue;
|
||||
}
|
||||
const tab = line.indexOf('\t');
|
||||
const sha = line.slice(0, tab);
|
||||
const subject = line.slice(tab + 1);
|
||||
if (subject.startsWith(prefix)) {
|
||||
picks.push({ sha: sha, subject: subject });
|
||||
}
|
||||
}
|
||||
return picks;
|
||||
}
|
||||
|
||||
function cherryPickAll(picks) {
|
||||
for (const pick of picks) {
|
||||
log(` cherry-pick ${pick.sha.slice(0, 9)} ${pick.subject}`);
|
||||
if (dryRun) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
git(['cherry-pick', pick.sha], { inherit: true });
|
||||
} catch {
|
||||
git(['cherry-pick', '--abort'], { inherit: true });
|
||||
fail(
|
||||
`Cherry-pick of ${pick.sha.slice(0, 9)} failed (conflict). ` +
|
||||
`Aborted the cherry-pick — resolve manually and re-run.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkout(branch) {
|
||||
log(`\n→ checkout ${branch}`);
|
||||
if (!dryRun) {
|
||||
git(['checkout', branch], { inherit: true });
|
||||
}
|
||||
}
|
||||
|
||||
function confirm(question) {
|
||||
if (assumeYes || dryRun) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
return new Promise(function resolver(resolve) {
|
||||
rl.question(`${question} [y/N] `, function onAnswer(answer) {
|
||||
rl.close();
|
||||
resolve(/^y(es)?$/i.test(answer.trim()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
ensureCleanTree();
|
||||
[DEV_BRANCH, CORE_BRANCH, PLUS_BRANCH].forEach(ensureBranchExists);
|
||||
|
||||
const startBranch = gitOut(['rev-parse', '--abbrev-ref', 'HEAD']);
|
||||
|
||||
if (dryRun) {
|
||||
log('Running in --dry-run mode: no branches will be modified.\n');
|
||||
}
|
||||
|
||||
// Capture the last-released plus tip before we touch anything. Step 3 merges
|
||||
// core into plus and moves the branch, so both pick sets must be computed
|
||||
// against this saved boundary, not the live plus ref.
|
||||
const boundary = gitOut(['rev-parse', PLUS_BRANCH]);
|
||||
log(`Boundary (last released ${PLUS_BRANCH}): ${boundary.slice(0, 9)}\n`);
|
||||
|
||||
const corePicks = commitsToCherryPick(boundary, DEV_BRANCH, CORE_PREFIX);
|
||||
log(
|
||||
`Found ${corePicks.length} "${CORE_PREFIX}" commit(s) in ` +
|
||||
`${DEV_BRANCH} since last ${PLUS_BRANCH} release.`,
|
||||
);
|
||||
|
||||
// Step 1 + 2: bring missing [core] commits onto core.
|
||||
checkout(CORE_BRANCH);
|
||||
cherryPickAll(corePicks);
|
||||
|
||||
// Step 3: merge the freshly updated core into plus.
|
||||
checkout(PLUS_BRANCH);
|
||||
log(`\n→ merge ${CORE_BRANCH} into ${PLUS_BRANCH}`);
|
||||
if (!dryRun) {
|
||||
try {
|
||||
git(['merge', '--no-edit', CORE_BRANCH], { inherit: true });
|
||||
} catch {
|
||||
git(['merge', '--abort'], { inherit: true });
|
||||
fail(
|
||||
`Merge of ${CORE_BRANCH} into ${PLUS_BRANCH} failed (conflict). ` +
|
||||
`Aborted the merge — resolve manually and re-run.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: bring missing [PLUS] commits onto plus (same saved boundary).
|
||||
const plusPicks = commitsToCherryPick(boundary, DEV_BRANCH, PLUS_PREFIX);
|
||||
log(
|
||||
`\nFound ${plusPicks.length} "${PLUS_PREFIX}" commit(s) in ` +
|
||||
`${DEV_BRANCH} since last ${PLUS_BRANCH} release.`,
|
||||
);
|
||||
cherryPickAll(plusPicks);
|
||||
|
||||
// Step 5: rebase dev onto the freshly built plus.
|
||||
log(`\n→ rebase ${DEV_BRANCH} onto ${PLUS_BRANCH} (rewrites ${DEV_BRANCH})`);
|
||||
const proceed = await confirm(
|
||||
`This will force-rewrite "${DEV_BRANCH}". Continue?`,
|
||||
);
|
||||
if (!proceed) {
|
||||
fail('Aborted before rebasing dev. core/plus changes are kept.');
|
||||
}
|
||||
checkout(DEV_BRANCH);
|
||||
if (!dryRun) {
|
||||
try {
|
||||
git(['rebase', PLUS_BRANCH], { inherit: true });
|
||||
} catch {
|
||||
git(['rebase', '--abort'], { inherit: true });
|
||||
fail(
|
||||
`Rebase of ${DEV_BRANCH} onto ${PLUS_BRANCH} failed (conflict). ` +
|
||||
`Aborted the rebase — resolve manually and re-run.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
checkout(startBranch);
|
||||
log('\nDry run complete. Re-run without --dry-run to apply.');
|
||||
} else {
|
||||
log(
|
||||
`\n✓ Release complete. ${DEV_BRANCH} now sits on top of ${PLUS_BRANCH}.`,
|
||||
);
|
||||
log(
|
||||
` Push when ready: git push origin ${CORE_BRANCH} ${PLUS_BRANCH} ` +
|
||||
`&& git push --force-with-lease origin ${DEV_BRANCH}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(function onError(error) {
|
||||
fail(error.message || String(error));
|
||||
});
|
||||
Reference in new issue
Block a user