diff --git a/adapters/laravel-inertia/adapter.env b/adapters/laravel-inertia/adapter.env index 0003c51..c54e2e8 100644 --- a/adapters/laravel-inertia/adapter.env +++ b/adapters/laravel-inertia/adapter.env @@ -17,3 +17,10 @@ ADAPTER_FAMILY="laravel" # a cold cache — it reads as a hang. Widened, not disabled, so a real hang still # ends the test. ADAPTER_GENERATOR='SHELL_VERBOSITY=-1 COMPOSER_PROCESS_TIMEOUT=900 composer create-project "laravel/vue-starter-kit:dev-main#11a7368240d7d9146415aa6bfc858a90b6ce8490" "$APP_DIR" --no-interaction --prefer-dist' +# The vue starter kit ships its own .github/, and composer create-project +# brings it along into apps/app/. At that path github ignores both files in +# it, so they configure nothing — but zizmor still audits every workflow and +# dependabot file in the tree, and the kit's dependabot.yml sets +# `cooldown.default-days: 5`, which fails the security gate with exit 13 on +# the first pull request. Inert config whose only effect is a red check. +ADAPTER_POST_GENERATE='rm -rf .github' diff --git a/adapters/laravel-inertia/mise.toml b/adapters/laravel-inertia/mise.toml index ce0c049..c09e52e 100644 --- a/adapters/laravel-inertia/mise.toml +++ b/adapters/laravel-inertia/mise.toml @@ -9,10 +9,23 @@ node = "24.15.0" [tasks.install] # mise installs composer as composer.phar, not composer +# `npm run build` belongs here, not in check or test, and it is not optional. +# Two artifacts it produces are gitignored and generated by nothing else: +# resources/js/{actions,routes}, written by the vite plugin's +# `php artisan wayfinder:generate --with-form`, which types:check compiles +# against; and public/build/manifest.json, without which every test that +# renders an inertia page 500s. Neither is visible locally, because the +# pre-push checklist runs build and leaves both on disk — so the app passed +# every local run and could never pass CI, which checks out clean. +# +# This is the order the starter kit's own CI uses: `composer setup` ends with +# `npm run build`, and only then does `composer ci:check` run types:check and +# the tests. run = [ "php -r 'exit(version_compare(PHP_VERSION, \"8.3.0\", \">=\") ? 0 : 1);' || { echo \"laravel-inertia requires system php >= 8.3.0 (found $(php -r 'echo PHP_VERSION;' 2>/dev/null || echo 'no php on PATH')); install php 8.3 or newer via your OS package manager (e.g. apt install php8.3-cli) or https://php.net, then re-run mise install\" >&2; exit 1; }", "composer.phar install --no-interaction --prefer-dist", "npm ci", + "npm run build", ] # the starter kit's frontend toolchain is vite-plus, whose `vp check` covers diff --git a/adapters/nestjs/.prettierignore b/adapters/nestjs/.prettierignore index bd5535a..94e72f3 100644 --- a/adapters/nestjs/.prettierignore +++ b/adapters/nestjs/.prettierignore @@ -1 +1,6 @@ pnpm-lock.yaml +# `nest build` writes here, and format is `prettier --check .`. Within one +# checklist run format precedes build, so the first run is clean and every +# run after it fails on the previous run's output — a task list that poisons +# itself. CI never saw it: a fresh checkout has no dist. +dist diff --git a/adapters/nextjs/Dockerfile b/adapters/nextjs/Dockerfile index 3bf75de..807c4f2 100644 --- a/adapters/nextjs/Dockerfile +++ b/adapters/nextjs/Dockerfile @@ -26,6 +26,10 @@ COPY --from=build /app/.next/static ./.next/static COPY --from=build /app/public ./public USER node EXPOSE 3000 +# `/`, not `/api/health`: create-next-app generates no health route, so every +# image built from here reported unhealthy from first boot until somebody +# noticed and wrote one. An app that adds a real health endpoint should point +# this at it. HEALTHCHECK --interval=30s --timeout=3s \ - CMD wget -qO- http://localhost:3000/api/health || exit 1 + CMD wget -qO- http://localhost:3000/ || exit 1 CMD ["node", "server.js"] diff --git a/adapters/nextjs/Dockerfile.workspace b/adapters/nextjs/Dockerfile.workspace index bc63e32..f578239 100644 --- a/adapters/nextjs/Dockerfile.workspace +++ b/adapters/nextjs/Dockerfile.workspace @@ -32,6 +32,10 @@ COPY --from=build /app/apps/@APP_FILTER@/.next/static ./apps/@APP_FILTER@/.next/ COPY --from=build /app/apps/@APP_FILTER@/public ./apps/@APP_FILTER@/public USER node EXPOSE 3000 +# `/`, not `/api/health`: create-next-app generates no health route, so every +# image built from here reported unhealthy from first boot until somebody +# noticed and wrote one. An app that adds a real health endpoint should point +# this at it. HEALTHCHECK --interval=30s --timeout=3s \ - CMD wget -qO- http://localhost:3000/api/health || exit 1 + CMD wget -qO- http://localhost:3000/ || exit 1 CMD ["node", "apps/@APP_FILTER@/server.js"] diff --git a/common/.github/workflows/security.yml b/common/.github/workflows/security.yml index 4c59de3..4daa3f1 100644 --- a/common/.github/workflows/security.yml +++ b/common/.github/workflows/security.yml @@ -4,7 +4,7 @@ on: push: branches: [main] schedule: - - cron: "17 3 * * 1" + - cron: '17 3 * * 1' permissions: {} jobs: security: diff --git a/common/.prettierignore b/common/.prettierignore new file mode 100644 index 0000000..ad156aa --- /dev/null +++ b/common/.prettierignore @@ -0,0 +1,11 @@ +# The pre-commit hook runs `prettier --write` over every staged file matching +# its glob, and `yaml` is in that glob. Left to itself prettier reformats +# pnpm-lock.yaml — it grew by 2-4 KB the one time it happened — and rewrites +# CHANGELOG.md's bullets from `*` to `-` on every release, fighting Release +# Please for a file Release Please owns. Neither file is written by hand, so +# neither is prettier's to format. +pnpm-lock.yaml +**/pnpm-lock.yaml +package-lock.json +**/package-lock.json +CHANGELOG.md diff --git a/common/commitlint.config.js b/common/commitlint.config.js index b29b5ae..0616fb9 100644 --- a/common/commitlint.config.js +++ b/common/commitlint.config.js @@ -1,3 +1,3 @@ export default { - extends: ["@commitlint/config-conventional"], + extends: ['@commitlint/config-conventional'], }; diff --git a/common/compose.yaml b/common/compose.yaml index 6e179a8..1b3b472 100644 --- a/common/compose.yaml +++ b/common/compose.yaml @@ -25,4 +25,4 @@ services: required: false restart: always ports: - - "${APP_PORT:-8080}:8080" + - '${APP_PORT:-8080}:8080' diff --git a/common/lefthook.yml b/common/lefthook.yml index 0f4e7fe..21c3e36 100644 --- a/common/lefthook.yml +++ b/common/lefthook.yml @@ -6,7 +6,7 @@ pre-commit: parallel: true commands: prettier: - glob: "*.{ts,tsx,js,mjs,cjs,json,md,yml,yaml,css}" + glob: '*.{ts,tsx,js,mjs,cjs,json,md,yml,yaml,css}' run: mise exec -- pnpm exec prettier --write {staged_files} stage_fixed: true gitleaks: diff --git a/common/pnpm-workspace.yaml b/common/pnpm-workspace.yaml index 9fc38f8..5ad1ce8 100644 --- a/common/pnpm-workspace.yaml +++ b/common/pnpm-workspace.yaml @@ -9,4 +9,4 @@ packages: allowBuilds: unrs-resolver: false esbuild: true - "@parcel/watcher": false + '@parcel/watcher': false diff --git a/docs/runbook/first-project-walkthrough.md b/docs/runbook/first-project-walkthrough.md index 89da895..5231564 100644 --- a/docs/runbook/first-project-walkthrough.md +++ b/docs/runbook/first-project-walkthrough.md @@ -102,7 +102,7 @@ Then read what it made before doing anything else: ```sh cd demo-app git log --oneline # one commit, "chore: scaffold project" -cat mise.toml # config_roots: apps/web, apps/api, docs +cat mise.toml # [monorepo] config_roots = apps/web, apps/api, docs # [vars] database = "postgres", cache = "redis" ls .github/workflows # five call sites grep -l database compose*.yaml # all three: compose.yaml, .dev.yaml, .test.yaml @@ -133,6 +133,16 @@ mv /tmp/env-aside apps/api/.env Expect: still pass. A failure here is a real finding — it means a check depends on a file that is not committed, and CI will fail where you succeeded. +Which files those are depends on the adapter, and this is the step where +guessing costs you a red pull request. Move aside everything the app's +`.gitignore` excludes that some task writes, not just the two named above. +For `laravel-inertia` that is `apps/app/resources/js/actions`, +`apps/app/resources/js/routes` and `apps/app/public/build` — the first two +are written by the vite build's wayfinder plugin and compiled against by +`types:check`, the third is the vite manifest, without which every test that +renders an inertia page returns 500. Running the checks with those in place +is not the same experiment as CI runs. + A `nestjs` app's `check` and `build` also depend on a `prisma` task now (`prisma generate` first, when the project has a database). Moving its `.env` aside the same way still passes — `prisma generate` only parses the schema, @@ -200,12 +210,19 @@ gh pr create --fill gh pr checks --watch ``` +The first `gh pr checks --watch` usually exits 1 straight away with `no checks +reported on the 'feat/health' branch`. Nothing is wrong: GitHub has not +registered the check runs yet, and `--watch` does not wait for a first one to +appear. Give it twenty seconds and issue it again. + Expect: `CI / ci / ci (apps/api)` runs, because `apps/api` changed. Expect `CI / ci / changes` to skip roots that did not. Expect `commitlint` to run — -it only ever runs on a pull request. Expect all seven checks green, including -`security / codeql`, `security / gitleaks` and `security / zizmor` — on a -private repository those three each need something the workflow grants them -explicitly, so a failure there is a finding, not the normal state. +it only ever runs on a pull request. Expect every check green, including `security / codeql`, +`security / gitleaks` and `security / zizmor` — on a private repository those +three each need something the workflow grants them explicitly, so a failure +there is a finding, not the normal state. How many checks there are depends on +the project: six named jobs plus one `ci ()` for each config root the +commit touched, so this project has seven and an api-only one has six. The `gh api` call is required, not optional: without it Release Please cannot open its pull request later, and the failure appears several steps away from @@ -225,6 +242,13 @@ If that pull request's checks sit at `Action required`, the repository has no `RELEASE_APP_ID`/`RELEASE_APP_PRIVATE_KEY`; a pull request opened with `GITHUB_TOKEN` starts no workflow. Merging it directly still releases. +Those runs do not stay amber. Once the approval window passes, or once the +squash merge deletes the head branch, they end as `failure`, and `gh run view` +explains them with `This run likely failed because of a workflow file issue`. +There is no workflow file issue. Expect to be left with three red runs in the +history that the release did not depend on and that say nothing true about +your project. + ```sh gh pr merge --squash gh release list @@ -249,10 +273,17 @@ git status ``` Expect: `apps/worker` exists, `mise.toml` gained a config root, `ci.yml`'s -`roots:` gained an entry, and `lefthook.yml`, `pnpm-workspace.yaml` and -`pnpm-lock.yaml` also changed — the new app's own hooks, `allowBuilds` and -`minimumReleaseAgeExclude` entries, and dependencies. Expect the changes to -be left uncommitted for review. +`roots:` gained an entry, and `lefthook.yml` and `pnpm-lock.yaml` also +changed. Expect the changes to be left uncommitted for review. + +Two of those are conditional, and a diff that does not show them is not a +finding. `pnpm-workspace.yaml` changes only when the new app needs an +`allowBuilds` or `minimumReleaseAgeExclude` entry the project does not +already carry — adding a second `nestjs` app to a project that already has +one leaves it untouched, because `apps/*` already matched and prisma was +already decided. And `lefthook.yml` changes without gaining a hook: `nestjs` +contributes none, since prettier in the common layer already covers +typescript sources. The new application is wired to the database and cache this project already recorded, not asked again: `apps/worker/.env.example` names a `DATABASE_URL` diff --git a/lib/adapter.sh b/lib/adapter.sh index 07d38a8..7e26faf 100644 --- a/lib/adapter.sh +++ b/lib/adapter.sh @@ -67,7 +67,12 @@ merge_lefthook_fragment() { # cleaned up on both paths: under `set -e` a yq failure leaves immediately # and the file survives the run. Not a RETURN trap — that fires again in # callers, where $rendered is out of scope and the shell aborts. - if ! yq eval-all --inplace 'select(fileIndex==0) * select(fileIndex==1)' \ + # -P (block style) because yq propagates the *fragment's* style to the whole + # merged document, and an adapter contributing no hook ships `{}` — one flow + # mapping is enough to collapse all 26 lines of lefthook.yml onto a single + # line and replace every comment in it. The hooks still run; the file a + # human has to read and review does not survive. + if ! yq eval-all --inplace -P 'select(fileIndex==0) * select(fileIndex==1)' \ "${project}/lefthook.yml" "$rendered"; then rm -f "$rendered" die "failed to merge the lefthook fragment for ${rel}" diff --git a/lib/project.sh b/lib/project.sh index 27c9e99..cdf0b77 100644 --- a/lib/project.sh +++ b/lib/project.sh @@ -251,20 +251,17 @@ sync_standalone_build_policy() { "$file" "${project}/pnpm-workspace.yaml" } -# pnpm_install [ceiling] +# pnpm_install # pnpm reports its failures on stdout, so silencing the install leaves a `die` # that names the step and proves nothing — a CI failure here was unreadable # until this kept the output. Shown only on failure; a successful install is # still quiet. pnpm_install() { - local dir="$1" what="$2" ceiling="${3:-}" log status=0 + local dir="$1" what="$2" log status=0 log="$(mktemp)" ( cd "$dir" - # exported inside the subshell, not written as a `VAR=x cmd` prefix: the - # prefix has to be literal, and an expanded one is read as a command name. - [ -z "$ceiling" ] || export MISE_CEILING_PATHS="$ceiling" # --no-frozen-lockfile because pnpm turns frozen on by itself when CI=true, # and this install exists precisely to rewrite the lockfile a generator just # produced. Without it the step is a contradiction that only fails on a diff --git a/scaffold b/scaffold index e43fa0b..587ea15 100755 --- a/scaffold +++ b/scaffold @@ -141,6 +141,21 @@ cmd_new() { *) target="$(pwd)/${target}" ;; esac local name; name="$(basename "$target")" + + # `mise exec` walks upward for config and trusts what it finds — persistently, + # in ~/.local/state/mise, and it loads the file it trusts, which means running + # any `_.source` or `{{ exec(...) }}` in that config's [env]. Generate a + # project inside a tree you did not write and someone else's mise.toml is + # executed and permanently trusted, with none of the asking mise exists to do. + # `mise trust -C` and `mise lock -C` do not walk; only `mise exec` does. + # + # Set once here rather than threaded through the one function somebody + # remembered: this run makes mise subprocesses from pnpm_install, + # resolve_minimum_release_age, sync_workspace_lockfile and every adapter + # generator, and the guard has to cover all of them or it covers nothing. + MISE_CEILING_PATHS="$(dirname "$target")" + export MISE_CEILING_PATHS + local -a requested=() local database="" cache="" @@ -279,10 +294,7 @@ cmd_new() { # backs lefthook's commit-msg hook, which must work in a php-only # project (docs/decisions/0007). # - # MISE_CEILING_PATHS because `mise exec` walks upward and would otherwise - # trust a mise.toml above $target; `mise trust -C` does not cover that. - pnpm_install "$target" "installing the project root's own tooling dependencies" \ - "$(dirname "$target")" + pnpm_install "$target" "installing the project root's own tooling dependencies" # pnpm_install above installed at full strength (minimum-release-age=0), # so a violation among commitlint's own dependencies never surfaced — # the loop below resolves each app's lockfile but never the root's, and @@ -368,6 +380,12 @@ cmd_add() { { [ -f "${project}/mise.toml" ] && grep -q '^monorepo_root = true$' "${project}/mise.toml"; } \ || die "not a scaffold project (${project}): run 'scaffold add' from inside a project created by 'scaffold new'" + # Same reason as cmd_new's: every mise subprocess this run makes would + # otherwise walk above the project and permanently trust — and execute — + # whatever mise.toml it found there. + MISE_CEILING_PATHS="$(dirname "$project")" + export MISE_CEILING_PATHS + # Read back rather than asked for: an application added in month six is # wired to the database the project has had since month one, and a second # answer to a question already settled is a way for the two to disagree. diff --git a/services/mongodb/compose.dev.fragment.yaml b/services/mongodb/compose.dev.fragment.yaml index dafdaf5..1b0d973 100644 --- a/services/mongodb/compose.dev.fragment.yaml +++ b/services/mongodb/compose.dev.fragment.yaml @@ -4,4 +4,4 @@ services: # loopback, not 0.0.0.0: docker publishes to every interface by default # and its iptables rules bypass most host firewalls, so the bare form # exposes this password-is-"app" database to the whole local network. - - "127.0.0.1:27017:27017" + - '127.0.0.1:27017:27017' diff --git a/services/mongodb/compose.fragment.yaml b/services/mongodb/compose.fragment.yaml index 8e9f8df..ca95892 100644 --- a/services/mongodb/compose.fragment.yaml +++ b/services/mongodb/compose.fragment.yaml @@ -8,7 +8,7 @@ services: # mongosh, not a tcp probe: the port is open well before the server will # accept a query, and a check that passes early is worse than none — # depends_on releases the application into a database that is not ready. - test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"] + test: ['CMD', 'mongosh', '--quiet', '--eval', "db.adminCommand('ping')"] interval: 10s timeout: 5s retries: 5 diff --git a/services/mongodb/drivers/nest.sh b/services/mongodb/drivers/nest.sh index 939c653..621a473 100644 --- a/services/mongodb/drivers/nest.sh +++ b/services/mongodb/drivers/nest.sh @@ -1,8 +1,12 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # read by services/shared/nest.sh, sourced below PRISMA_PROVIDER="mongodb" -# directConnection, because prisma's mongodb provider otherwise expects a -# replica set and a single-node container is not one. +# authSource=admin is the parameter this actually depends on: the container +# creates the user in `admin`, and without it prisma authenticates against +# `app` and fails with a SCRAM error. directConnection is kept for prisma's +# mongodb provider, which otherwise expects a replica set — measured against a +# single-node container it changes nothing for `db push`, so treat it as +# unproven for anything but transactional writes. PRISMA_URL="mongodb://app:app@localhost:27017/app?authSource=admin&directConnection=true" # shellcheck source=/dev/null . "${SCAFFOLD_ROOT}/services/shared/nest.sh" diff --git a/services/mysql/compose.dev.fragment.yaml b/services/mysql/compose.dev.fragment.yaml index 346c008..a904b68 100644 --- a/services/mysql/compose.dev.fragment.yaml +++ b/services/mysql/compose.dev.fragment.yaml @@ -1,4 +1,4 @@ services: database: ports: - - "127.0.0.1:3306:3306" + - '127.0.0.1:3306:3306' diff --git a/services/mysql/compose.fragment.yaml b/services/mysql/compose.fragment.yaml index 08514b8..56fa8b2 100644 --- a/services/mysql/compose.fragment.yaml +++ b/services/mysql/compose.fragment.yaml @@ -6,7 +6,7 @@ services: MYSQL_PASSWORD: ${DB_PASSWORD:-app} MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-app} healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"] + test: ['CMD', 'mysqladmin', 'ping', '-h', '127.0.0.1', '--silent'] interval: 10s timeout: 5s retries: 5 diff --git a/services/postgres/compose.dev.fragment.yaml b/services/postgres/compose.dev.fragment.yaml index cfc631b..9a1e328 100644 --- a/services/postgres/compose.dev.fragment.yaml +++ b/services/postgres/compose.dev.fragment.yaml @@ -4,4 +4,4 @@ services: # loopback, not 0.0.0.0: docker publishes to every interface by default # and its iptables rules bypass most host firewalls, so the bare form # exposes this password-is-"app" database to the whole local network. - - "127.0.0.1:5432:5432" + - '127.0.0.1:5432:5432' diff --git a/services/postgres/compose.fragment.yaml b/services/postgres/compose.fragment.yaml index 3cb5323..8601efd 100644 --- a/services/postgres/compose.fragment.yaml +++ b/services/postgres/compose.fragment.yaml @@ -5,7 +5,7 @@ services: POSTGRES_USER: ${DB_USERNAME:-app} POSTGRES_PASSWORD: ${DB_PASSWORD:-app} healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-app}"] + test: ['CMD-SHELL', 'pg_isready -U ${DB_USERNAME:-app}'] interval: 10s timeout: 5s retries: 5 diff --git a/services/redis/compose.dev.fragment.yaml b/services/redis/compose.dev.fragment.yaml index 48c5326..63eab7b 100644 --- a/services/redis/compose.dev.fragment.yaml +++ b/services/redis/compose.dev.fragment.yaml @@ -4,4 +4,4 @@ services: # loopback, not 0.0.0.0: docker publishes to every interface by default # and its iptables rules bypass most host firewalls, so the bare form # exposes this password-is-"app" cache to the whole local network. - - "127.0.0.1:6379:6379" + - '127.0.0.1:6379:6379' diff --git a/services/redis/compose.fragment.yaml b/services/redis/compose.fragment.yaml index 8ffd6de..6d198db 100644 --- a/services/redis/compose.fragment.yaml +++ b/services/redis/compose.fragment.yaml @@ -1,8 +1,16 @@ services: cache: - command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:-app}"] + # block style, and it has to be block *here*: yq's merge keeps the style of + # its first input, so the prod fragment's own command inherits whatever + # this one uses. As a flow sequence the assembled prod command runs past + # prettier's print width, and the project's pre-commit hook then rewrote + # the compose.yaml scaffold had just handed it. + command: + - redis-server + - --requirepass + - '${REDIS_PASSWORD:-app}' healthcheck: - test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-app}", "ping"] + test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD:-app}', 'ping'] interval: 10s timeout: 5s retries: 5 diff --git a/services/redis/compose.prod.fragment.yaml b/services/redis/compose.prod.fragment.yaml index bc0ad1a..1a09ad9 100644 --- a/services/redis/compose.prod.fragment.yaml +++ b/services/redis/compose.prod.fragment.yaml @@ -1,8 +1,14 @@ services: cache: - command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:-changeme}", "--appendonly", "yes"] + # block style, matching the base fragment — see the comment there. + command: + - redis-server + - --requirepass + - ${REDIS_PASSWORD:-changeme} + - --appendonly + - 'yes' healthcheck: - test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-changeme}", "ping"] + test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD:-changeme}', 'ping'] interval: 10s timeout: 5s retries: 5 diff --git a/tests/helpers/setup.bash b/tests/helpers/setup.bash index bc3914e..d522d39 100644 --- a/tests/helpers/setup.bash +++ b/tests/helpers/setup.bash @@ -18,6 +18,18 @@ if [ -z "${GIT_CONFIG_GLOBAL:-}" ]; then git config --global user.email "tests@scaffold.invalid" fi +# mise records every config it trusts under its state directory, so a suite +# that generates projects in throwaway tmpdirs writes an entry per run into +# the developer's real store and never removes it — it had grown past 7600 +# stale `tmp.*-demo` entries. Owned by the test, like GIT_CONFIG_GLOBAL above, +# which also makes the trust behaviour itself observable: the real store is +# pre-trusted on a runner, so a test asserting an untrusted parent could only +# ever skip there. +if [ -z "${MISE_STATE_DIR:-}" ]; then + MISE_STATE_DIR="${BATS_TEST_TMPDIR:-${BATS_SUITE_TMPDIR:-/tmp}}/mise-state" + export MISE_STATE_DIR +fi + # bats' `run` captures the output, so a bare `[ "$status" -eq 0 ]` reports the # line that failed and nothing about why. assert_ok() { diff --git a/tests/new-project.bats b/tests/new-project.bats index f3d50f7..34fd6b0 100644 --- a/tests/new-project.bats +++ b/tests/new-project.bats @@ -90,18 +90,20 @@ teardown() { @test "new does not trust a parent config it did not create" { printf 'monorepo_root = true\n\n[monorepo]\nconfig_roots = [\n "x",\n]\n' \ > "${WORKDIR}/mise.toml" - # mise trusts every config it finds when CI is set, and unsetting CI here does - # not undo it: the parent mise process already started under it, so a child - # cannot get the untrusted state back. This test therefore cannot hold its own - # precondition on a runner. Say so and skip, rather than fail on an - # environment fact that says nothing about scaffold. - run mise trust --show -C "$WORKDIR" - [[ "$output" == *"${WORKDIR}: untrusted"* ]] \ - || skip "environment pre-trusts configs (CI=${CI:-unset}); trust --show said: ${output}" - - scaffold new "$PROJECT" - - run mise trust --show -C "$PROJECT" + # No skip guard. There used to be one, for the true reason that `CI=true` + # makes mise trust every config it finds — but its only two outcomes were + # "skipped on a runner" and "failed everywhere else", so the property was + # never verified anywhere and the leak it guards against shipped. The state + # directory is the suite's own now (tests/helpers/setup), and CI is scrubbed + # from the run below, so the precondition holds in both environments. + run env -u CI -u MISE_YES -u GITHUB_ACTIONS mise trust --show -C "$WORKDIR" + [[ "$output" == *"${WORKDIR}: untrusted"* ]] || { + echo "precondition failed; trust --show reported:"; echo "$output"; false + } + + env -u CI -u MISE_YES -u GITHUB_ACTIONS scaffold new "$PROJECT" + + run env -u CI -u MISE_YES -u GITHUB_ACTIONS mise trust --show -C "$PROJECT" [[ "$output" == *"${WORKDIR}: untrusted"* ]] \ || { echo "trust --show reported:"; echo "$output"; false; } [[ "$output" == *"${PROJECT}: trusted"* ]]