fixed sonarqube findings
This commit is contained in:
@@ -226,3 +226,16 @@ Wegwichtel verwendet keine Werbe- oder Tracking-Cookies und bindet keine Analyse
|
||||
Der Initialisierungsbildschirm bleibt nach erfolgreichem Laden sichtbar. Erst eine bewusste Pointer- oder Tastaturaktion öffnet die eigentliche Anwendung. Das Informationsfenster und die Datenschutzeinstellungen können bereits auf dem Startbildschirm verwendet werden, ohne die Anwendung weiterzuschalten.
|
||||
|
||||
Gemeinsame Entwürfe und Wichtelcodes: [Gruppenbearbeitung.md](Gruppenbearbeitung.md).
|
||||
|
||||
## Deployment-Freigabe
|
||||
|
||||
Automatische Deployments benötigen ein erfolgreiches SonarQube Quality Gate.
|
||||
Der Analysejob erzeugt danach eine Freigabe für den geprüften Commit, den
|
||||
Workflow-Lauf und dessen aktuellen Versuch. Ohne passende Freigabe darf das
|
||||
Deployment nicht fortfahren. Das SSH-Skript übernimmt ausschließlich diesen
|
||||
Commit und prüft ihn vor dem Neustart des App-Dienstes. So kann ein inzwischen
|
||||
weiter fortgeschrittener Branch nicht unbemerkt als ungeprüfter Stand starten.
|
||||
|
||||
Der ausdrücklich manuell ausgewählte Workflow-Aufruf `job=deploy` bleibt als
|
||||
bestehender Override verfügbar. Er benötigt eine bewusste Auswahl und ersetzt
|
||||
keine neue Analyse. Ein gewöhnlicher Push kann diesen Override nicht verwenden.
|
||||
|
||||
@@ -178,3 +178,22 @@ Rechte fremder Urheber und bestehende Lizenzen bleiben davon unberührt.
|
||||
|
||||
Vor Änderungen an Gruppenrechten und Code-Eingabe zusätzlich
|
||||
`docs/agents/GRUPPENBEARBEITUNG.md` lesen und bei Übergaben ausdrücklich nennen.
|
||||
|
||||
## Deployment-Freigabe nach dem Quality Gate
|
||||
|
||||
Am 18. Oktober 2026 ist Lauf 1828 für Commit 0e4cdfb trotz fehlgeschlagenem
|
||||
Quality Gate bis zum erfolgreichen Deployment gelaufen. Die vorhandene
|
||||
jobweite `if`-Bedingung allein hat das Deployment nicht verhindert.
|
||||
|
||||
- Automatische Deployments benötigen ein erst nach erfolgreichem Gate erzeugtes
|
||||
Freigabeartefakt für genau Commit, Workflow-Lauf und Laufversuch.
|
||||
- Im Deployment-Schritt muss die Freigabe geprüft werden; im SSH-Skript müssen
|
||||
eine fehlende Freigabe und ein abweichender Commit vor Änderungen abgewiesen
|
||||
werden. Nur den freigegebenen Commit übernehmen und vor dem Neustart prüfen.
|
||||
- Der bestehende ausdrückliche manuelle Aufruf `job=deploy` bleibt ein bewusster
|
||||
Override. Ein Push darf diese Ausnahme nicht aktivieren.
|
||||
- Verhaltenstests in `test/deployment-workflow.test.js` ausführen. Sie führen die
|
||||
tatsächlichen Shell-Blöcke mit isolierten Dateien und simulierten externen
|
||||
Befehlen aus; keine echten SSH-Verbindungen oder Neustarts im Test.
|
||||
- Die neue Freigabekette muss nach dem nächsten Check-in im echten Workflow
|
||||
bestätigt werden. Lokale Shell-Tests ersetzen keine erfolgreiche Pipeline.
|
||||
|
||||
@@ -615,7 +615,7 @@
|
||||
.group-code img { width: 100%; height: 64px; object-fit: contain; }
|
||||
.group-actions { max-width: min(18rem, 52vw); flex-wrap: wrap; justify-content: flex-end; }
|
||||
.group-actions .editor-secondary-button { font-size: .85rem; }
|
||||
.group-status { min-height: 1.5rem; color: #9b2c2c; }
|
||||
.group-status { display: block; min-height: 1.5rem; color: #9b2c2c; }
|
||||
@media (prefers-reduced-motion: reduce) { .group-wheel-viewport { scroll-behavior: auto; } }
|
||||
|
||||
.point-compare-dialog { width: min(56rem, calc(100vw - 2rem)); }
|
||||
|
||||
+1
-1
@@ -607,7 +607,7 @@
|
||||
<p id="group-expiry"></p>
|
||||
<button id="group-edit" type="button" data-i18n="groups.edit">Entwurf bearbeiten</button>
|
||||
</section>
|
||||
<p id="group-status" class="group-status" role="status"></p>
|
||||
<output id="group-status" class="group-status"></output>
|
||||
</dialog>
|
||||
<dialog id="point-compare-dialog" class="group-dialog point-compare-dialog" aria-labelledby="point-compare-title">
|
||||
<h2 id="point-compare-title" data-i18n="point-locks.compare-title">Die Station wurde geändert</h2>
|
||||
|
||||
+18
-18
@@ -214,7 +214,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
}
|
||||
|
||||
function showListView() {
|
||||
void ns.GroupPoints?.release();
|
||||
ns.GroupPoints?.release();
|
||||
releasePoiPreview();
|
||||
ns.RouteRecorder?.endEditing();
|
||||
setDialogTitle('Streckenverwaltung');
|
||||
@@ -224,7 +224,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
}
|
||||
|
||||
function showRouteView() {
|
||||
void ns.GroupPoints?.release();
|
||||
ns.GroupPoints?.release();
|
||||
releasePoiPreview();
|
||||
setDialogTitle(routeDialogTitle());
|
||||
setHidden(byId(ids.routeListView), true);
|
||||
@@ -478,8 +478,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
renderRouteList();
|
||||
setStatus('');
|
||||
// Local drafts are usable even while this request is waiting for a connection.
|
||||
void refreshPublicRoutes(requestGeneration);
|
||||
void refreshSubmittedRoutes(requestGeneration);
|
||||
refreshPublicRoutes(requestGeneration);
|
||||
refreshSubmittedRoutes(requestGeneration);
|
||||
}
|
||||
|
||||
async function refreshSubmittedRoutes(requestGeneration) {
|
||||
@@ -536,7 +536,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
setDialogExpanded(true);
|
||||
showListView();
|
||||
await guarded(loadRoutes);
|
||||
void ns.Groups?.refresh?.();
|
||||
ns.Groups?.refresh?.();
|
||||
}
|
||||
|
||||
async function openNewLocalRoute() {
|
||||
@@ -546,7 +546,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
|
||||
function closeEditor() {
|
||||
if (pointSaveInProgress) return;
|
||||
void ns.GroupPoints?.release();
|
||||
ns.GroupPoints?.release();
|
||||
releasePoiPreview();
|
||||
ns.RouteRecorder?.endEditing();
|
||||
const dialog = byId(ids.dialog);
|
||||
@@ -948,8 +948,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
async function saveRoute() {
|
||||
const values = routeFromForm();
|
||||
if (!values.name) throw new Error(ns.I18n.t("editor.error.route-name"));
|
||||
if (state.currentSource !== 'local') await saveServerRoute(values);
|
||||
else await saveLocalRoute(values);
|
||||
if (state.currentSource === 'local') await saveLocalRoute(values);
|
||||
else await saveServerRoute(values);
|
||||
}
|
||||
|
||||
function safeGpxFilename(name) {
|
||||
@@ -1157,7 +1157,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
}
|
||||
|
||||
function newPoi() {
|
||||
void ns.GroupPoints?.release();
|
||||
ns.GroupPoints?.release();
|
||||
releasePoiPreview();
|
||||
const position = currentPosition();
|
||||
const lat = position?.lat ?? null;
|
||||
@@ -1195,9 +1195,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
if (draft) {
|
||||
state.pendingAudioFile = draft.media?.audio || null;
|
||||
state.pendingImages = (draft.media?.images || []).map(file => ({ file, key: `pending-image-${ns.LocalRoutes.createPoiId()}`, previewUrl: objectUrlFor(file) }));
|
||||
const hiddenImages = state.groupDeletions.map(item => item.path);
|
||||
state.currentPoi = { ...poi, images: (poi.images || []).filter(image => !hiddenImages.includes(`pictures/${image.id}`)),
|
||||
...(hiddenImages.includes('audio') ? { audioUrl: null } : {}) };
|
||||
const hiddenImages = new Set(state.groupDeletions.map(item => item.path));
|
||||
state.currentPoi = { ...poi, images: (poi.images || []).filter(image => !hiddenImages.has(`pictures/${image.id}`)),
|
||||
...(hiddenImages.has('audio') ? { audioUrl: null } : {}) };
|
||||
renderPoiMediaPreview();
|
||||
updatePoiMediaStatus();
|
||||
setStatus(ns.I18n.t(state.groupDeletePoint ? 'point-locks.delete-restored' : 'point-locks.restored'));
|
||||
@@ -1325,7 +1325,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
await uploadOne(`${basePath}/audio`, 'audio', state.pendingAudioFile, method);
|
||||
if (state.currentSource === 'group') { state.pendingAudioFile = null; await rememberGroupPoint(poiFromForm(), currentPoiId); }
|
||||
}
|
||||
for (const pending of [...state.pendingImages]) {
|
||||
for (const pending of state.pendingImages) {
|
||||
await uploadOne(`${basePath}/pictures`, 'picture', pending.file);
|
||||
if (state.currentSource === 'group') {
|
||||
state.pendingImages = state.pendingImages.filter(item => item !== pending);
|
||||
@@ -1366,7 +1366,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
await ns.GroupPoints.remove(currentRouteId, 'new');
|
||||
await ns.GroupPoints.acceptNew(currentRouteId, saved);
|
||||
}
|
||||
for (const deletion of [...state.groupDeletions]) {
|
||||
for (const deletion of state.groupDeletions) {
|
||||
await sendDelete(`/routes/${currentRouteId}/pois/${savedPoiId}/${deletion.path}`);
|
||||
state.groupDeletions = state.groupDeletions.filter(item => item !== deletion);
|
||||
await rememberGroupPoint(values, savedPoiId);
|
||||
@@ -1725,14 +1725,14 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
byId(ids.dialog)?.addEventListener(eventName, event => {
|
||||
if (pointSaveInProgress && eventName === 'cancel') { event.preventDefault(); return; }
|
||||
releasePoiPreview();
|
||||
void ns.GroupPoints?.release();
|
||||
ns.GroupPoints?.release();
|
||||
ns.RouteRecorder?.endEditing();
|
||||
});
|
||||
}
|
||||
activate(byId(ids.button), () => guarded(openEditor));
|
||||
activate(byId(ids.close), closeEditor);
|
||||
activate(byId(ids.newRoute), newRoute);
|
||||
activate(byId(ids.routeBack), () => { showListView(); void guarded(loadRoutes); });
|
||||
activate(byId(ids.routeBack), () => { showListView(); guarded(loadRoutes); });
|
||||
activate(byId(ids.routeUpload), () => guarded(uploadLocalRoute));
|
||||
activate(byId(ids.rightsOpen), () => ns.LegalUi?.open('terms'));
|
||||
activate(byId(ids.addPoi), newPoi);
|
||||
@@ -1742,8 +1742,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
||||
byId(ids.poiImages)?.addEventListener('change', () => guarded(handleImageSelection));
|
||||
activate(byId(ids.poiMediaPreview), event => guarded(() => mediaPreviewAction(event)));
|
||||
activate(byId(ids.poiDelete), () => {
|
||||
if (state.currentSource === 'group') { state.groupDeletePoint = true; void guarded(savePoi); }
|
||||
else void guarded(deleteCurrentPoi);
|
||||
if (state.currentSource === 'group') { state.groupDeletePoint = true; guarded(savePoi); }
|
||||
else guarded(deleteCurrentPoi);
|
||||
});
|
||||
activate(byId(ids.poiCancel), showRouteView);
|
||||
activate(byId(ids.routeList), event => guarded(() => routeListAction(event)));
|
||||
|
||||
@@ -178,7 +178,7 @@ globalThis.Wegwichtel ??= {};
|
||||
}
|
||||
} finally { touching = false; }
|
||||
}
|
||||
globalThis.setInterval?.(() => { void keepActive(); }, 30_000);
|
||||
globalThis.setInterval?.(() => { keepActive(); }, 30_000);
|
||||
function headersFor(path) {
|
||||
const match = /^\/routes\/(\d+)\/pois\/(\d+)(?:\/|$)/.exec(path);
|
||||
if (!match || !current || String(current.routeId) !== match[1] || String(current.poiId) !== match[2]) return {};
|
||||
|
||||
+23
-21
@@ -22,12 +22,12 @@ globalThis.Wegwichtel ??= {};
|
||||
button.addEventListener('pointerup', event => {
|
||||
if (event.isPrimary === false || (event.button && event.button !== 0)) return;
|
||||
event.preventDefault();
|
||||
void action();
|
||||
action();
|
||||
});
|
||||
button.addEventListener('keydown', event => {
|
||||
if (!['Enter', ' '].includes(event.key)) return;
|
||||
event.preventDefault();
|
||||
void action();
|
||||
action();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ globalThis.Wegwichtel ??= {};
|
||||
|
||||
function openEntry() {
|
||||
dialogGeneration += 1;
|
||||
selection = Array(codeLength).fill(0);
|
||||
selection = new Array(codeLength).fill(0);
|
||||
element(ids.status).textContent = '';
|
||||
element(ids.entry).hidden = false;
|
||||
element(ids.invitation).hidden = true;
|
||||
@@ -297,34 +297,36 @@ globalThis.Wegwichtel ??= {};
|
||||
if (routeId && revision !== null && /^\d+$/.test(revision)) revisions.set(routeId, Number(revision));
|
||||
}
|
||||
|
||||
async function joinSelectedGroup() {
|
||||
const generation = dialogGeneration;
|
||||
const motifs = ns.GroupSymbols.list();
|
||||
let payload;
|
||||
try {
|
||||
payload = await request('/join', 'POST', {
|
||||
code: selection.map(index => motifs[index].unicode), creator_id: ns.LocalRoutes.creatorId()
|
||||
});
|
||||
} catch (error) {
|
||||
if (!element(ids.dialog).open || generation !== dialogGeneration) return;
|
||||
throw error;
|
||||
}
|
||||
if (!element(ids.dialog).open || generation !== dialogGeneration) return;
|
||||
await edit(payload.group);
|
||||
await refresh();
|
||||
}
|
||||
|
||||
function init() {
|
||||
globalThis.setInterval?.(() => { void refreshInvitation(); }, 30_000);
|
||||
globalThis.setInterval?.(() => { refreshInvitation(); }, 30_000);
|
||||
activate(element('group-share-start'), () => finishShare(true));
|
||||
activate(element('group-share-close'), () => finishShare(false));
|
||||
element('group-share-dialog').addEventListener('close', () => {
|
||||
const resolve = shareDecision; shareDecision = null; resolve?.(false);
|
||||
});
|
||||
document.addEventListener('wegwichtel:point-drafts-changed', () => { void renderPending(); void ns.StorageUsage?.refresh(); });
|
||||
document.addEventListener('wegwichtel:point-drafts-changed', () => { renderPending(); ns.StorageUsage?.refresh(); });
|
||||
element(ids.dialog).addEventListener('cancel', () => { dialogGeneration += 1; });
|
||||
element(ids.dialog).addEventListener('close', () => { dialogGeneration += 1; });
|
||||
for (const id of [ids.joinOpen, ids.editorJoin]) activate(element(id), openEntry);
|
||||
activate(element(ids.close), () => element(ids.dialog).close());
|
||||
activate(element(ids.join), () => guarded(async () => {
|
||||
const generation = dialogGeneration;
|
||||
const motifs = ns.GroupSymbols.list();
|
||||
let payload;
|
||||
try {
|
||||
payload = await request('/join', 'POST', {
|
||||
code: selection.map(index => motifs[index].unicode), creator_id: ns.LocalRoutes.creatorId()
|
||||
});
|
||||
} catch (error) {
|
||||
if (!element(ids.dialog).open || generation !== dialogGeneration) return;
|
||||
throw error;
|
||||
}
|
||||
if (!element(ids.dialog).open || generation !== dialogGeneration) return;
|
||||
await edit(payload.group);
|
||||
await refresh();
|
||||
}));
|
||||
activate(element(ids.join), () => guarded(joinSelectedGroup));
|
||||
activate(element(ids.edit), () => guarded(() => edit(currentGroup)));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { isLoopbackAddress } from './route-write-access.js';
|
||||
import { guardPoint, clearPointLocks, hasPointLocks } from './point-locks.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
@@ -163,7 +164,7 @@ export function listGroups(db, req) {
|
||||
}
|
||||
|
||||
function failureKey(req) {
|
||||
const localProxy = ['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(req.socket?.remoteAddress);
|
||||
const localProxy = isLoopbackAddress(req.socket?.remoteAddress);
|
||||
return hash(String(localProxy && req.headers['x-real-ip'] || req.socket?.remoteAddress || 'unknown'));
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,10 @@ function read(routeId, poiId) {
|
||||
}
|
||||
return lock;
|
||||
}
|
||||
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
||||
catch (error) {
|
||||
if (error.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function write(routeId, poiId, lock) {
|
||||
const target = filename(routeId, poiId);
|
||||
@@ -86,7 +89,10 @@ export function hasPointLocks(routeId) {
|
||||
const directory = path.join(routeDirectory(routeId), 'locks');
|
||||
let entries;
|
||||
try { entries = fs.readdirSync(directory, { withFileTypes: true }); }
|
||||
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
||||
catch (error) {
|
||||
if (error.code === 'ENOENT') return false;
|
||||
throw error;
|
||||
}
|
||||
let locked = false;
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && /^\d+\.json$/.test(entry.name) && read(routeId, Number(entry.name.slice(0, -5)))) locked = true;
|
||||
|
||||
@@ -13,7 +13,7 @@ function hasForwardingHeader(req, name) {
|
||||
return value != null && String(value).trim().length > 0;
|
||||
}
|
||||
|
||||
function isLoopbackAddress(address) {
|
||||
export function isLoopbackAddress(address) {
|
||||
if (typeof address !== 'string' || address.length === 0) return false;
|
||||
if (address === '::1') return true;
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import test from 'node:test';
|
||||
|
||||
const workflow = fs.readFileSync(new URL('../.gitea/workflows/build-and-deploy.yaml', import.meta.url), 'utf8');
|
||||
const commit = 'a'.repeat(40);
|
||||
function script(name, attribute = 'run') {
|
||||
const lines = workflow.split('\n');
|
||||
const start = lines.findIndex(line => line.trim() === `- name: ${name}`);
|
||||
assert.notEqual(start, -1);
|
||||
const offset = lines.findIndex((line, index) => index > start && line.trim() === `${attribute}: |-`);
|
||||
assert.notEqual(offset, -1);
|
||||
const indent = lines[offset].search(/\S/) + 2;
|
||||
const result = [];
|
||||
for (const line of lines.slice(offset + 1)) {
|
||||
if (line.trim() && line.search(/\S/) < indent) break;
|
||||
result.push(line.slice(indent));
|
||||
}
|
||||
return result.join('\n');
|
||||
}
|
||||
function fixture(t) {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'wegwichtel-deploy-'));
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
const output = path.join(directory, 'output');
|
||||
const env = { ...process.env, EXPECTED_COMMIT: commit, WORKFLOW_RUN: '1828', WORKFLOW_ATTEMPT: '1',
|
||||
RUN_EVENT: 'push', SELECTED_JOB: '', GITHUB_OUTPUT: output };
|
||||
function run(source, extra = {}) { return spawnSync('sh', ['-s'], { input: source, cwd: directory, env: { ...env, ...extra }, encoding: 'utf8' }); }
|
||||
function proof(value = `${commit}:1828:1`) {
|
||||
fs.mkdirSync(path.join(directory, 'deployment-approval'), { recursive: true });
|
||||
fs.writeFileSync(path.join(directory, 'deployment-approval', 'quality-gate-passed.txt'), value);
|
||||
}
|
||||
return { directory, output, env, run, proof };
|
||||
}
|
||||
|
||||
test('a deployment approval is written only after the Quality Gate step succeeded', t => {
|
||||
const h = fixture(t), source = script('Write deployment approval');
|
||||
for (const outcome of ['', 'failure', 'skipped', 'cancelled']) {
|
||||
assert.notEqual(h.run(source, { GATE_OUTCOME: outcome }).status, 0);
|
||||
assert.equal(fs.existsSync(path.join(h.directory, 'quality-gate-passed.txt')), false);
|
||||
}
|
||||
assert.equal(h.run(source, { GATE_OUTCOME: 'success' }).status, 0);
|
||||
assert.equal(fs.readFileSync(path.join(h.directory, 'quality-gate-passed.txt'), 'utf8'), `${commit}:1828:1\n`);
|
||||
});
|
||||
|
||||
test('push deployments reject missing, mismatching and previous-attempt approval artifacts', t => {
|
||||
const h = fixture(t), source = script('Verify deployment approval');
|
||||
assert.notEqual(h.run(source).status, 0);
|
||||
for (const proof of ['wrong', `${'b'.repeat(40)}:1828:1`, `${commit}:1827:1`, `${commit}:1828:0`]) {
|
||||
h.proof(proof); assert.notEqual(h.run(source).status, 0);
|
||||
assert.equal(fs.existsSync(h.output), false);
|
||||
}
|
||||
h.proof(); assert.equal(h.run(source).status, 0);
|
||||
assert.equal(fs.readFileSync(h.output, 'utf8'), `commit=${commit}\n`);
|
||||
});
|
||||
|
||||
test('only an explicit manual deploy can use the existing manual override', t => {
|
||||
const h = fixture(t), source = script('Verify deployment approval');
|
||||
assert.notEqual(h.run(source, { RUN_EVENT: 'push', SELECTED_JOB: 'deploy' }).status, 0);
|
||||
assert.notEqual(h.run(source, { RUN_EVENT: 'workflow_dispatch', SELECTED_JOB: 'all' }).status, 0);
|
||||
assert.equal(h.run(source, { RUN_EVENT: 'workflow_dispatch', SELECTED_JOB: 'deploy' }).status, 0);
|
||||
assert.equal(fs.readFileSync(h.output, 'utf8'), `commit=${commit}\n`);
|
||||
});
|
||||
|
||||
test('the remote script refuses missing approval before git access and restarts only the approved commit', t => {
|
||||
const h = fixture(t), source = script('Deploy Wegwichtel', 'script');
|
||||
const log = path.join(h.directory, 'calls');
|
||||
const stubs = String.raw`
|
||||
cd() { return 0; }
|
||||
git() {
|
||||
printf 'git %s\n' "$*" >> "$TASK_LOG"
|
||||
if [ "$1" = merge ] && [ "$MERGE_FAILS" = yes ]; then return 1; fi
|
||||
if [ "$1" = rev-parse ]; then printf '%s\n' "$ACTUAL_COMMIT"; fi
|
||||
}
|
||||
systemctl() { printf 'systemctl %s\n' "$*" >> "$TASK_LOG"; }
|
||||
`;
|
||||
const invoke = (approval, actual = commit, mergeFails = 'no') => h.run(stubs + source, {
|
||||
DEPLOY_APPROVED_COMMIT: approval, ACTUAL_COMMIT: actual, TASK_LOG: log, MERGE_FAILS: mergeFails
|
||||
});
|
||||
assert.notEqual(invoke('').status, 0); assert.equal(fs.existsSync(log), false);
|
||||
assert.notEqual(invoke('b'.repeat(40)).status, 0); assert.equal(fs.existsSync(log), false);
|
||||
assert.notEqual(invoke(commit, 'b'.repeat(40)).status, 0);
|
||||
assert.doesNotMatch(fs.readFileSync(log, 'utf8'), /systemctl/);
|
||||
fs.rmSync(log);
|
||||
assert.notEqual(invoke(commit, commit, 'yes').status, 0);
|
||||
assert.doesNotMatch(fs.readFileSync(log, 'utf8'), /systemctl/);
|
||||
fs.rmSync(log);
|
||||
assert.equal(invoke(commit).status, 0);
|
||||
const calls = fs.readFileSync(log, 'utf8');
|
||||
assert.match(calls, new RegExp(`git merge --ff-only ${commit}`));
|
||||
assert.match(calls, /systemctl --user restart wegwichtel.service/);
|
||||
});
|
||||
Reference in New Issue
Block a user