From 408dea0c2db9aed31671502837119bbcfb0a1fb9 Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Sat, 29 Aug 2026 02:30:19 +0200 Subject: [PATCH 1/3] fix(ci): normalize shell validation input (fixes #3) --- .gitattributes | 3 +++ src/shared/shell-verification.cjs | 6 +++++- tests/shell-verification.test.mjs | 27 ++++++++++----------------- 3 files changed, 18 insertions(+), 18 deletions(-) create mode 100644 .gitattributes 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); -- 2.54.0 From 0b8deed1e3a4c8d5265cf364fd13a7efd8e6513f Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Sat, 29 Aug 2026 03:35:32 +0200 Subject: [PATCH 2/3] fix(ci): use one required pull-request gate --- .gitea/workflows/quality.yml | 86 ++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/.gitea/workflows/quality.yml b/.gitea/workflows/quality.yml index 4280a96..606d211 100644 --- a/.gitea/workflows/quality.yml +++ b/.gitea/workflows/quality.yml @@ -1,46 +1,46 @@ -name: ForgeFlow quality gate - +name: ForgeFlow quality gate + on: push: branches: [main] - pull_request: - -jobs: - secret-scan: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Secret scan - shell: bash - run: | - set -euo pipefail - scan_container="$(docker create ghcr.io/trufflesecurity/trufflehog:3.79.0 filesystem /scan --only-verified --fail --no-update)" - trap 'docker rm -f "${scan_container}" >/dev/null 2>&1 || true' EXIT - tar --exclude=.git --transform='s#^\.$#scan#;s#^\./#scan/#' -cf - . | docker cp - "${scan_container}:/" - docker start -a "${scan_container}" - - quality: - # Browser quality runs against the dedicated bounded Windows 11 VM runner. - runs-on: windows-native - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - run: npm ci - # The native runner deliberately skips Electron's install-time binary - # download. Prime it once before Node's parallel test workers require - # Electron, otherwise they can race while creating the same directory. - - run: npx electron --version - - run: npm run quality - - run: npx playwright install chromium - - run: npm run test:browser:ci - - name: Preserve browser failure evidence - if: failure() - uses: actions/upload-artifact@v3.2.2-node20 - with: - name: forgeflow-browser-failure-evidence - path: artifacts/ - if-no-files-found: ignore - - run: npm audit --omit=dev --audit-level=high + workflow_dispatch: + +jobs: + secret-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Secret scan + shell: bash + run: | + set -euo pipefail + scan_container="$(docker create ghcr.io/trufflesecurity/trufflehog:3.79.0 filesystem /scan --only-verified --fail --no-update)" + trap 'docker rm -f "${scan_container}" >/dev/null 2>&1 || true' EXIT + tar --exclude=.git --transform='s#^\.$#scan#;s#^\./#scan/#' -cf - . | docker cp - "${scan_container}:/" + docker start -a "${scan_container}" + + quality: + # Browser quality runs against the dedicated bounded Windows 11 VM runner. + runs-on: windows-native + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + # The native runner deliberately skips Electron's install-time binary + # download. Prime it once before Node's parallel test workers require + # Electron, otherwise they can race while creating the same directory. + - run: npx electron --version + - run: npm run quality + - run: npx playwright install chromium + - run: npm run test:browser:ci + - name: Preserve browser failure evidence + if: failure() + uses: actions/upload-artifact@v3.2.2-node20 + with: + name: forgeflow-browser-failure-evidence + path: artifacts/ + if-no-files-found: ignore + - run: npm audit --omit=dev --audit-level=high -- 2.54.0 From 4b4718d2314d583d138f1ad9e7f3f467e1abe90b Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Sat, 29 Aug 2026 03:36:34 +0200 Subject: [PATCH 3/3] fix(ci): preserve workflow line endings --- .gitea/workflows/quality.yml | 84 ++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/.gitea/workflows/quality.yml b/.gitea/workflows/quality.yml index 606d211..709729d 100644 --- a/.gitea/workflows/quality.yml +++ b/.gitea/workflows/quality.yml @@ -1,46 +1,46 @@ -name: ForgeFlow quality gate - +name: ForgeFlow quality gate + on: push: branches: [main] workflow_dispatch: - -jobs: - secret-scan: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Secret scan - shell: bash - run: | - set -euo pipefail - scan_container="$(docker create ghcr.io/trufflesecurity/trufflehog:3.79.0 filesystem /scan --only-verified --fail --no-update)" - trap 'docker rm -f "${scan_container}" >/dev/null 2>&1 || true' EXIT - tar --exclude=.git --transform='s#^\.$#scan#;s#^\./#scan/#' -cf - . | docker cp - "${scan_container}:/" - docker start -a "${scan_container}" - - quality: - # Browser quality runs against the dedicated bounded Windows 11 VM runner. - runs-on: windows-native - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - run: npm ci - # The native runner deliberately skips Electron's install-time binary - # download. Prime it once before Node's parallel test workers require - # Electron, otherwise they can race while creating the same directory. - - run: npx electron --version - - run: npm run quality - - run: npx playwright install chromium - - run: npm run test:browser:ci - - name: Preserve browser failure evidence - if: failure() - uses: actions/upload-artifact@v3.2.2-node20 - with: - name: forgeflow-browser-failure-evidence - path: artifacts/ - if-no-files-found: ignore - - run: npm audit --omit=dev --audit-level=high + +jobs: + secret-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Secret scan + shell: bash + run: | + set -euo pipefail + scan_container="$(docker create ghcr.io/trufflesecurity/trufflehog:3.79.0 filesystem /scan --only-verified --fail --no-update)" + trap 'docker rm -f "${scan_container}" >/dev/null 2>&1 || true' EXIT + tar --exclude=.git --transform='s#^\.$#scan#;s#^\./#scan/#' -cf - . | docker cp - "${scan_container}:/" + docker start -a "${scan_container}" + + quality: + # Browser quality runs against the dedicated bounded Windows 11 VM runner. + runs-on: windows-native + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + # The native runner deliberately skips Electron's install-time binary + # download. Prime it once before Node's parallel test workers require + # Electron, otherwise they can race while creating the same directory. + - run: npx electron --version + - run: npm run quality + - run: npx playwright install chromium + - run: npm run test:browser:ci + - name: Preserve browser failure evidence + if: failure() + uses: actions/upload-artifact@v3.2.2-node20 + with: + name: forgeflow-browser-failure-evidence + path: artifacts/ + if-no-files-found: ignore + - run: npm audit --omit=dev --audit-level=high -- 2.54.0