diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1a89dc5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.sh text eol=lf +examples/server/forgeflow-deploy text eol=lf +scripts/* text eol=lf diff --git a/src/shared/shell-verification.cjs b/src/shared/shell-verification.cjs index b9e8b0c..a5b1f14 100644 --- a/src/shared/shell-verification.cjs +++ b/src/shared/shell-verification.cjs @@ -1,3 +1,4 @@ +const fs = require('node:fs'); const path = require('node:path'); function normalizeRelativePosixPath(value) { @@ -19,11 +20,14 @@ function bashSyntaxCheckInvocation(root, scriptPath = 'examples/server/forgeflow if (typeof root !== 'string' || !root.trim()) { throw new Error('Project root is required for shell validation.'); } + const relativeScriptPath = normalizeRelativePosixPath(scriptPath); + const scriptText = fs.readFileSync(path.join(root, ...relativeScriptPath.split('/')), 'utf8'); return { command: 'bash', - args: ['-n', normalizeRelativePosixPath(scriptPath)], + args: ['-n'], options: { cwd: root, + input: scriptText.replace(/\r\n?/g, '\n'), encoding: 'utf8', windowsHide: true } diff --git a/tests/shell-verification.test.mjs b/tests/shell-verification.test.mjs index e858740..60a37b2 100644 --- a/tests/shell-verification.test.mjs +++ b/tests/shell-verification.test.mjs @@ -8,15 +8,6 @@ import shellVerification from '../src/shared/shell-verification.cjs'; const { bashSyntaxCheckInvocation, bashSyntaxCheckFromTextInvocation, normalizeRelativePosixPath, validateShellScriptStructure, shouldRunExternalBash } = shellVerification; -test('Bash syntax validation keeps Windows project roots in cwd and passes a relative POSIX path', () => { - const invocation = bashSyntaxCheckInvocation('C:\\Projects\\ForgeFlow'); - assert.equal(invocation.command, 'bash'); - assert.deepEqual(invocation.args, ['-n', 'examples/server/forgeflow-deploy']); - assert.equal(invocation.options.cwd, 'C:\\Projects\\ForgeFlow'); - assert.equal(invocation.args[1].includes('\\'), false); - assert.equal(/^[A-Za-z]:/.test(invocation.args[1]), false); -}); - test('Shell validation refuses absolute and escaping script paths', () => { assert.throws(() => normalizeRelativePosixPath('C:\\Projects\\ForgeFlow\\script.sh'), /must be relative/); assert.throws(() => normalizeRelativePosixPath('/tmp/script.sh'), /must be relative/); @@ -34,11 +25,19 @@ test('Bash syntax validation works from a project root containing spaces', async await mkdir(relativeDirectory, { recursive: true }); await copyFile(new URL('../examples/server/forgeflow-deploy', import.meta.url), path.join(relativeDirectory, 'forgeflow-deploy')); const invocation = bashSyntaxCheckInvocation(tempBase); + assert.equal(invocation.options.cwd, tempBase); + assert.deepEqual(invocation.args, ['-n']); + assert.equal(invocation.options.input.includes('\r'), false); const result = spawnSync(invocation.command, invocation.args, invocation.options); assert.equal(result.status, 0, result.stderr); } finally { try { - await rm(tempBase, { recursive: true, force: true, maxRetries: 20, retryDelay: 100 }); + await rm(tempBase, { + recursive: true, + force: true, + maxRetries: 20, + retryDelay: 100 + }); } catch (error) { // Git Bash on Windows can retain a short-lived working-directory handle // after bash -n exits. Do not fail a successful syntax test solely because @@ -48,7 +47,6 @@ test('Bash syntax validation works from a project root containing spaces', async } }); - test('Bash syntax validation from text does not depend on a Windows working directory', () => { const invocation = bashSyntaxCheckFromTextInvocation('#!/usr/bin/env bash\nset -euo pipefail\necho ok\n'); assert.equal(invocation.command, 'bash'); @@ -67,7 +65,6 @@ test('Bash syntax validation from text detects malformed scripts', (t) => { assert.notEqual(result.status, 0); }); - test('portable server-script validation does not require a local Bash executable', () => { const script = `#!/usr/bin/env bash set -Eeuo pipefail @@ -89,13 +86,9 @@ write_status "unhealthy" }); test('portable server-script validation refuses missing deployment safety markers', () => { - assert.throws( - () => validateShellScriptStructure('#!/usr/bin/env bash\nset -Eeuo pipefail\necho unsafe\n'), - /missing required safety marker/ - ); + assert.throws(() => validateShellScriptStructure('#!/usr/bin/env bash\nset -Eeuo pipefail\necho unsafe\n'), /missing required safety marker/); }); - test('Windows publication never depends on an external Bash shim', () => { assert.equal(shouldRunExternalBash('win32'), false); assert.equal(shouldRunExternalBash('linux'), true);