feat(scripts): sync-sched-publish.mjs — полный синк sched→GitHub с pre-push CI-гейтом
Алгоритм (ранбук wiki:3332, один коммит на волну): collect → copy → bleach → scrub (скраб kzntsv в т.ч. \u002F) → lock (yarn.lock против npmjs, gotcha 8) → gate (build 9 пакетов топо-порядком + tsc + vitest прямыми вызовами node_modules, обход Windows yarn-bin-болезни) → один чистый коммит (--amend при фиксе) → push. Фикс бага: pairs были массивом строк, деструктуризация [old,nw] давала односимвольные замены (g→i, r→e — ломало dist); теперь пары [old,nw]. Идемпотентен: повторный прогон = 0 изменений.
This commit is contained in:
334
scripts/sync-sched-publish.mjs
Normal file
334
scripts/sync-sched-publish.mjs
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
// sched → GitHub: полный синк курируемой копии (ранбук wiki:3332, task:1711).
|
||||||
|
//
|
||||||
|
// Алгоритм (один коммит на волну, pre-push CI-эквивалентный гейт):
|
||||||
|
// collect — ls-files dev минус исключения (.wiki/.tasks/.agents/AGENTS/CLAUDE/
|
||||||
|
// branding-pora/pngtree/327274997/cat-clock/MIGRATION)
|
||||||
|
// copy — mkdir -p + cp в pub (untracked dev-мусор не копируется)
|
||||||
|
// bleach — dev-приватное → публичное (yarnrc/Dockerfile/compose/nuxt/
|
||||||
|
// sched-links/publish-check/CHANGELOG/examples)
|
||||||
|
// scrub — dist: rm -rf + cp из dev + скраб git.kzntsv (в т.ч. \u002F и \\u002F)
|
||||||
|
// lock — yarn.lock регенерация против npmjs (gotcha 8): если
|
||||||
|
// `yarn install --immutable` падает YN0028 → `yarn install` → перепроверка
|
||||||
|
// gate — CI-эквивалент ЛОКАЛЬНО до пуша (gotcha 10/11):
|
||||||
|
// tsc --noEmit + vitest (vitest.ci.config.ts) прямыми вызовами
|
||||||
|
// node_modules (обходит Windows yarn-workspace-bin-болезнь)
|
||||||
|
// commit — один чистый коммит (amend при --amend)
|
||||||
|
// push — только если gate зелёный (без --push — стоп перед пушем)
|
||||||
|
//
|
||||||
|
// Windows-заметка: `yarn workspace run` не добавляет root .bin в PATH
|
||||||
|
// (command not found: tsc) — гейт вызывает node node_modules/... напрямую.
|
||||||
|
//
|
||||||
|
// Использование:
|
||||||
|
// node scripts/sync-sched-publish.mjs # полный цикл до пуша (стоп, показать)
|
||||||
|
// node scripts/sync-sched-publish.mjs --push # + push origin main
|
||||||
|
// node scripts/sync-sched-publish.mjs --amend # amend последнего коммита (после фикса dev)
|
||||||
|
// node scripts/sync-sched-publish.mjs --only=lock # только фаза
|
||||||
|
// node scripts/sync-sched-publish.mjs --no-copy # пропустить фазы copy/bleach/scrub
|
||||||
|
//
|
||||||
|
// Возврат: 0 = зелёно, 1 = что-то упало (причины в stdout, мутаций нет после точки отказа).
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
|
|
||||||
|
const HOME = os.homedir().replace(/\\/g, "/");
|
||||||
|
const DEV = process.env.SCHED_DEV || `${HOME}/projects/sched`;
|
||||||
|
const PUB = process.env.SCHED_PUB || `${HOME}/projects/sched-upstream`;
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const PUSH = args.includes("--push");
|
||||||
|
const AMEND = args.includes("--amend");
|
||||||
|
const ONLY = args.includes("--only=") ? args.find((a) => a.startsWith("--only=")).slice(7) : null;
|
||||||
|
const NO_COPY = args.includes("--no-copy");
|
||||||
|
|
||||||
|
// ── helpers ────────────────────────────────────────────────────────────────
|
||||||
|
const sh = (cmd, opts = {}) =>
|
||||||
|
execSync(cmd, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8", ...opts });
|
||||||
|
const shSilent = (cmd) => {
|
||||||
|
try { return sh(cmd).trim(); } catch { return null; }
|
||||||
|
};
|
||||||
|
const log = (s) => console.log(s);
|
||||||
|
const fail = (s) => { console.error(`\n✗ ${s}`); process.exitCode = 1; };
|
||||||
|
|
||||||
|
function copyTree(srcFile, dstFile) {
|
||||||
|
if (!fs.existsSync(srcFile)) return; // удалён в dev → удалится в pub через git add -A
|
||||||
|
fs.mkdirSync(path.dirname(dstFile), { recursive: true });
|
||||||
|
fs.copyFileSync(srcFile, dstFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── фазы ───────────────────────────────────────────────────────────────────
|
||||||
|
function collect() {
|
||||||
|
const out = shSilent(`git -C "${DEV}" ls-files`);
|
||||||
|
if (out === null) { fail("dev не git-репо"); return null; }
|
||||||
|
const EXCL = /^\.wiki\/|^\.tasks\/|^\.agents\/|^AGENTS\.md$|^CLAUDE\.md$|^branding\/pora-|^pngtree-|^327274997_|^cat-clock-|^MIGRATION\.md$/;
|
||||||
|
return out.split("\n").filter((f) => f && !EXCL.test(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
function copy(files) {
|
||||||
|
for (const f of files) copyTree(`${DEV}/${f}`, `${PUB}/${f}`);
|
||||||
|
log(`copy: ${files.length} файлов`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// выбеливание — точечные замены, идемпотентно (эталон: верифицированный b0afd14)
|
||||||
|
function bleach() {
|
||||||
|
const sub = (rel, pairs) => {
|
||||||
|
const p = `${PUB}/${rel}`;
|
||||||
|
if (!fs.existsSync(p)) return;
|
||||||
|
let s = fs.readFileSync(p, "utf8");
|
||||||
|
let dirty = false;
|
||||||
|
for (const [old, nw] of pairs) if (s.includes(old)) { s = s.replaceAll(old, nw); dirty = true; }
|
||||||
|
if (dirty) fs.writeFileSync(p, s);
|
||||||
|
};
|
||||||
|
|
||||||
|
// .yarnrc.yml — канон целиком (3 строки, фиксированный)
|
||||||
|
fs.writeFileSync(`${PUB}/.yarnrc.yml`, `nodeLinker: node-modules\nenableTelemetry: false\nnpmRegistryServer: "https://registry.npmjs.org"\n`);
|
||||||
|
|
||||||
|
sub("docker-compose.dev.yml", [
|
||||||
|
["# npm-based image: pull @schedjs/daemon from the private verdaccio.",
|
||||||
|
"# npm-based image: pull @schedjs/daemon from the public npm registry."],
|
||||||
|
["NPM_REGISTRY: ${NPM_REGISTRY:-https://verdaccio.kzntsv.site}",
|
||||||
|
"NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org}"],
|
||||||
|
["NPM_TOKEN: ${VERDACCIO_TOKEN:-}", "NPM_TOKEN: ${NPM_TOKEN:-}"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
sub("apps/daemon/Dockerfile", [
|
||||||
|
["# Build (public registry):\n# docker build -t schedd:0.10.2 apps/daemon\n# Build (private verdaccio — our internal registry):\n# docker build --secret id=verdaccio_token,env=VERDACCIO_TOKEN \\\n# --build-arg NPM_REGISTRY=https://verdaccio.kzntsv.site \\\n# -t registry.kzntsv.site/sched-daemon:0.10.2 apps/daemon\n#",
|
||||||
|
"# Build:\n# docker build -t schedd:0.12.1 apps/daemon\n#\n# Private-registry mirror (optional, internal use): pass the registry URL as\n# NPM_REGISTRY + a BuildKit secret to install from a mirror instead of npmjs.\n#"],
|
||||||
|
["(verdaccio serves none)", "(a mirror serves none)"],
|
||||||
|
["# VERDACCIO_TOKEN comes from the BuildKit secret (mount type=secret).",
|
||||||
|
"# NPM_TOKEN comes from the BuildKit secret (mount type=secret)."],
|
||||||
|
["id=verdaccio_token", "id=npm_token"],
|
||||||
|
["/run/secrets/verdaccio_token", "/run/secrets/npm_token"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
sub("docs/nuxt.config.ts", [
|
||||||
|
["?? 'https://git.kzntsv.site/victor/sched'", "?? 'https://github.com/schedjs/schedjs'"],
|
||||||
|
["?? 'https://verdaccio.kzntsv.site'", "?? 'https://registry.npmjs.org'"],
|
||||||
|
["?? 'registry.kzntsv.site'", "?? 'ghcr.io/schedjs'"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
sub("docs/modules/sched-links.ts", [
|
||||||
|
["?? 'https://git.kzntsv.site/victor/sched'", "?? 'https://github.com/schedjs/schedjs'"],
|
||||||
|
["?? 'https://verdaccio.kzntsv.site'", "?? 'https://registry.npmjs.org'"],
|
||||||
|
["?? 'registry.kzntsv.site'", "?? 'ghcr.io/schedjs'"],
|
||||||
|
["VERDACCIO_TOKEN", "NPM_TOKEN"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
sub("CHANGELOG.md", [
|
||||||
|
["на verdaccio", "на npm"], ["→ verdaccio", "→ npm"],
|
||||||
|
["registry.kzntsv.site/sched-daemon", "ghcr.io/schedjs/sched-daemon"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const p of ["scripts/publish-check.mjs", "scripts/publish-check.test.mjs"]) {
|
||||||
|
sub(p, [["registry.kzntsv.site/sched-daemon", "ghcr.io/schedjs/sched-daemon"]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const f of ["examples/admin-client-python/generate.ps1", "examples/admin-client-ts/generate.ps1"]) {
|
||||||
|
sub(f, [[" (verdaccio subpath export);", ";"]]);
|
||||||
|
}
|
||||||
|
for (const f of [
|
||||||
|
"examples/admin-client-python/README.md",
|
||||||
|
"examples/admin-client-ts/README.md",
|
||||||
|
"examples/admin-client-python/tests/test_admin_client.py",
|
||||||
|
]) {
|
||||||
|
sub(f, [["verdaccio", "npm"]]);
|
||||||
|
}
|
||||||
|
log("bleach: выбелено");
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrubDist() {
|
||||||
|
const devDist = `${DEV}/docs/dist`;
|
||||||
|
const pubDist = `${PUB}/docs/dist`;
|
||||||
|
if (!fs.existsSync(devDist)) { log("scrub: нет dev docs/dist — пропуск"); return; }
|
||||||
|
fs.rmSync(pubDist, { recursive: true, force: true });
|
||||||
|
fs.cpSync(devDist, pubDist, { recursive: true });
|
||||||
|
|
||||||
|
// скраб остатков kzntsv (прямые + \u002F + \\u002F в _payload.json)
|
||||||
|
const pairs = [
|
||||||
|
["git.kzntsv.site/victor/sched", "github.com/schedjs/schedjs"],
|
||||||
|
["verdaccio.kzntsv.site/:_authToken=${VERDACCIO_TOKEN}", "registry.npmjs.org/:_authToken=${NPM_TOKEN}"],
|
||||||
|
["verdaccio.kzntsv.site", "registry.npmjs.org"],
|
||||||
|
["registry.kzntsv.site/sched-daemon", "ghcr.io/schedjs/sched-daemon"],
|
||||||
|
["registry.kzntsv.site", "ghcr.io/schedjs"],
|
||||||
|
["VERDACCIO_TOKEN", "NPM_TOKEN"],
|
||||||
|
];
|
||||||
|
const esc1 = "\\u002F", esc2 = "\\\\u002F"; // \u002F и \\u002F (JSON-двойной)
|
||||||
|
const escPairs = [
|
||||||
|
["git.kzntsv.site" + esc2 + "victor" + esc2 + "sched", "github.com" + esc2 + "schedjs" + esc2 + "schedjs"],
|
||||||
|
["git.kzntsv.site" + esc1 + "victor" + esc1 + "sched", "github.com" + esc1 + "schedjs" + esc1 + "schedjs"],
|
||||||
|
["registry.kzntsv.site" + esc2 + "sched-daemon", "ghcr.io/schedjs" + esc2 + "sched-daemon"],
|
||||||
|
["registry.kzntsv.site" + esc1 + "sched-daemon", "ghcr.io/schedjs" + esc1 + "sched-daemon"],
|
||||||
|
["verdaccio.kzntsv.site" + esc2 + ":_authToken=${VERDACCIO_TOKEN}", "registry.npmjs.org" + esc2 + ":_authToken=${NPM_TOKEN}"],
|
||||||
|
["verdaccio.kzntsv.site" + esc1 + ":_authToken=${VERDACCIO_TOKEN}", "registry.npmjs.org" + esc1 + ":_authToken=${NPM_TOKEN}"],
|
||||||
|
["verdaccio.kzntsv.site" + esc2, "registry.npmjs.org" + esc2],
|
||||||
|
["verdaccio.kzntsv.site" + esc1, "registry.npmjs.org" + esc1],
|
||||||
|
["registry.kzntsv.site" + esc2, "ghcr.io/schedjs" + esc2],
|
||||||
|
["registry.kzntsv.site" + esc1, "ghcr.io/schedjs" + esc1],
|
||||||
|
];
|
||||||
|
|
||||||
|
const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
||||||
|
const p = `${dir}/${e.name}`;
|
||||||
|
return e.isDirectory() ? walk(p) : [p];
|
||||||
|
});
|
||||||
|
let changed = 0;
|
||||||
|
for (const f of walk(pubDist)) {
|
||||||
|
if (f.endsWith(".svg")) continue;
|
||||||
|
let data;
|
||||||
|
try { data = fs.readFileSync(f, "utf8"); } catch { continue; }
|
||||||
|
let orig = data;
|
||||||
|
for (const [old, nw] of pairs) data = data.split(old).join(nw);
|
||||||
|
for (const [old, nw] of escPairs) data = data.split(old).join(nw);
|
||||||
|
if (data !== orig) { fs.writeFileSync(f, data); changed++; }
|
||||||
|
}
|
||||||
|
log(`scrub: dist скопирован, изменено ${changed} файлов`);
|
||||||
|
|
||||||
|
// logo.svg НЕ коммитить (gotcha 4)
|
||||||
|
if (fs.existsSync(`${pubDist}/logo.svg`)) fs.rmSync(`${pubDist}/logo.svg`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lock() {
|
||||||
|
// сначала immutable; если падает — регенерация и перепроверка
|
||||||
|
const imm = shSilent(`cd "${PUB}" && yarn install --immutable`);
|
||||||
|
if (imm === null || imm.includes("YN0028") || /Failed with errors/.test(imm)) {
|
||||||
|
log("lock: immutable упал — регенерирую против npmjs");
|
||||||
|
const regen = shSilent(`cd "${PUB}" && yarn install`);
|
||||||
|
if (regen === null || /Failed with errors/.test(regen)) { fail("lock: yarn install упал"); return false; }
|
||||||
|
const imm2 = shSilent(`cd "${PUB}" && yarn install --immutable`);
|
||||||
|
if (imm2 === null || /Failed with errors/.test(imm2) || imm2.includes("YN0028")) {
|
||||||
|
fail("lock: immutable после регенерации всё ещё красный"); return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log("lock: yarn.lock валиден против npmjs");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gate() {
|
||||||
|
// CI-эквивалент прямыми вызовами node_modules (Windows: yarn workspace run не видит .bin)
|
||||||
|
// gotcha 5: build до tsc (cross-package types через dist) — собрать dist всех пакетов
|
||||||
|
// топологический порядок (как yarn workspaces -pt): пакет раньше своих @schedjs-потребителей
|
||||||
|
const buildCfgs = [];
|
||||||
|
for (const sub of ["packages", "apps"]) {
|
||||||
|
for (const dir of fs.readdirSync(`${PUB}/${sub}`)) {
|
||||||
|
const cfg = `${PUB}/${sub}/${dir}/tsconfig.build.json`;
|
||||||
|
if (fs.existsSync(cfg)) buildCfgs.push(`${sub}/${dir}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const order = [];
|
||||||
|
const remaining = new Set(buildCfgs);
|
||||||
|
const depsOf = (p) => {
|
||||||
|
const pj = JSON.parse(fs.readFileSync(`${PUB}/${p}/package.json`, "utf8"));
|
||||||
|
const deps = { ...(pj.dependencies || {}), ...(pj.devDependencies || {}), ...(pj.peerDependencies || {}) };
|
||||||
|
return Object.keys(deps).filter((d) => d.startsWith("@schedjs/"));
|
||||||
|
};
|
||||||
|
const localName = (p) => {
|
||||||
|
const pj = JSON.parse(fs.readFileSync(`${PUB}/${p}/package.json`, "utf8"));
|
||||||
|
return pj.name;
|
||||||
|
};
|
||||||
|
const localNames = new Map(buildCfgs.map((p) => [localName(p), p]));
|
||||||
|
while (remaining.size) {
|
||||||
|
let progress = false;
|
||||||
|
for (const p of [...remaining]) {
|
||||||
|
const unmet = depsOf(p).filter((d) => localNames.has(d) && !order.includes(localNames.get(d)));
|
||||||
|
if (unmet.length === 0) { order.push(p); remaining.delete(p); progress = true; }
|
||||||
|
}
|
||||||
|
if (!progress) { fail("gate: цикл зависимостей build-пакетов"); return false; }
|
||||||
|
}
|
||||||
|
for (const pkg of order) {
|
||||||
|
const clean = `${PUB}/${pkg}/dist`;
|
||||||
|
if (fs.existsSync(clean)) fs.rmSync(clean, { recursive: true, force: true });
|
||||||
|
const b = shSilent(`cd "${PUB}/${pkg}" && node ../../node_modules/typescript/bin/tsc -p tsconfig.build.json`);
|
||||||
|
if (b === null || /error TS/.test(b)) {
|
||||||
|
fail(`gate: build ${pkg} упал`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// полный build-скрипт пакета может включать bundle (esbuild) — повторить
|
||||||
|
const pj = JSON.parse(fs.readFileSync(`${PUB}/${pkg}/package.json`, "utf8"));
|
||||||
|
if ((pj.scripts?.build || "").includes("bundle.mjs")) {
|
||||||
|
const bun = shSilent(`cd "${PUB}/${pkg}" && node scripts/bundle.mjs`);
|
||||||
|
if (bun === null) { fail(`gate: bundle ${pkg} упал`); return false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log(`gate: build ${order.length} пакетов (dist, топо-порядок)`);
|
||||||
|
|
||||||
|
const tsc = shSilent(`cd "${PUB}" && node node_modules/typescript/bin/tsc --noEmit`);
|
||||||
|
if (tsc === null || /error TS/.test(tsc)) {
|
||||||
|
const errs = (tsc || "").split("\n").filter((l) => l.includes("error TS")).slice(0, 10);
|
||||||
|
fail(`gate: tsc — ${errs.length} ошибок${errs.length ? "\n" + errs.join("\n") : ""}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log("gate: tsc зелёный");
|
||||||
|
|
||||||
|
const vit = shSilent(`cd "${PUB}" && node node_modules/vitest/vitest.mjs run --config vitest.ci.config.ts`);
|
||||||
|
if (vit === null || /failed/.test(vit)) {
|
||||||
|
fail("gate: vitest упал (смотри вывод)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const passLine = (vit.match(/(\d+) passed/) || [])[1] || "?";
|
||||||
|
log(`gate: vitest зелёный (${passLine} passed)`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function grepCheck() {
|
||||||
|
const hits = shSilent(`cd "${PUB}" && grep -rnE 'git\\.kzntsv|verdaccio|vds\\.kzntsv|registry\\.kzntsv|VERDACCIO' --exclude-dir=node_modules --exclude-dir=.git .`);
|
||||||
|
if (hits && hits.length > 0) {
|
||||||
|
fail(`grep: остались ${hits.split("\n").length} вхождений kzntsv/verdaccio`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log("grep: 0 вхождений kzntsv/verdaccio");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function commit() {
|
||||||
|
const msg = `chore: sync curated copy from dev (${shSilent(`cd "${DEV}" && git log -1 --format=%h`) || "?"})`;
|
||||||
|
sh(`cd "${PUB}" && git add -A`);
|
||||||
|
const staged = shSilent(`cd "${PUB}" && git status -s`);
|
||||||
|
if (!staged || staged.trim() === "") {
|
||||||
|
log("commit: нет изменений — волна уже синхронизирована");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (AMEND) {
|
||||||
|
shSilent(`cd "${PUB}" && git commit --amend --no-edit`);
|
||||||
|
log("commit: amended");
|
||||||
|
} else {
|
||||||
|
sh(`cd "${PUB}" && git commit -m "${msg}"`);
|
||||||
|
log(`commit: ${shSilent(`cd "${PUB}" && git log -1 --format=%s`)}`);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── main ────────────────────────────────────────────────────────────────────
|
||||||
|
if (!fs.existsSync(`${PUB}/.git`)) { fail("pub не git-репо"); process.exit(1); }
|
||||||
|
if (!fs.existsSync(`${DEV}/.git`)) { fail("dev не git-репо"); process.exit(1); }
|
||||||
|
|
||||||
|
const files = collect();
|
||||||
|
if (!files) process.exit(1);
|
||||||
|
|
||||||
|
if (ONLY && ONLY !== "lock") { fail("--only поддерживает: lock"); process.exit(1); }
|
||||||
|
|
||||||
|
let ok = true;
|
||||||
|
if (!ONLY) {
|
||||||
|
if (!NO_COPY) { copy(files); bleach(); scrubDist(); }
|
||||||
|
ok = grepCheck() && ok;
|
||||||
|
}
|
||||||
|
if (ONLY === "lock") {
|
||||||
|
ok = lock() && ok;
|
||||||
|
} else if (!NO_COPY) {
|
||||||
|
ok = lock() && ok;
|
||||||
|
}
|
||||||
|
if (ok && !ONLY && !NO_COPY) {
|
||||||
|
ok = gate() && ok;
|
||||||
|
if (ok) ok = commit() && ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
log("\n✗ цикл прерван — мутаций после точки отказа нет. Фикс в dev → повтор.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!PUSH) {
|
||||||
|
log(`\n✓ готово до пуша: ${shSilent(`cd "${PUB}" && git log --oneline -1`)}`);
|
||||||
|
log(" запусти с --push чтобы отправить origin main");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
const p = shSilent(`cd "${PUB}" && git push origin main`);
|
||||||
|
if (p === null || /rejected/.test(p)) { fail("push отклонён — pull --ff-only и повтори"); process.exit(1); }
|
||||||
|
log("✓ pushed origin main");
|
||||||
Reference in New Issue
Block a user