diff --git a/fedac/native/scripts/upload-release.sh b/fedac/native/scripts/upload-release.sh index d8a120f75f..3fd882e6ba 100755 --- a/fedac/native/scripts/upload-release.sh +++ b/fedac/native/scripts/upload-release.sh @@ -59,9 +59,25 @@ DO_SPACES_REGION="${DO_SPACES_REGION:-sfo3}" BASE_URL="https://${DO_SPACES_BUCKET}.${DO_SPACES_REGION}.digitaloceanspaces.com" -# Build version string from git (append -dirty if uncommitted changes) +# Build version string from git. Dirty/conflicted uploads are blocked by default. GIT_HASH=$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown") +CONFLICT_FILES=$(git -C "$SCRIPT_DIR" diff --name-only --diff-filter=U 2>/dev/null || true) +if [ -n "$CONFLICT_FILES" ]; then + echo "Error: refusing upload with unresolved merge conflicts:" >&2 + echo "$CONFLICT_FILES" >&2 + exit 1 +fi +DIRTY_TRACKED=0 if ! git -C "$SCRIPT_DIR" diff --quiet HEAD 2>/dev/null; then + DIRTY_TRACKED=1 +fi +if [ "$DIRTY_TRACKED" -eq 1 ] && [ "${ALLOW_DIRTY_UPLOAD:-0}" != "1" ]; then + echo "Error: refusing dirty upload. Commit/stash/reset native changes first." >&2 + git -C "$SCRIPT_DIR" status --porcelain --untracked-files=no -- fedac/native 2>/dev/null >&2 || true + echo "Override only for emergencies: ALLOW_DIRTY_UPLOAD=1 ./scripts/upload-release.sh ..." >&2 + exit 1 +fi +if [ "$DIRTY_TRACKED" -eq 1 ]; then GIT_HASH="${GIT_HASH}-dirty" fi BUILD_TS=$(date -u '+%Y-%m-%dT%H:%M') diff --git a/oven/native-builder.mjs b/oven/native-builder.mjs index 4f1319c38f..7f97083eef 100644 --- a/oven/native-builder.mjs +++ b/oven/native-builder.mjs @@ -287,6 +287,66 @@ async function runBuildJob(job) { const repoDir = path.resolve(NATIVE_DIR, "../.."); + // Preflight: hard-sync build repo and refuse conflicted/dirty native trees. + addLogLine(job, "stdout", "Preflight: syncing native git checkout..."); + await runPhase(job, "preflight-sync", "bash", ["-lc", [ + "set -euo pipefail", + "git fetch origin main --quiet || true", + "git checkout -f main --quiet || true", + "if git rev-parse --verify origin/main >/dev/null 2>&1; then", + " git reset --hard origin/main --quiet", + "fi", + "git clean -fdq -- fedac/native", + ].join("\n")], repoDir); + + const syncedRef = await runSync("git", ["rev-parse", "HEAD"], repoDir); + if (syncedRef) job.ref = syncedRef; + + const trackedDirty = await runSync( + "git", + ["status", "--porcelain", "--untracked-files=no", "--", "fedac/native"], + repoDir, + ); + if (trackedDirty) { + throw new Error( + `Refusing native build: fedac/native tree is dirty after sync:\n${trackedDirty}`, + ); + } + + const unresolved = await runSync( + "git", + ["diff", "--name-only", "--diff-filter=U", "--", "fedac/native"], + repoDir, + ); + if (unresolved) { + throw new Error( + `Refusing native build: unresolved merge conflict(s): ${unresolved}`, + ); + } + + const conflictMarkers = await runSync( + "bash", + [ + "-lc", + "grep -nE '^(<<<<<<<|=======|>>>>>>>)|Updated upstream|Stashed changes' fedac/native/initramfs/init 2>/dev/null || true", + ], + repoDir, + ); + if (conflictMarkers) { + throw new Error( + `Refusing native build: conflict markers detected in initramfs/init:\n${conflictMarkers}`, + ); + } + + // Parse-check init script explicitly so syntax issues fail before kernel compile/upload. + await runPhase( + job, + "preflight-init", + "bash", + ["-lc", "set -euo pipefail\nsh -n fedac/native/initramfs/init"], + repoDir, + ); + // Resolve ref from git HEAD if manual trigger didn't provide one if (!job.ref || job.ref === "unknown") { const headRef = await runSync("git", ["rev-parse", "HEAD"], repoDir); diff --git a/oven/native-git-poller.mjs b/oven/native-git-poller.mjs index 333b1dd2dc..1e88175bba 100644 --- a/oven/native-git-poller.mjs +++ b/oven/native-git-poller.mjs @@ -95,9 +95,10 @@ async function poll() { return; } - // Pull the changes so build-and-flash.sh works on up-to-date code - await git(["checkout", BRANCH, "--quiet"]); - await git(["merge", `origin/${BRANCH}`, "--ff-only", "--quiet"]); + // Hard-sync to origin so stale local edits/conflicts cannot leak into OTA builds. + await git(["checkout", "-f", BRANCH, "--quiet"]); + await git(["reset", "--hard", `origin/${BRANCH}`, "--quiet"]); + await git(["clean", "-fdq"]); logFn( "info",