Repository navigation
feat: unified config package with typed tool sections #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
b8f1f4e
build: add the package, lint, test, mutation, release and CI tooling
Mearman 2197434
feat: add typed section descriptors and the shared layout section
Mearman 3f57b51
feat: resolve a tool's section from the unified file or a standalone …
Mearman be56a28
feat: report config sections that no installed tool owns, with a command
Mearman f388a6b
build: emit the command line entry as an ES module only
Mearman fb7245b
docs: describe authoring, resolution, section authoring and the docto…
Mearman 3e692e9
ci: run the packaged-install check from a script that also runs the c…
Mearman f183635
docs: use a neutral tool in the authoring example
Mearman 03bbb53
fix: refuse section descriptors that widen or collide at the type level
Mearman 0515119
fix: fail on a config file that exports no default or a null default
Mearman 6a16cb4
fix: fail when the directory to read config from does not exist
Mearman 1b5e031
fix: namespace the standalone file and read .mts and .cts config files
Mearman 5f84329
fix: accept doctor --help, fail on a missing command, and say when th…
Mearman 8b42b21
fix: let the project's own manifest own sections and tolerate unknown…
Mearman 1e0777f
feat: re-export ConfigValidationError and ConfigValidationIssue
Mearman 537afd9
fix: declare the runtime dependencies as caret ranges
Mearman 403b0a9
test: pin the zod version of the packaged-install check to the reposi…
Mearman 4ece2aa
docs: state what the type checker verifies and what waits for the load
Mearman fb055d9
refactor: keep config values out of the layout messages and document …
Mearman 7480922
test: stop the alias test re-evaluating the source, which hid the cov…
Mearman b02b499
fix: reject a malformed package.json in doctor instead of ignoring it
Mearman 0cb0b09
test: kill the surviving mutants of the section, layout and command code
Mearman 7c57cbe
ci: run mutation testing on manual dispatch only
Mearman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| workflow_dispatch: | ||
|
|
||
| # Cancels a superseded run when a branch or PR gets pushed to again. Scoped away from main so a merge landing mid-release never kills the release job, which runs solely on a push to main. | ||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| determine-runner: | ||
| uses: ./.github/workflows/determine-runner.yml | ||
|
|
||
| commitlint: | ||
| name: Commitlint | ||
| needs: determine-runner | ||
| runs-on: ${{ fromJson(needs.determine-runner.outputs.selected-runner) }} | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| with: | ||
| fetch-depth: 0 | ||
| - uses: pnpm/action-setup@v6 | ||
| - uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: '22' | ||
| cache: pnpm | ||
| # Not wagoid/commitlint-github-action: it is a Docker container action, and the self-hosted fleet has no Docker daemon. | ||
| - run: pnpm install --frozen-lockfile | ||
| - name: Lint the commits of a pull request | ||
| if: github.event_name == 'pull_request' | ||
| run: pnpm exec commitlint --from "$BASE_SHA" --to "$HEAD_SHA" | ||
| env: | ||
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | ||
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | ||
| - name: Lint the last commit of a push | ||
| if: github.event_name != 'pull_request' | ||
| run: pnpm exec commitlint --last | ||
|
|
||
| lint: | ||
| name: Lint | ||
| needs: determine-runner | ||
| runs-on: ${{ fromJson(needs.determine-runner.outputs.selected-runner) }} | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - uses: pnpm/action-setup@v6 | ||
| - uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: '22' | ||
| cache: pnpm | ||
| - uses: actions/cache@v6 | ||
| with: | ||
| path: .eslintcache | ||
| # eslint.config.ts and pnpm-lock.yaml both invalidate the cache: a rule change or a dependency bump can change lint results even for files whose own content did not change. | ||
| key: eslintcache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.ts') }} | ||
| restore-keys: eslintcache-${{ runner.os }}- | ||
| - run: pnpm install --frozen-lockfile | ||
| # The check-only script: `pnpm lint` fixes in place, so it would exit 0 on a fixable violation that never reaches the commit. | ||
| - run: pnpm lint:check | ||
|
|
||
| typecheck: | ||
| name: Typecheck | ||
| needs: determine-runner | ||
| runs-on: ${{ fromJson(needs.determine-runner.outputs.selected-runner) }} | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - uses: pnpm/action-setup@v6 | ||
| - uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: '22' | ||
| cache: pnpm | ||
| - run: pnpm install --frozen-lockfile | ||
| - run: pnpm typecheck | ||
| - run: pnpm test | ||
| - run: pnpm build | ||
| - run: pnpm exec publint | ||
| - run: pnpm exec attw --pack | ||
|
|
||
| mutation: | ||
| name: Mutation | ||
| # Manual only: run it from the Actions tab (workflow_dispatch) when a change warrants a full mutation pass. It is not a required check and does not gate a release. | ||
| if: github.event_name == 'workflow_dispatch' | ||
| needs: determine-runner | ||
| runs-on: ${{ fromJson(needs.determine-runner.outputs.selected-runner) }} | ||
| timeout-minutes: 60 | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - uses: pnpm/action-setup@v6 | ||
| - uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: '22' | ||
| cache: pnpm | ||
| - run: pnpm install --frozen-lockfile | ||
| # Content-diff based (Stryker's IncrementalDiffer text-diffs stored against current source, never hash, mtime or git ref), so restoring from an unrelated prior run is always safe: a mismatch only means bigger diffs and more reruns, never a wrongly skipped mutant. | ||
| - name: Restore Stryker incremental result cache | ||
| uses: actions/cache/restore@v6 | ||
| with: | ||
| path: reports/stryker-incremental.json | ||
| key: stryker-incremental-${{ github.run_id }} | ||
| restore-keys: | | ||
| stryker-incremental- | ||
| # Stryker only diffs mutated and test files, so a change outside that scope (config, fixture data, an env var) is invisible to the incremental differ. --force on push to main (never on a PR) rebuilds the incremental file from a known-clean baseline so that blind spot cannot compound. | ||
| - run: pnpm test:mutation${{ github.ref == 'refs/heads/main' && ' --force' || '' }} | ||
| - name: Save Stryker incremental result cache | ||
| if: always() | ||
| uses: actions/cache/save@v6 | ||
| with: | ||
| path: reports/stryker-incremental.json | ||
| key: stryker-incremental-${{ github.run_id }} | ||
|
|
||
| # The packed tarball, not the source tree, is what consumers install: this proves the published files, exports map, command line entry and peer dependency work in a clean project on every Node line the package supports and on both cosmiconfig majors. The toolchain needs Node 22, so the package is built and packed on 22 and the consumer check runs on the Node under test. | ||
| package: | ||
| name: Package (Node ${{ matrix.node }}, cosmiconfig ${{ matrix.cosmiconfig }}) | ||
| needs: determine-runner | ||
| runs-on: ${{ fromJson(needs.determine-runner.outputs.selected-runner) }} | ||
| timeout-minutes: 15 | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - { node: '20', cosmiconfig: '9' } | ||
| - { node: '22', cosmiconfig: '9' } | ||
| - { node: '22', cosmiconfig: '10' } | ||
| - { node: '24', cosmiconfig: '9' } | ||
| - { node: '24', cosmiconfig: '10' } | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| - uses: pnpm/action-setup@v6 | ||
| - uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: '22' | ||
| cache: pnpm | ||
| - run: pnpm install --frozen-lockfile | ||
| - run: pnpm build | ||
| - run: pnpm pack --pack-destination "$RUNNER_TEMP/pack" | ||
| - uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: ${{ matrix.node }} | ||
| - name: Install the tarball into a scratch project and load sections through it, as ESM and as CommonJS | ||
| run: bash test/package/install-check.sh "$RUNNER_TEMP/pack" "${{ matrix.cosmiconfig }}" "$RUNNER_TEMP/scratch" | ||
|
|
||
| required-checks: | ||
| name: Required Checks | ||
| needs: [determine-runner, commitlint, lint, typecheck, package] | ||
| # Always runs, even if a dependency failed, so it reports that failure itself rather than being skipped: a required check that is silently absent blocks a PR indefinitely instead of failing it visibly. | ||
| if: always() | ||
| runs-on: ${{ fromJson(needs.determine-runner.outputs.selected-runner) }} | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - name: Fail if any required job failed or was cancelled | ||
| run: | | ||
| if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then | ||
| echo "One or more required jobs did not succeed:" | ||
| echo '${{ toJSON(needs) }}' | ||
| exit 1 | ||
| fi | ||
|
|
||
| release: | ||
| name: Release | ||
| needs: [commitlint, lint, typecheck, package] | ||
| if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') | ||
| # Deliberately a GitHub-hosted runner, not the resolved one: npm trusted publishing only works from GitHub-hosted runners, so the OIDC token exchange cannot happen on the self-hosted fleet or Blacksmith. | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| permissions: | ||
| contents: write # to push the release commit and tag and create the GitHub Release | ||
| issues: write # to comment on released issues | ||
| pull-requests: write # to comment on released pull requests | ||
| id-token: write # OIDC identity for npm trusted publishing and provenance (no NPM_TOKEN) | ||
| steps: | ||
| - uses: actions/checkout@v7 | ||
| with: | ||
| # semantic-release analyses the full commit history since the last release. | ||
| fetch-depth: 0 | ||
| - uses: pnpm/action-setup@v6 | ||
| - uses: actions/setup-node@v7 | ||
| with: | ||
| node-version: '22' | ||
| cache: pnpm | ||
| # registry-url is deliberately absent: setup-node would otherwise write an .npmrc _authToken line that wins over the OIDC token exchange, breaking trusted publishing. | ||
| - run: pnpm install --frozen-lockfile | ||
| - name: Upgrade npm for OIDC trusted publishing (needs npm CLI >=11.5.1) | ||
| run: npm install -g npm@latest | ||
| - name: Release | ||
| # HUSKY=0 so the commit-msg hook never fires against the automated release commit. | ||
| run: HUSKY=0 pnpm exec semantic-release | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| # Blanked, not omitted: an inherited NPM_TOKEN or NODE_AUTH_TOKEN would otherwise be used in preference to the OIDC exchange. | ||
| NPM_TOKEN: '' | ||
| NODE_AUTH_TOKEN: '' | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| name: Determine runner | ||
|
|
||
| # The one runner-selection job every job in ci.yml waits on (via workflow_call) instead of hard-coding a runner: ExaDev's self-hosted fleet first, falling back to Blacksmith if the fleet's heartbeat is stale. The decision is made by ExaDev/runner-fallback-action, which reads a secret gist refreshed by the fleet's heartbeat (exadev/github-runner); no token, no secret, no per-repo setup. The action also forces the fallback for Dependabot and fork PRs so untrusted code never runs on the fleet. | ||
| # | ||
| # The resolver itself runs on Blacksmith's smallest runner, not ubuntu-latest and not the fleet. This job decides whether the fleet can be used at all, so it must sit on infrastructure independent of both: on the fleet, a fleet-wide outage deadlocks every workflow at this job because the fallback can never engage; on ubuntu-latest, the org's recurring GitHub-hosted billing block stops the job before it starts. Blacksmith is billed outside GitHub's spending limit. | ||
| # | ||
| # The fallback is Blacksmith rather than the action's ubuntu-latest default for the same reason: routing dependent jobs to a pool that cannot start would recreate the deadlock one hop out. The preferred label is the action's default (exadev-runners, the fleet's shared runs-on label), so no `preferred-labels` override is set and routing returns to the fleet whenever the heartbeat is current. | ||
| # | ||
| # The action is pinned to a commit SHA, not a branch: b774d51 is the commit that introduced the secret-gist design. Bump deliberately when a newer commit is wanted. | ||
| on: | ||
| workflow_call: | ||
| outputs: | ||
| selected-runner: | ||
| description: "A JSON array of runner labels for fromJson at the call site." | ||
| value: ${{ jobs.determine-runner.outputs.selected-runner }} | ||
| preferred-available: | ||
| description: "Whether the preferred (fleet) runner was reachable." | ||
| value: ${{ jobs.determine-runner.outputs.preferred-available }} | ||
|
|
||
| jobs: | ||
| determine-runner: | ||
| runs-on: blacksmith-2vcpu-ubuntu-2404 | ||
| timeout-minutes: 5 | ||
| outputs: | ||
| selected-runner: ${{ steps.fallback.outputs.selected-runner || '["blacksmith-2vcpu-ubuntu-2404"]' }} | ||
| preferred-available: ${{ steps.fallback.outputs.preferred-available || 'false' }} | ||
| steps: | ||
| - id: fallback | ||
| uses: ExaDev/runner-fallback-action@b774d51ff5360bf53705b2eb34ff019afd4d7f07 | ||
| with: | ||
| fallback-labels: blacksmith-2vcpu-ubuntu-2404 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| npx commitlint --edit "$1" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| npx lint-staged |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| npx pnpm run typecheck && npx pnpm run test && npx pnpm run build |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| README.md |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| README.md |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛡️ Codex Security Review · Automatically triggered
When fork PR workflows are approved or auto-run and the shared fleet is available, keep every pull-request job on a hosted runner.
pull_requestevaluates the PR-controlled workflow revision, so an attacker can change the repository-localdetermine-runner.ymlor thisruns-onexpression to select the self-hosted fleet; the fork-aware action is mutable before scheduling. CI then checks out the PR and executespnpm installand repository scripts, yielding arbitrary code execution on that host. The read-only token and hosted release job limit direct publication but do not protect the runner. Enforce hosted runners for PRs or an immutable gate plus runner-group restrictions.Useful? React with 👍 / 👎.