1- import { existsSync , mkdirSync , renameSync , rmSync } from "node:fs" ;
1+ import { chmodSync , existsSync , mkdirSync , renameSync , rmSync , statSync } from "node:fs" ;
22import { join } from "node:path" ;
33import {
44 CANDIDATE_MANIFEST_FILENAME ,
@@ -355,7 +355,8 @@ async function promoteTwoSlot(
355355 * 3. `docker stop motis`, then rename `data/motis/live` →
356356 * `data/motis/live.previous` and `data/motis/staging` →
357357 * `data/motis/live` (so the rename never happens under a running mount).
358- * Recreate an empty staging dir so the next pipeline run has a clean target.
358+ * Recreate an empty staging dir with the same permissions so the next
359+ * pipeline run has a clean, writable target.
359360 * 4. `docker restart motis` and poll `/api/v1/map/initial` until it responds
360361 * (5-minute budget).
361362 *
@@ -471,6 +472,17 @@ export const run: StageFn = async (ctx) => {
471472 }
472473
473474 // Second rename: staging → current.
475+ // Preserve the staging root's access mode across the rename. The next
476+ // import writes MOTIS's compiled data into a newly-created staging dir;
477+ // mkdir's default mode would otherwise apply the process umask and can
478+ // silently turn a deliberately group/world-writable bind dir into 0755.
479+ let stagingMode = 0o755 ;
480+ try {
481+ stagingMode = statSync ( stagingDir ) . mode & 0o777 ;
482+ } catch {
483+ // The rename below will provide the authoritative failure if the path
484+ // disappeared between the pre-flight check and the swap.
485+ }
474486 try {
475487 renameSync ( stagingDir , currentDir ) ;
476488 } catch ( error ) {
@@ -497,7 +509,10 @@ export const run: StageFn = async (ctx) => {
497509
498510 // Recreate an empty staging dir for the next cycle.
499511 try {
500- mkdirSync ( stagingDir , { recursive : true } ) ;
512+ mkdirSync ( stagingDir , { recursive : true , mode : stagingMode } ) ;
513+ // mkdir applies the process umask, so explicitly restore the exact mode
514+ // captured above rather than relying on the requested mode alone.
515+ chmodSync ( stagingDir , stagingMode ) ;
501516 } catch {
502517 // Non-fatal: the next pipeline run will recreate it.
503518 }
0 commit comments