Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,63 @@ jobs:
node scripts/check-upstream-port-parity.mjs --self-test
node scripts/check-upstream-port-parity.mjs

# ── The bash 3.2 floor on this repository's own shell (objectui#7692) ──
# macOS ships bash 3.2.57 and no bash 4+, for licensing reasons. CI runs
# bash 5, where every bash-4-only construct works — so a `mapfile`,
# a `declare -A` or an unguarded `$EPOCHSECONDS` in a hand-run script is
# invisible to a normal green run: the defect AND its repair both read as
# green, and the only reader who ever sees the failure is a contributor on
# a Mac at the moment they most need the script to work.
#
# This repository held its shell to nothing. Measured on `origin/main`
# `28cfff4` with a control beside the zero: 0 tracked files named
# `bash32`/`bash-32`, against 47 `scripts/check-*.mjs` gates that do exist.
# `check-control-bytes` judges bytes in every text file and
# `check-shell-escape-residue` judges markdown FENCES ("206 file(s) and
# 1309 fenced block(s)"), so neither is this.
#
# ⭐ The scan roots are `scripts/**`, `.claude/hooks/**` and `e2e/**`, and
# the third one is the point rather than a detail: two of the four shell
# files this repository wrote itself live in `e2e/live/ci/`. Upstream's
# roots (`.githooks/**` instead of `e2e/**`) would have walked past half
# the population while printing a green line.
#
# This is a RATCHET, not a repair: the gate's first full-population run on
# this tree is green — 12 shell files, 19 constructs, 0 findings. The card
# that filed it had grepped 10 of the 19 by hand; this step is what makes
# the other 9 a measurement instead of an assumption, and what stops the
# first violation being typed.
#
# Ported from objectstack `scripts/check-bash32-floor.mjs` at `6136293`
# and deliberately NOT registered in `scripts/upstream-port-pin.json` —
# that ledger carries ONE global `upstream.ref` (`bf10deb` today) which
# `--resync` rewrites for every entry, so registering this file would mean
# either shipping `bf10deb`'s older, weaker construct table or re-syncing
# three unrelated ported files. The gate's own header carries the full
# argument and names the follow-up.
#
# Runs before install, next to the three gates above and for the same
# reason: node builtins, `scripts/invoked-as.mjs` and `git ls-files` only,
# so nothing it needs is in `node_modules` and an install failure cannot
# make it green. Invoked as `node` rather than through the
# `pnpm check:bash32-floor` alias for that placement, matching the steps
# above; `scripts/__tests__/bash32-floor-wiring.test.ts` holds the two
# spellings to the same script. `--self-test` runs FIRST and is the half
# that stops the scanner rotting into decoration — a pattern that matches
# nothing reports a clean tree forever, so every row is driven against a
# real instance of its construct AND against the 3.2 replacement the
# failure text tells you to write.
#
# Cost, measured on this tree so the job ceiling question is answered
# rather than assumed: both invocations together run in well under two
# seconds. `timeout-minutes: 25` at the job header is DERIVED from this
# job's own distribution (objectui#7270, max 7m14s) and is untouched.
- name: Verify this repository's shell holds the bash 3.2 floor
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-bash32-floor.mjs --self-test
node scripts/check-bash32-floor.mjs

# ── Cache bookkeeping cannot void a recorded verdict (objectui#7048) ──
# THE ORDERING, carried here from the type-check split in `ci.yml`
# (objectui#6577, PR #7047) because it is what a future reader needs in
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"check:docs-route-closure": "node scripts/check-docs-route-eager-closure.mjs",
"check:entry-guard": "node scripts/check-entry-guard.mjs",
"check:upstream-port-parity": "node scripts/check-upstream-port-parity.mjs",
"check:bash32-floor": "node scripts/check-bash32-floor.mjs",
"check:governed-queue-guard": "node scripts/check-governed-queue-guard.mjs --self-test",
"governed": "node scripts/check-governed-queue-guard.mjs --test",
"check:merge-queue-head": "node scripts/check-merge-queue-head.mjs --self-test",
Expand Down
154 changes: 154 additions & 0 deletions scripts/__tests__/bash32-floor-wiring.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { describe, expect, it } from 'vitest';
import { execFileSync, spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parse as parseYaml } from 'yaml';

const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..');
const GATE = 'scripts/check-bash32-floor.mjs';

/**
* objectui#7692: this repository held its own shell to nothing.
*
* macOS ships bash 3.2.57 and no bash 4+, CI runs bash 5, and every bash-4-only
* construct therefore works everywhere CI can see. A `mapfile`, a `declare -A`
* or an unguarded `$EPOCHSECONDS` in `scripts/setup.sh` — which a contributor
* runs on their own machine — is green in CI, green locally on Linux, and dies
* only on the host the floor exists for. The sibling repository has gated every
* tracked shell file at bash 3.2 for some time; here, four shell files this
* repository wrote itself (`e2e/live/ci/start-backend.sh`,
* `e2e/live/ci/stop-backend.sh`, `scripts/ensure-chromium-ready.sh`,
* `scripts/setup.sh`) were held to nothing at all.
*
* The gate itself carries its own battery: `--self-test` drives all 19
* construct rows against a real instance of each and against the 3.2
* replacement its own failure text tells you to write, plus the end-to-end
* discovery path over throwaway git repositories. ⛔ This file deliberately
* does NOT restate any of that, and does not count cases either — a hand-copied
* enumeration drifts by construction, which is the lesson `lint-workflow.test.ts`
* records at length for this same workflow.
*
* What this file pins is the WIRING, in the directions that can go wrong
* quietly, because a gate nobody runs is indistinguishable from a gate that
* passes:
*
* 1. the script exists and `package.json`'s alias points at it;
* 2. `lint.yml` really runs it, in a step that executes, on pull requests;
* 3. the workflow spelling and the alias spelling name the SAME script, so
* `pnpm check:bash32-floor` and the pre-install `node` invocation cannot
* drift into checking different things;
* 4. it runs BEFORE `pnpm install` — placed after, an install failure takes
* the gate with it and the tree goes unjudged with the job red for an
* unrelated reason;
* 5. its `--self-test` is what the workflow runs first, and it passes;
* 6. ⭐ `e2e/**` is in the declared scan roots, and the `e2e/` shell files are
* really in the population it reports. That is the one thing about this
* port a reviewer cannot check by reading upstream, and the one thing
* whose loss would put the card straight back.
*/
describe('check-bash32-floor is wired, not merely present', () => {
const workflow = parseYaml(fs.readFileSync(path.join(ROOT, '.github/workflows/lint.yml'), 'utf8'));
const steps: Array<Record<string, unknown>> = workflow.jobs.lint.steps;
const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(GATE));

it('the gate script exists', () => {
expect(fs.existsSync(path.join(ROOT, GATE))).toBe(true);
});

it('package.json aliases it, and the alias points at the script that exists', () => {
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
const alias = pkg.scripts['check:bash32-floor'];
expect(alias).toBeTruthy();
expect(alias).toContain(GATE);
});

it('lint.yml runs it — exactly one step, both legs', () => {
expect(gateSteps).toHaveLength(1);
const run = gateSteps[0].run as string;
expect(run).toContain(`node ${GATE} --self-test`);
expect(run.split('\n').some((l) => l.trim() === `node ${GATE}`)).toBe(true);
});

it('that step is not disabled — it runs whenever the job runs its other steps', () => {
// Same guard the rest of the job uses, rather than a hard-coded string: a
// step commented out with `if: false` would pass a literal comparison.
const condition = gateSteps[0].if;
const others = steps
.filter((s) => s !== gateSteps[0] && typeof s.uses !== 'undefined')
.map((s) => s.if);
expect(others).toContain(condition);
});

it('lint.yml still gates pull requests, or the step above is inert', () => {
// `on` parses as the boolean `true` in YAML 1.1; `yaml` gives back `on`.
const on = workflow.on ?? workflow[true as unknown as string];
expect(Object.keys(on)).toContain('pull_request');
});

it('it runs BEFORE pnpm install, so an install failure cannot take it with it', () => {
const gateIndex = steps.indexOf(gateSteps[0]);
const installIndex = steps.findIndex(
(s) => typeof s.run === 'string' && (s.run as string).includes('pnpm install'),
);
expect(installIndex).toBeGreaterThan(-1);
expect(gateIndex).toBeLessThan(installIndex);
});

it('its self-test passes — the half that makes a green scan mean something', () => {
const out = execFileSync('node', [GATE, '--self-test'], { cwd: ROOT, encoding: 'utf8' });
expect(out).toMatch(/check-bash32-floor self-test: \d+ cases pass/);
});
});

/**
* ⭐ The port's one substantive change, pinned where a reviewer can see it.
*
* Upstream declares `scripts/**`, `.claude/hooks/**`, `.githooks/**`. This
* repository has no `.githooks/`, and two of the four shell files it wrote
* itself live under `e2e/`. A verbatim copy of upstream's roots would walk past
* half the population objectui#7692 is about — and would still print a
* confident green line, which is the exact failure the gate exists to refuse.
*
* Both directions are asserted, and the second is the load-bearing one: naming
* the root in the declaration is cheap, and a declaration the walk does not
* honour is the species this pins against.
*/
describe('the e2e/** scan root — the reason this is a port and not a copy', () => {
it('declares e2e/** among its scan roots', async () => {
const { POPULATION_ROOTS } = await import(path.join(ROOT, GATE));
expect(POPULATION_ROOTS).toContain('e2e/**');
expect(POPULATION_ROOTS).not.toContain('.githooks/**');
});

it('and really scans the e2e/ shell files — the declaration is honoured by the walk', async () => {
const { scanTree } = await import(path.join(ROOT, GATE));
const { population } = scanTree(ROOT);
const scanned: string[] = population.map((p: { rel: string }) => p.rel).sort();

// The tree's own listing, not a hand-written expectation: a literal list
// here would go stale the day someone adds a shell file, and the claim is
// about the walk agreeing with the tree rather than with this file.
const tracked = spawnSync('git', ['-C', ROOT, 'ls-files', '--', 'e2e'], { encoding: 'utf8' })
.stdout.split('\n')
.filter((f) => f.endsWith('.sh'))
.sort();

expect(
tracked.length,
'no tracked `.sh` file under e2e/ was found at all, so "the walk sees them" is not a ' +
'reading — this assertion would pass on an empty set. objectui#7692 names two: ' +
'e2e/live/ci/start-backend.sh and e2e/live/ci/stop-backend.sh.',
).toBeGreaterThan(0);

for (const file of tracked) {
expect(
scanned,
`${file} is tracked shell under e2e/ but check-bash32-floor did not scan it. If the ` +
'`e2e/**` root was dropped from POPULATION_ROOTS, that is objectui#7692 reopening: ' +
'the gate goes on printing a green line over a population missing half the shell ' +
'this repository wrote itself.',
).toContain(file);
}
});
});
Loading
Loading