Merge pull request #137 from fiddlerwoaroof/fix/f9qf-pin-the-shell
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
fix(scripts): one shell and one environment for every checker (improvise-f9qf)
This commit is contained in:
+13
-13
@@ -43,7 +43,7 @@ jobs:
|
|||||||
# assembly still exited 0 (improvise-w8ih). It guards the assemble step
|
# assembly still exited 0 (improvise-w8ih). It guards the assemble step
|
||||||
# below, so it fails under this job's name rather than another's.
|
# below, so it fails under this job's name rather than another's.
|
||||||
- name: The stamp gate refuses what it claims to
|
- name: The stamp gate refuses what it claims to
|
||||||
run: ./scripts/assemble-stamp-test.sh
|
run: ./scripts/checkers.sh assemble-stamp
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: wasm32-unknown-unknown
|
targets: wasm32-unknown-unknown
|
||||||
@@ -245,7 +245,7 @@ jobs:
|
|||||||
# The guard above is only worth its line if its refusals are themselves
|
# The guard above is only worth its line if its refusals are themselves
|
||||||
# checked -- the step it replaces was green while asserting nothing.
|
# checked -- the step it replaces was green while asserting nothing.
|
||||||
- name: The node test runner refuses what it claims to
|
- name: The node test runner refuses what it claims to
|
||||||
run: ./scripts/node-test-test.sh
|
run: ./scripts/checkers.sh node-test
|
||||||
|
|
||||||
kb:
|
kb:
|
||||||
name: Knowledge base consistency
|
name: Knowledge base consistency
|
||||||
@@ -265,18 +265,18 @@ jobs:
|
|||||||
# writer could regress green -- the review that found the directive
|
# writer could regress green -- the review that found the directive
|
||||||
# bypass also found that nothing ran this suite.
|
# bypass also found that nothing ran this suite.
|
||||||
- name: The kb tool refuses what it claims to
|
- name: The kb tool refuses what it claims to
|
||||||
run: nix shell nixpkgs#trealla --command ./scripts/kb-test.sh
|
run: nix shell nixpkgs#trealla --command ./scripts/checkers.sh kb
|
||||||
# No trealla needed: pr-watch is driven entirely from fixtures.
|
# Every suite goes through scripts/checkers.sh, which pins a bash >= 5
|
||||||
- name: The PR watcher refuses what it claims to
|
# and scrubs the environment, so the shell and the variables a suite
|
||||||
run: ./scripts/pr-watch-test.sh
|
# passes under here are the ones it passes under locally (improvise-f9qf).
|
||||||
# The release preflight is the one guard whose absence is invisible until
|
- name: The checker runner refuses what it claims to
|
||||||
# a tag is already public and immutable, so its refusals are checked here.
|
run: ./scripts/checkers.sh --self-test
|
||||||
- name: The release preflight refuses what it claims to
|
# pr-watch is driven entirely from fixtures. The release preflight is the
|
||||||
run: ./scripts/release-preflight-test.sh
|
# one guard whose absence is invisible until a tag is public and
|
||||||
# pr-reviews is what stands between a merge and an unread review, and it
|
# immutable. pr-reviews stands between a merge and an unread review, and
|
||||||
# was blind to inline comments for as long as it existed (improvise-xgra).
|
# was blind to inline comments for as long as it existed (improvise-xgra).
|
||||||
- name: The PR review reader refuses what it claims to
|
- name: The remaining checkers refuse what they claim to
|
||||||
run: ./scripts/pr-reviews-test.sh
|
run: ./scripts/checkers.sh pr-watch release-preflight pr-reviews
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Lints the one crate the `test` job cannot reach.
|
# Lints the one crate the `test` job cannot reach.
|
||||||
|
|||||||
@@ -494,3 +494,149 @@ guarded, so the next reader does not mistake the silence for coverage.
|
|||||||
**Where it runs.** The `kb` job, beside `scripts/pr-watch-test.sh` — this
|
**Where it runs.** The `kb` job, beside `scripts/pr-watch-test.sh` — this
|
||||||
repo keeps its check_the_checkers there, not in `test`. The step needs no
|
repo keeps its check_the_checkers there, not in `test`. The step needs no
|
||||||
trealla and no nix of its own; it is `sh`, `git archive` and `awk`.
|
trealla and no nix of its own; it is `sh`, `git archive` and `awk`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. One shell, one environment: `scripts/checkers.sh` (improvise-f9qf)
|
||||||
|
|
||||||
|
`scripts/pr-reviews-test.sh` passed **19/19 locally and 2/19 in CI on the same
|
||||||
|
commit**, because a multi-assignment `local` does not evaluate left to right on
|
||||||
|
bash 5 while macOS's bash 3.2 tolerates it. Two real bugs were invisible to the
|
||||||
|
machine that wrote them. This section is what that cost to close, and the half
|
||||||
|
of it that is not obvious is worth more than the half that is.
|
||||||
|
|
||||||
|
### The obvious half: two platform splits, neither detected
|
||||||
|
|
||||||
|
At `22ab58f` there were sixteen shell scripts in `scripts/`: eleven
|
||||||
|
`#!/usr/bin/env bash`, four `#!/bin/sh`, one `#!/usr/bin/env sh`. That is *two*
|
||||||
|
splits, not one. `bash` is 3.2 on macOS and 5.2 on the CI runner; `sh` is
|
||||||
|
bash-in-sh-mode on macOS and **dash** on the runner. Neither split had anything
|
||||||
|
watching it, and the second had never been noticed at all.
|
||||||
|
|
||||||
|
Both are now `#!/usr/bin/env bash`, and `checkers.sh` refuses any other shell
|
||||||
|
shebang in `scripts/`. Converting the `sh` scripts cost nothing — they were
|
||||||
|
already a subset — and the point is not that dash is bad, it is that **a script
|
||||||
|
whose interpreter depends on the machine is a script nothing has tested**.
|
||||||
|
|
||||||
|
### The half that decides the design: direnv gives the right bash and the wrong environment
|
||||||
|
|
||||||
|
The cheap fix looks like "run scripts through direnv", and `.envrc` does put a
|
||||||
|
nix bash **5.3.15** on `PATH`, which really is CI's semantics. It is not enough,
|
||||||
|
and the reason generalises far past this ticket.
|
||||||
|
|
||||||
|
`pkgs.mkShell` exports a pile of generically-named lowercase variables that CI
|
||||||
|
does not have. Measured inside this repo's dev shell: `name`, `out`, `outputs`,
|
||||||
|
`system`, `shell`, `builder`, `stdenv`, `phases`, `patches`, `buildInputs`,
|
||||||
|
`nativeBuildInputs`, `doCheck`, `strictDeps` and about twenty more. A script
|
||||||
|
under `set -u` that reads one of them by accident is **silent inside the dev
|
||||||
|
shell and fatal in CI**.
|
||||||
|
|
||||||
|
That is exactly what happened. `fixture_file`'s broken third clause read `$name`
|
||||||
|
— and inside the dev shell `$name` was `nix-shell-env`, so `set -u` never fired,
|
||||||
|
the `[ -f "$f" ] ||` fallback re-evaluated `$name` correctly on its own line,
|
||||||
|
and the suite passed. Reproduced at `051f541^`, on one machine, in one sitting:
|
||||||
|
|
||||||
|
| how the suite was run | result |
|
||||||
|
|---|---|
|
||||||
|
| macOS `/bin/bash` 3.2 | 19 passed, 0 failed |
|
||||||
|
| `direnv exec . ./scripts/pr-reviews-test.sh` (nix bash 5.3) | 19 passed, 0 failed |
|
||||||
|
| the same, with `name` unset | **2 passed, 17 failed** |
|
||||||
|
|
||||||
|
The middle row is the finding. Two different reasons for green, one of them the
|
||||||
|
*correct interpreter*, and the whole point of reaching for direnv was that it
|
||||||
|
would not be green. **A dev shell is not a reproduction of CI; it is a third
|
||||||
|
environment**, and it is the one that hides `set -u` faults.
|
||||||
|
|
||||||
|
### The decision, and the two options that were weighed with numbers
|
||||||
|
|
||||||
|
A **nix shebang** was the ticket's first option and the user's own suggestion.
|
||||||
|
Measured warm on the orchestrator's machine, 20 invocations each:
|
||||||
|
|
||||||
|
| | ms per invocation |
|
||||||
|
|---|---|
|
||||||
|
| ambient `bash -c true` | 5.1 |
|
||||||
|
| `nix shell nixpkgs#bash --command true` | 61.8 |
|
||||||
|
| `nix-shell -p bash --run true` (the manpage shebang form) | 675.2 |
|
||||||
|
|
||||||
|
61.8 ms is affordable; 675 ms is 132× the ambient cost, and
|
||||||
|
`scripts/beads-merge-driver.sh` runs inside git's merge machinery. But the cost
|
||||||
|
that decides it is not startup: **only the `kb` job installs nix.** `build`,
|
||||||
|
`test`, `wasm`, `js`, `harness-lint`, `browser` and `pages-deploy` have none, so
|
||||||
|
a nix shebang on `assemble-stamp-test.sh` or `node-test.sh` would need an
|
||||||
|
installer step added to three more jobs — paid on every run, to reach a bash
|
||||||
|
those runners already have. Declined.
|
||||||
|
|
||||||
|
**zsh** was the other option. It is not a shebang change: word splitting differs,
|
||||||
|
arrays index from 1, globbing differs. Porting sixteen scripts means reading
|
||||||
|
sixteen scripts, and the review cost is the whole diff rather than the shebang
|
||||||
|
line. The runner also does not ship zsh, so every job would need an `apt-get`.
|
||||||
|
Declined.
|
||||||
|
|
||||||
|
What was adopted instead pins the *interpreter and the environment together*, in
|
||||||
|
one place, with no per-invocation nix cost:
|
||||||
|
|
||||||
|
**`scripts/checkers.sh` is the one entry point for every check_the_checker.** CI
|
||||||
|
calls it and the local gate calls it, so there is no second invocation to drift.
|
||||||
|
It does three things a bare `./scripts/X-test.sh` does not:
|
||||||
|
|
||||||
|
1. **Pins a bash ≥ 5.** The ambient one is tried first, so a runner that already
|
||||||
|
has bash 5.2 pays nothing; nix supplies one only when the ambient bash is
|
||||||
|
older, which on macOS outside the dev shell it is. No bash ≥ 5 and no nix is
|
||||||
|
**exit 3** — a refusal, never a quiet run under 3.2.
|
||||||
|
2. **Scrubs the environment to an allowlist** (`env -i` plus `PATH`, `HOME`,
|
||||||
|
`TMPDIR`, the `NIX_*` and `GITHUB_*` names, extensible through
|
||||||
|
`CHECKERS_KEEP`). This is the part that closes the direnv hole above, and it
|
||||||
|
is a *positive* list on purpose: the failure mode of a missing name is a loud
|
||||||
|
"command not found", where the failure mode of an extra name is the silence
|
||||||
|
this section is about.
|
||||||
|
3. **Enforces the shebang rule**, so the `sh`/`bash` split cannot come back.
|
||||||
|
|
||||||
|
The pinned bash's directory goes on the front of `PATH`, so a script the suite
|
||||||
|
invokes through its *own* `#!/usr/bin/env bash` — or through `sh`, which
|
||||||
|
nixpkgs' bash also provides — gets the pinned one too. Pinning only the outer
|
||||||
|
shell would have left every inner one on the system bash, which is where the
|
||||||
|
fault actually lived.
|
||||||
|
|
||||||
|
**A suite is `X-test.sh` where `X` or `X.sh` is present beside it.** That rule
|
||||||
|
exists because `scripts/node-test.sh` matches `*-test.sh` and is a test
|
||||||
|
*runner*, not a suite — the first version of the glob ran it with no arguments,
|
||||||
|
which printed its usage inside an otherwise green gate. The naming convention
|
||||||
|
("a check_the_checker is named after the thing it checks") was already true; it
|
||||||
|
is now load-bearing.
|
||||||
|
|
||||||
|
### What the old local gate actually was
|
||||||
|
|
||||||
|
The gate in circulation was `for t in scripts/*-test.sh; do sh "$t"; done`. That
|
||||||
|
loop **overrides every shebang with `/bin/sh`**, so on macOS it ran every suite
|
||||||
|
under bash 3.2 in sh-mode no matter what the file said — the worst of the
|
||||||
|
available shells, chosen by the gate rather than by any script. It is replaced
|
||||||
|
by `./scripts/checkers.sh`.
|
||||||
|
|
||||||
|
### What is still not covered
|
||||||
|
|
||||||
|
`web/*.sh` is outside `scripts/` and outside the shebang check; `web/build.sh`
|
||||||
|
and `web/assemble.sh` are still whatever their own first lines say. The
|
||||||
|
allowlist is a list, so a suite that comes to need a new variable fails loudly
|
||||||
|
and someone edits it — deliberate, but it is a maintenance edge. And the
|
||||||
|
scrubbing protects the *suites*; a production script run by hand inside the dev
|
||||||
|
shell still inherits `name` and friends. Closing that would mean a guard in
|
||||||
|
every script, which `scripts/beads-merge-driver.sh` — running inside git's merge
|
||||||
|
machinery, possibly outside direnv — is a good reason not to write.
|
||||||
|
|
||||||
|
### The seventh site is not a shell file
|
||||||
|
|
||||||
|
Seven places ran a repo script through an explicit `sh`, overriding the callee's
|
||||||
|
own shebang — five in `scripts/`, one in `web/assemble.sh`, and one in
|
||||||
|
`dist-workspace.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
build = ["sh", "scripts/build-web-release.sh"]
|
||||||
|
```
|
||||||
|
|
||||||
|
All seven now invoke the script directly. The TOML one is the interesting one,
|
||||||
|
and it is why the shebang rule alone is not the whole answer: **`checkers.sh`
|
||||||
|
scans first lines, so it cannot see an interpreter named in a build
|
||||||
|
configuration.** A script may declare `#!/usr/bin/env bash`, pass the gate, and
|
||||||
|
still be handed to dash by whatever invokes it. The only defence there is
|
||||||
|
reading, which is how this one was found — by grepping for the *call* after
|
||||||
|
fixing the *declaration*, rather than stopping at the gate going green.
|
||||||
|
|||||||
@@ -288,3 +288,53 @@ helper; a fixture holding one array could not have caught this, because it
|
|||||||
would exercise a shape the live fetch never produces. No PR in this repo has
|
would exercise a shape the live fetch never produces. No PR in this repo has
|
||||||
yet had enough inline comments to reach a second page, so the test is the only
|
yet had enough inline comments to reach a second page, so the test is the only
|
||||||
thing that will ever see it before a user does.
|
thing that will ever see it before a user does.
|
||||||
|
|
||||||
|
### The checker runner's own suite — `scripts/checkers-test.sh`
|
||||||
|
|
||||||
|
The bug behind improvise-f9qf was **invisible to a local run**, so the evidence
|
||||||
|
that matters here is not "the suite is green" but "the suite goes red when the
|
||||||
|
fault is put back, in the place the fault was not seen."
|
||||||
|
|
||||||
|
**The fault, injected against the real script.** Restoring `pr-reviews`'
|
||||||
|
multi-assignment `local` — the exact text `051f541` replaced — and running the
|
||||||
|
suite three ways on one machine in one sitting:
|
||||||
|
|
||||||
|
| gate | result |
|
||||||
|
|---|---|
|
||||||
|
| `sh scripts/pr-reviews-test.sh` (the old local gate) | 22 passed, 0 failed |
|
||||||
|
| `direnv exec . ./scripts/pr-reviews-test.sh` | 22 passed, 0 failed |
|
||||||
|
| `./scripts/checkers.sh pr-reviews` | **2 passed, 20 failed**, exit 1 |
|
||||||
|
|
||||||
|
The middle row is the one that earns the design. The dev shell supplies the
|
||||||
|
*correct* bash and would still have shipped this, because `pkgs.mkShell` exports
|
||||||
|
`name` and the broken clause read `$name`. Only scrubbing the environment as
|
||||||
|
well as pinning the shell makes the fault visible, and the 2/20 shape matches
|
||||||
|
the 2/19 CI actually reported.
|
||||||
|
|
||||||
|
**`checkers-test.sh` itself is twelve cases, and each injects rather than
|
||||||
|
asserts.** The two that carry the argument:
|
||||||
|
|
||||||
|
- *a suite reading an inherited dev-shell variable fails under the runner* — a
|
||||||
|
fixture suite whose body is `echo "$name"` under `set -euo pipefail`, run with
|
||||||
|
`name=nix-shell-env` exported, exactly as direnv leaves it. It is paired with
|
||||||
|
*and passes when run directly with that variable exported*, which asserts the
|
||||||
|
**same fixture passes the old way**. Without that pair the first case is a
|
||||||
|
test of `set -u` and proves nothing about the runner.
|
||||||
|
- *a bash-3.2-only construct fails under the runner's pinned bash* — a fixture
|
||||||
|
suite containing `local a="$1" b="$a/x"`, which is the bug's shape reduced to
|
||||||
|
two lines. Green on bash 3.2, fatal on bash 5.
|
||||||
|
|
||||||
|
The rest separate the runner's outcomes from each other: a `#!/bin/sh` script in
|
||||||
|
the directory is refused **and the same sandbox passes once it is removed** (so
|
||||||
|
the refusal is not standing in for some other complaint); a failing suite fails
|
||||||
|
and a passing one passes; an old bash with no nix is exit 3 rather than a quiet
|
||||||
|
run; an unknown suite name is exit 4 rather than a silent no-op; and `--list`
|
||||||
|
names exactly the suites present, including the case where an `X-test.sh` has no
|
||||||
|
`X` beside it — `scripts/node-test.sh` is a test *runner*, and the first version
|
||||||
|
of the glob ran it as a suite, where it printed its usage inside a green gate.
|
||||||
|
|
||||||
|
**What is asserted about the runner's own environment, and what is not.** The
|
||||||
|
allowlist is checked only in the sense that seven real suites pass through it;
|
||||||
|
there is no case asserting a *particular* name is absent beyond `name`. A suite
|
||||||
|
that grows a dependency on some other inherited variable will fail loudly on
|
||||||
|
first run rather than be caught here. Stated rather than guarded.
|
||||||
|
|||||||
+1
-1
@@ -36,4 +36,4 @@ install-updater = false
|
|||||||
# web/assemble.sh, the same two scripts ci.yml and a developer run.
|
# web/assemble.sh, the same two scripts ci.yml and a developer run.
|
||||||
[[dist.extra-artifacts]]
|
[[dist.extra-artifacts]]
|
||||||
artifacts = ["improvise-web.tar.gz"]
|
artifacts = ["improvise-web.tar.gz"]
|
||||||
build = ["sh", "scripts/build-web-release.sh"]
|
build = ["scripts/build-web-release.sh"]
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ bad() { printf ' FAIL %s (exit %s, wanted %s)\n' "$1" "$2" "$3"; fail=$((fail+1
|
|||||||
run_case() {
|
run_case() {
|
||||||
local name="$1" want="$2" says="$3"; shift 3
|
local name="$1" want="$2" says="$3"; shift 3
|
||||||
local err
|
local err
|
||||||
err="$(sh "$RUN" "$@" 2>&1 >/dev/null)"
|
err="$("$RUN" "$@" 2>&1 >/dev/null)"
|
||||||
local got=$?
|
local got=$?
|
||||||
if [ "$got" != "$want" ]; then
|
if [ "$got" != "$want" ]; then
|
||||||
bad "$name" "$got" "$want"
|
bad "$name" "$got" "$want"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env bash
|
||||||
# Every cache-bust stamp web/assemble.sh writes must be readable back.
|
# Every cache-bust stamp web/assemble.sh writes must be readable back.
|
||||||
#
|
#
|
||||||
# Usage: scripts/assemble-stamp.sh FILE PATTERN [FILE PATTERN ...]
|
# Usage: scripts/assemble-stamp.sh FILE PATTERN [FILE PATTERN ...]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env bash
|
||||||
# Git merge driver for .beads/issues.jsonl. Reasoning: context/code/tooling.md.
|
# Git merge driver for .beads/issues.jsonl. Reasoning: context/code/tooling.md.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env bash
|
||||||
# Capture the assembled site as a release artifact (improvise-oujb).
|
# Capture the assembled site as a release artifact (improvise-oujb).
|
||||||
#
|
#
|
||||||
# `dist` runs this from the repo root as an `[[dist.extra-artifacts]]` build
|
# `dist` runs this from the repo root as an `[[dist.extra-artifacts]]` build
|
||||||
@@ -81,14 +81,14 @@ if [ "${node_major:-0}" != "$NODE_VERSION" ]; then
|
|||||||
fi
|
fi
|
||||||
node --version
|
node --version
|
||||||
|
|
||||||
sh "$root/web/build.sh"
|
"$root/web/build.sh"
|
||||||
|
|
||||||
# The site is assembled into a directory named for what it is, so extracting
|
# The site is assembled into a directory named for what it is, so extracting
|
||||||
# the tarball leaves one self-describing folder rather than loose files.
|
# the tarball leaves one self-describing folder rather than loose files.
|
||||||
stage="$root/target/web-release"
|
stage="$root/target/web-release"
|
||||||
rm -rf "$stage"
|
rm -rf "$stage"
|
||||||
mkdir -p "$stage"
|
mkdir -p "$stage"
|
||||||
sh "$root/web/assemble.sh" "$stage/improvise-web" "$token"
|
"$root/web/assemble.sh" "$stage/improvise-web" "$token"
|
||||||
|
|
||||||
rm -f "$out"
|
rm -f "$out"
|
||||||
tar -czf "$out" -C "$stage" improvise-web
|
tar -czf "$out" -C "$stage" improvise-web
|
||||||
|
|||||||
Executable
+180
@@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# check_the_checker for scripts/checkers.sh. Every case injects a fault that
|
||||||
|
# a bare `./scripts/X-test.sh` cannot see, and asserts the runner does.
|
||||||
|
# See context/test-evidence/tooling.md.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
RUNNER="$here/checkers.sh"
|
||||||
|
work="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
|
||||||
|
pass=0; fail=0
|
||||||
|
ok() { printf ' ok %s\n' "$1"; pass=$((pass+1)); }
|
||||||
|
bad() { printf ' FAIL %s (%s)\n' "$1" "$2"; fail=$((fail+1)); }
|
||||||
|
|
||||||
|
# A sandbox is a copy of the runner beside fixture suites, so `$HERE` inside it
|
||||||
|
# is the sandbox: the shebang scan and the suite list are both the fixtures'.
|
||||||
|
sandbox() {
|
||||||
|
local d="$work/$1"
|
||||||
|
mkdir -p "$d"
|
||||||
|
cp "$RUNNER" "$d/checkers.sh"
|
||||||
|
chmod +x "$d/checkers.sh"
|
||||||
|
printf '%s' "$d"
|
||||||
|
}
|
||||||
|
|
||||||
|
# A suite is only discovered when the thing it checks is present, so every
|
||||||
|
# fixture gets a stub subject beside it.
|
||||||
|
suite() {
|
||||||
|
local d="$1" name="$2"
|
||||||
|
cat > "$d/$name-test.sh"
|
||||||
|
chmod +x "$d/$name-test.sh"
|
||||||
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$d/$name.sh"
|
||||||
|
chmod +x "$d/$name.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 name, $2 expected exit, rest: the command. Nothing follows the runner in
|
||||||
|
# its own pipeline, which is the rule its own header states.
|
||||||
|
expect() {
|
||||||
|
local what="$1" want="$2"; shift 2
|
||||||
|
"$@" > "$work/out" 2>&1
|
||||||
|
local got=$?
|
||||||
|
[ "$got" = "$want" ] && ok "$what" || bad "$what" "exit $got, wanted $want"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. The fault this runner exists for. `nix develop` exports `name`; a suite
|
||||||
|
# that reads an unset variable under `set -u` is therefore SILENT in the dev
|
||||||
|
# shell and fatal in CI. That is improvise-f9qf, reproduced exactly.
|
||||||
|
d="$(sandbox setu)"
|
||||||
|
suite "$d" masked <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
echo "read: $name"
|
||||||
|
EOF
|
||||||
|
export name=nix-shell-env
|
||||||
|
expect "a suite reading an inherited dev-shell variable fails under the runner" \
|
||||||
|
1 "$d/checkers.sh" masked
|
||||||
|
|
||||||
|
# The same suite run the way the old gate ran it PASSES, which is what makes
|
||||||
|
# the case above discriminating rather than a test of `set -u`.
|
||||||
|
"$d/masked-test.sh" > "$work/out" 2>&1
|
||||||
|
direct=$?
|
||||||
|
[ "$direct" = 0 ] \
|
||||||
|
&& ok "and passes when run directly with that variable exported" \
|
||||||
|
|| bad "and passes when run directly with that variable exported" \
|
||||||
|
"exit $direct, wanted 0"
|
||||||
|
unset name
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. The bash-5 `local` semantics. A multi-assignment `local` does not read
|
||||||
|
# left to right on bash 5, so this suite passes on macOS bash 3.2 and dies
|
||||||
|
# under `set -u` on bash 5 -- PR #133's 19/19 local against 2/19 in CI.
|
||||||
|
d="$(sandbox bash5)"
|
||||||
|
suite "$d" vers <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
f() { local a="$1" b="$a/x"; echo "$b"; }
|
||||||
|
f one
|
||||||
|
EOF
|
||||||
|
expect "a bash-3.2-only construct fails under the runner's pinned bash" \
|
||||||
|
1 "$d/checkers.sh" vers
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. The shebang rule. `#!/bin/sh` is dash in CI and bash-in-sh-mode on macOS.
|
||||||
|
d="$(sandbox shebang)"
|
||||||
|
suite "$d" fine <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
printf '#!/bin/sh\nexit 0\n' > "$d/legacy.sh"
|
||||||
|
chmod +x "$d/legacy.sh"
|
||||||
|
expect "a #!/bin/sh script in the scripts directory is refused" 1 \
|
||||||
|
"$d/checkers.sh" fine
|
||||||
|
grep -q 'wanted #!/usr/bin/env bash' "$work/out" \
|
||||||
|
&& ok "and the refusal names the shebang it wanted" \
|
||||||
|
|| bad "and the refusal names the shebang it wanted" "no such message"
|
||||||
|
|
||||||
|
# Without the injected file the same sandbox passes, so the case above is not
|
||||||
|
# passing on some other complaint.
|
||||||
|
rm -f "$d/legacy.sh"
|
||||||
|
expect "and the same sandbox passes once it is removed" 0 "$d/checkers.sh" fine
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. A failing suite is reported, and a passing one is not.
|
||||||
|
d="$(sandbox rc)"
|
||||||
|
suite "$d" red <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 1
|
||||||
|
EOF
|
||||||
|
expect "a failing suite fails the runner" 1 "$d/checkers.sh" red
|
||||||
|
|
||||||
|
d="$(sandbox green)"
|
||||||
|
suite "$d" blue <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
expect "a passing suite passes the runner" 0 "$d/checkers.sh" blue
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 5. No bash >= 5 and no nix is exit 3, not a silent run under whatever is
|
||||||
|
# there. This is the arm that would have refused the macOS system bash.
|
||||||
|
d="$(sandbox oldbash)"
|
||||||
|
suite "$d" any <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
mkdir -p "$work/fakebin"
|
||||||
|
cat > "$work/fakebin/bash" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
case "$*" in *BASH_VERSINFO*) echo 3; exit 0 ;; esac
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x "$work/fakebin/bash"
|
||||||
|
# The runner is invoked through a REAL bash, so the fake one below is only
|
||||||
|
# what its own `command -v bash` probe finds -- not its interpreter.
|
||||||
|
# /usr/bin:/bin carries the utilities the runner itself needs and carries no
|
||||||
|
# nix, so the fake bash is the only candidate and the nix fallback is absent.
|
||||||
|
env PATH="$work/fakebin:/usr/bin:/bin" HOME="$HOME" \
|
||||||
|
"${BASH:-bash}" "$d/checkers.sh" any > "$work/out" 2>&1
|
||||||
|
got=$?
|
||||||
|
[ "$got" = 3 ] && ok "an old bash with no nix is exit 3" \
|
||||||
|
|| bad "an old bash with no nix is exit 3" "exit $got, wanted 3"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 6. A named suite that does not exist is a usage error, not a silent no-op.
|
||||||
|
d="$(sandbox missing)"
|
||||||
|
suite "$d" present <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
expect "an unknown suite name is exit 4" 4 "$d/checkers.sh" absent
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 7. --list names every suite in the directory and nothing else.
|
||||||
|
d="$(sandbox listing)"
|
||||||
|
suite "$d" alpha <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
suite "$d" beta <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
"$d/checkers.sh" --list > "$work/out" 2>&1
|
||||||
|
listed="$(tr '\n' ' ' < "$work/out")"
|
||||||
|
[ "$listed" = "alpha beta " ] && ok "--list names exactly the suites present" \
|
||||||
|
|| bad "--list names exactly the suites present" "got [$listed]"
|
||||||
|
|
||||||
|
# A test RUNNER also ends in -test.sh. It is not a suite, and running it as
|
||||||
|
# one is how `scripts/node-test.sh` printed its usage inside a green gate.
|
||||||
|
printf '#!/usr/bin/env bash\nexit 4\n' > "$d/gamma-test.sh"
|
||||||
|
chmod +x "$d/gamma-test.sh"
|
||||||
|
"$d/checkers.sh" --list > "$work/out" 2>&1
|
||||||
|
listed="$(tr '\n' ' ' < "$work/out")"
|
||||||
|
[ "$listed" = "alpha beta " ] \
|
||||||
|
&& ok "an X-test.sh with no X beside it is not a suite" \
|
||||||
|
|| bad "an X-test.sh with no X beside it is not a suite" "got [$listed]"
|
||||||
|
|
||||||
|
printf '\ncheckers-test: %d passed, %d failed\n' "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
Executable
+154
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# The one entry point for every check_the_checker in scripts/. CI calls THIS,
|
||||||
|
# and so does the local gate, so a suite cannot be green under one shell and
|
||||||
|
# red under the other. Reasoning: context/code/tooling.md section 3.
|
||||||
|
#
|
||||||
|
# It does three things a bare `./scripts/X-test.sh` does not:
|
||||||
|
#
|
||||||
|
# 1. pins a bash >= 5, taken from nix when the ambient one is older, so the
|
||||||
|
# macOS system bash 3.2 can never be the shell a suite passes under;
|
||||||
|
# 2. scrubs the environment down to an allowlist, because `nix develop`
|
||||||
|
# exports `name`, `out`, `system`, `shell`, `builder` and two dozen more
|
||||||
|
# generically-named variables that CI does not have -- a `set -u` fault
|
||||||
|
# reading one of them is SILENT inside the dev shell and fatal in CI;
|
||||||
|
# 3. refuses a shell script in scripts/ whose shebang is not
|
||||||
|
# `#!/usr/bin/env bash`, so the sh/bash split cannot come back.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
usage: checkers.sh [--list] [--self-test] [name ...]
|
||||||
|
|
||||||
|
name a suite to run, spelled without the -test.sh suffix
|
||||||
|
(e.g. `pr-reviews` for scripts/pr-reviews-test.sh).
|
||||||
|
With no name, every suite in scripts/ runs.
|
||||||
|
--list print the suite names and exit
|
||||||
|
--self-test run scripts/checkers-test.sh, which injects the faults this
|
||||||
|
runner exists to catch
|
||||||
|
|
||||||
|
exit 0 every suite passed
|
||||||
|
exit 1 a suite failed, or a shebang is not `#!/usr/bin/env bash`
|
||||||
|
exit 3 no bash >= 5 could be found and nix could not supply one
|
||||||
|
exit 4 usage error
|
||||||
|
|
||||||
|
THIS SCRIPT'S `$?` MUST BE THE ONE YOU READ. Nothing may follow it in the same
|
||||||
|
pipeline or `;` list -- a trailing `echo "exit=$?"` takes the status.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# The allowlist. A variable not named here does not reach a suite, which is the
|
||||||
|
# whole point: the dev shell's `name` masked improvise-f9qf's fault for a day.
|
||||||
|
# CHECKERS_KEEP adds names, space-separated, for a caller that needs one.
|
||||||
|
ALLOW="HOME TMPDIR TMP TEMP TERM LANG LC_ALL USER LOGNAME
|
||||||
|
CI GITHUB_ACTIONS GITHUB_TOKEN GH_TOKEN
|
||||||
|
NIX_PATH NIX_REMOTE NIX_SSL_CERT_FILE SSL_CERT_FILE
|
||||||
|
XDG_CACHE_HOME XDG_CONFIG_HOME XDG_DATA_HOME"
|
||||||
|
|
||||||
|
# Prints the absolute path of a bash >= 5, or nothing. The ambient one is tried
|
||||||
|
# first so a machine that already has a modern bash pays no nix startup at all.
|
||||||
|
find_modern_bash() {
|
||||||
|
local candidate major
|
||||||
|
candidate="$(command -v bash || true)"
|
||||||
|
if [ -n "$candidate" ]; then
|
||||||
|
major="$("$candidate" -c 'echo "${BASH_VERSINFO[0]}"' 2>/dev/null || echo 0)"
|
||||||
|
if [ "$major" -ge 5 ] 2>/dev/null; then echo "$candidate"; return 0; fi
|
||||||
|
fi
|
||||||
|
command -v nix >/dev/null 2>&1 || return 0
|
||||||
|
nix shell nixpkgs#bash --command bash -c 'command -v bash' 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Every shell script in scripts/ says `#!/usr/bin/env bash` and nothing else.
|
||||||
|
# `#!/bin/sh` is dash in CI and bash 3.2 in sh-mode on macOS: a second platform
|
||||||
|
# split, undetected for the same reason the first one was.
|
||||||
|
check_shebangs() {
|
||||||
|
local bad=0 f first
|
||||||
|
for f in "$HERE"/*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
first="$(head -1 "$f")"
|
||||||
|
case "$first" in
|
||||||
|
'#!'*sh|'#!'*sh\ *)
|
||||||
|
case "$first" in
|
||||||
|
'#!/usr/bin/env bash') ;;
|
||||||
|
*) printf 'checkers: %s has shebang %s, wanted #!/usr/bin/env bash\n' \
|
||||||
|
"${f##*/}" "$first" >&2; bad=1 ;;
|
||||||
|
esac ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
return $bad
|
||||||
|
}
|
||||||
|
|
||||||
|
# A check_the_checker is named after the thing it checks, so `X-test.sh` is a
|
||||||
|
# suite only when `X` or `X.sh` is here too. That excludes `node-test.sh`,
|
||||||
|
# which is a test RUNNER whose own checker is `node-test-test.sh`.
|
||||||
|
suites() {
|
||||||
|
local f n
|
||||||
|
for f in "$HERE"/*-test.sh; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
n="${f##*/}"; n="${n%-test.sh}"
|
||||||
|
if [ -f "$HERE/$n" ] || [ -f "$HERE/$n.sh" ]; then printf '%s\n' "$n"; fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 the pinned bash, $2 the suite file. The suite runs with the pinned bash
|
||||||
|
# FIRST on PATH, so a script it invokes through its own `#!/usr/bin/env bash`
|
||||||
|
# -- or through `sh` -- gets the pinned one too, not the system 3.2.
|
||||||
|
run_suite() {
|
||||||
|
local pinned="$1" file="$2"
|
||||||
|
local bindir; bindir="$(dirname "$pinned")"
|
||||||
|
local -a env_args=(-i)
|
||||||
|
local v
|
||||||
|
for v in $ALLOW ${CHECKERS_KEEP:-}; do
|
||||||
|
if [ -n "${!v+set}" ]; then env_args+=("$v=${!v}"); fi
|
||||||
|
done
|
||||||
|
env_args+=("PATH=$bindir:${PATH:-/usr/bin:/bin}")
|
||||||
|
env "${env_args[@]}" "$pinned" "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local self_test=0
|
||||||
|
local -a names=()
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--list) suites; return 0 ;;
|
||||||
|
--self-test) self_test=1 ;;
|
||||||
|
-h|--help) usage; return 4 ;;
|
||||||
|
-*) printf 'checkers: unknown option %s\n' "$1" >&2; usage; return 4 ;;
|
||||||
|
*) names+=("$1") ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
local pinned; pinned="$(find_modern_bash)"
|
||||||
|
if [ -z "$pinned" ]; then
|
||||||
|
echo 'checkers: no bash >= 5 on PATH and nix cannot supply one.' >&2
|
||||||
|
echo ' Enter the dev shell (direnv allow / nix develop) and retry.' >&2
|
||||||
|
return 3
|
||||||
|
fi
|
||||||
|
printf 'checkers: bash %s at %s\n' \
|
||||||
|
"$("$pinned" -c 'echo "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}"')" "$pinned"
|
||||||
|
|
||||||
|
local rc=0
|
||||||
|
check_shebangs || rc=1
|
||||||
|
|
||||||
|
if [ "$self_test" = 1 ]; then
|
||||||
|
names=(checkers)
|
||||||
|
elif [ ${#names[@]} -eq 0 ]; then
|
||||||
|
while IFS= read -r line; do names+=("$line"); done < <(suites)
|
||||||
|
fi
|
||||||
|
|
||||||
|
local n file
|
||||||
|
for n in ${names[@]+"${names[@]}"}; do
|
||||||
|
file="$HERE/$n-test.sh"
|
||||||
|
if [ ! -f "$file" ]; then
|
||||||
|
printf 'checkers: no such suite: %s\n' "$n" >&2
|
||||||
|
rc=4; continue
|
||||||
|
fi
|
||||||
|
printf '\n=== %s ===\n' "$n"
|
||||||
|
run_suite "$pinned" "$file" || rc=1
|
||||||
|
done
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env bash
|
||||||
# check_the_checker for scripts/release-preflight.sh.
|
# check_the_checker for scripts/release-preflight.sh.
|
||||||
#
|
#
|
||||||
# It exists for the reason scripts/assemble-stamp-test.sh does: the guard is the
|
# It exists for the reason scripts/assemble-stamp-test.sh does: the guard is the
|
||||||
@@ -22,7 +22,7 @@ fails=0
|
|||||||
# Refuses, AND the refusal names the tag/version mismatch rather than the tree.
|
# Refuses, AND the refusal names the tag/version mismatch rather than the tree.
|
||||||
refuses_for_the_tag() {
|
refuses_for_the_tag() {
|
||||||
what=$1; tag=$2
|
what=$1; tag=$2
|
||||||
out=$(sh "$script" "$tag" 2>&1) && { echo "FAIL: $what -- accepted" >&2; fails=$((fails+1)); return; }
|
out=$("$script" "$tag" 2>&1) && { echo "FAIL: $what -- accepted" >&2; fails=$((fails+1)); return; }
|
||||||
case "$out" in
|
case "$out" in
|
||||||
*"Cargo.toml has"*) echo " ok: $what (refused, naming the version)" ;;
|
*"Cargo.toml has"*) echo " ok: $what (refused, naming the version)" ;;
|
||||||
*"uncommitted changes"*) echo "FAIL: $what -- refused for the DIRTY TREE, so this check proved nothing" >&2; fails=$((fails+1)) ;;
|
*"uncommitted changes"*) echo "FAIL: $what -- refused for the DIRTY TREE, so this check proved nothing" >&2; fails=$((fails+1)) ;;
|
||||||
@@ -46,13 +46,13 @@ cp -f "$root/Cargo.toml" "$tmp/Cargo.toml"
|
|||||||
(cd "$tmp" && git init -q . && git add -A && git -c user.email=t@t -c user.name=t commit -qm t)
|
(cd "$tmp" && git init -q . && git add -A && git -c user.email=t@t -c user.name=t commit -qm t)
|
||||||
script="$tmp/scripts/release-preflight.sh"
|
script="$tmp/scripts/release-preflight.sh"
|
||||||
|
|
||||||
if sh "$script" --self-test >/dev/null 2>&1; then
|
if "$script" --self-test >/dev/null 2>&1; then
|
||||||
echo " ok: --self-test (accepted)"
|
echo " ok: --self-test (accepted)"
|
||||||
else
|
else
|
||||||
echo "FAIL: --self-test refused" >&2; fails=$((fails+1))
|
echo "FAIL: --self-test refused" >&2; fails=$((fails+1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if sh "$script" >/dev/null 2>&1; then
|
if "$script" >/dev/null 2>&1; then
|
||||||
echo "FAIL: no argument should be a usage error" >&2; fails=$((fails+1))
|
echo "FAIL: no argument should be a usage error" >&2; fails=$((fails+1))
|
||||||
else
|
else
|
||||||
echo " ok: no argument is a usage error (refused)"
|
echo " ok: no argument is a usage error (refused)"
|
||||||
@@ -63,7 +63,7 @@ refuses_for_the_tag "the shape that actually broke CI" "v0.1.0-rc99"
|
|||||||
refuses_for_the_tag "a bare version with no v" "$version"
|
refuses_for_the_tag "a bare version with no v" "$version"
|
||||||
|
|
||||||
# The positive direction, which only a clean tree can exercise.
|
# The positive direction, which only a clean tree can exercise.
|
||||||
if sh "$script" "v$version" >/dev/null 2>&1; then
|
if "$script" "v$version" >/dev/null 2>&1; then
|
||||||
echo " ok: the matching tag v$version (accepted)"
|
echo " ok: the matching tag v$version (accepted)"
|
||||||
else
|
else
|
||||||
echo "FAIL: the matching tag v$version was refused" >&2; fails=$((fails+1))
|
echo "FAIL: the matching tag v$version was refused" >&2; fails=$((fails+1))
|
||||||
@@ -75,7 +75,7 @@ fi
|
|||||||
# only be the tree -- which is what separates this check from the ones above
|
# only be the tree -- which is what separates this check from the ones above
|
||||||
# instead of duplicating them. Disabling the guard leaves all six of those green.
|
# instead of duplicating them. Disabling the guard leaves all six of those green.
|
||||||
echo "dirtied by release-preflight-test" >> "$tmp/README.md"
|
echo "dirtied by release-preflight-test" >> "$tmp/README.md"
|
||||||
if out=$(sh "$script" "v$version" 2>&1); then
|
if out=$("$script" "v$version" 2>&1); then
|
||||||
echo "FAIL: a dirty tree -- accepted" >&2; fails=$((fails+1))
|
echo "FAIL: a dirty tree -- accepted" >&2; fails=$((fails+1))
|
||||||
else
|
else
|
||||||
case "$out" in
|
case "$out" in
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env bash
|
||||||
# Answer, before a tag exists, the question the Release workflow answers after.
|
# Answer, before a tag exists, the question the Release workflow answers after.
|
||||||
#
|
#
|
||||||
# Usage: scripts/release-preflight.sh vX.Y.Z[-suffix]
|
# Usage: scripts/release-preflight.sh vX.Y.Z[-suffix]
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@ sedi "s|'improvise_worker_server_bg\.wasm'|'improvise_worker_server_bg.wasm?v=${
|
|||||||
# script so its refusals can be exercised without fabricating a web/pkg
|
# script so its refusals can be exercised without fabricating a web/pkg
|
||||||
# (improvise-w8ih). Its argument is FILE PATTERN, five times -- the page's own
|
# (improvise-w8ih). Its argument is FILE PATTERN, five times -- the page's own
|
||||||
# #build-version among them (improvise-oujb).
|
# #build-version among them (improvise-oujb).
|
||||||
sh "$root/scripts/assemble-stamp.sh" \
|
"$root/scripts/assemble-stamp.sh" \
|
||||||
"$app/index.html" "id=\"build-version\">${token}<" \
|
"$app/index.html" "id=\"build-version\">${token}<" \
|
||||||
"$app/app.js" "pkg/improvise_wasm_client.js?v=${token}" \
|
"$app/app.js" "pkg/improvise_wasm_client.js?v=${token}" \
|
||||||
"$app/worker.js" "worker-pkg/improvise_worker_server.js?v=${token}" \
|
"$app/worker.js" "worker-pkg/improvise_worker_server.js?v=${token}" \
|
||||||
|
|||||||
Reference in New Issue
Block a user