Skip to content

Ship the engine as a package (Nix flake first); stop vendoring scripts (#2) - #4

Merged
williamdemeo merged 6 commits into
mainfrom
2-ship-engine-as-a-package
Aug 16, 2026
Merged

williamdemeo merged 6 commits into
mainfrom
2-ship-engine-as-a-package

Conversation

@williamdemeo

Copy link
Copy Markdown
Owner

Closes #2.

What changes

The engine gets exactly one home. Consumer projects hold data (docs/GITHUB_PROJECT.md, a thin Makefile, workflows) and reference the engine through a channel; they never carry a copy that can decay.

  • flake.nix now ships the engine: packages.gh-project-engine (CLIs gh-project-{populate,update,lint} with python3 + gh on their wrapped PATH; version = scripts/VERSION), apps for nix run .#{populate,update,update-check,lint}, and checks.engine-tests running the full offline suite inside the sandbox — nix flake check executes all 115 tests.
  • Makefile resolves the engine by channel: local scripts/ (this repo, or a not-yet-detached template copy) → gh-project-* CLIs on PATH (Nix dev shell / future pipx) → GHPROJECT_DIR checkout → a channel-listing error. make engine-mode prints the resolution.
  • make init is the one-time detach for projects created from the template: deletes the copied engine, installs templates/consumer/flake.nix (github-project as a pinned input: dev shell with the CLIs + re-exported apps), prints next steps. Prompted; INIT_YES=1 for automation. Every make target works identically before and after.
  • Workflows self-adapt: both ci.yml and project-plan-update.yml fetch an engine checkout via a hashFiles-guarded step exactly when the tree has no local copy, so the same YAML serves the engine repo, pre-init copies, and detached consumers — no init-time patching. ci.yml also gains an engine-repo-only nix job (nix flake check); the setup-python jobs stay Nix-free so consumers are never Nix-bound.
  • README documents the three consumption channels and drops the tarball re-vendor one-liner; CLAUDE.md and the project-setup skill follow. scripts/VERSION → 0.2.0 (distribution model change; engine behavior unchanged).

Verified

  • nix build + all four apps run; nix flake check green (115 sandbox tests).
  • Full detach lifecycle on a throwaway copy, now pinned by test_makefile_stub.py (suite 111 → 115): init removes exactly the engine files, is idempotent, and post-init all three channels resolve (including the acceptance case: a consumer with zero engine files running lint through its own flake's dev shell and re-exported apps, pinned by its own flake.lock).
  • Shellcheck/ruff clean; scripts/ci/project-plan-update.sh's no-engine path reports "could not run", never fake drift.

Decisions taken (flagging for review)

  • make init over prose for the post-create cleanup (the issue left this open): it's testable, one command, and the test suite pins it.
  • _utils stays inside the engine package for now, per the issue's own interim call; a dedicated utilities repo remains future work.
  • The engine-fetch steps in consumer CI check out the engine at main (unpinned) — Nix consumers get real pinning via their lock; a pinned-ref variable can follow if wanted.

Follow-up (after this merges)

  • agda-native-air PR #93 switches its project-* targets from GHPROJECT_DIR to the flake input (a commit on that open branch completes its acceptance criterion; it locks against main once this lands).
  • Tag v0.2.0.

AI-assisted development: Claude Fable 5 (Anthropic)

packages.gh-project-engine wraps the stdlib-only scripts as
gh-project-{populate,update,lint} CLIs with python3 and gh on their
runtime PATH; version comes from scripts/VERSION.  apps expose
nix run .#{populate,update,update-check,lint}; checks.engine-tests runs
the offline suite inside the sandbox (the recorded fake gh makes that
possible), so nix flake check executes all 111 tests.  The devShell is
unchanged.

Consumers take this repository as a flake input and pin it in their
flake.lock; upgrading is a deliberate nix flake update github-project.

AI-assisted: Claude Fable 5 (Anthropic)
… flake (#2)

The Makefile now finds the engine through whichever channel is
available — local scripts/ (the engine repo itself, or a template copy
that has not yet detached), gh-project-* CLIs on PATH (a Nix dev shell
carrying the engine package, or a future pipx install), or a
GHPROJECT_DIR checkout — and fails with a channel-listing error when
none is present.  make engine-mode prints the resolution for debugging.

make init is the one-time detach step for projects created from the
template: it deletes the copied engine (scripts py files, _utils,
tests, VERSION; scripts/ci stays — the freshness workflow's shell lives
there), installs templates/consumer/flake.nix as the project's flake
(github-project as a pinned input: dev shell with the CLIs, re-exported
apps), and prints the next steps.  Prompted by default; INIT_YES=1 for
automation.  make test degrades to a pointer at the engine repo once
the local copy is gone.

Verified in a throwaway copy: init leaves only scripts/ci, all three
post-init channels resolve (missing-mode error text, checkout mode,
PATH mode), and the consumer flake's dev shell and re-exported apps run
lint with zero engine files in the tree, pinned by the consumer's own
flake.lock.

AI-assisted: Claude Fable 5 (Anthropic)
…init (#2)

scripts/ci/project-plan-update.sh resolves the engine through the same
channels as the Makefile (explicit UPDATE override, local scripts,
gh-project-update on PATH, GHPROJECT_DIR checkout) and reports 'could
not run' — never fake drift — when none is available.  Both workflows
gain a hashFiles-guarded step that fetches an engine checkout exactly
when the tree carries no local copy, so the same YAML serves the engine
repository, pre-init template copies, and detached consumers with no
init-time patching.  ci.yml also grows an engine-repo-only nix job
(install-nix + nix flake check, which builds the package and runs the
suite in the sandbox); the setup-python jobs stay Nix-free so consumers
are never Nix-bound.

test_makefile_stub.py pins the detach lifecycle on a throwaway copy of
the tree: init removes exactly the engine files (scripts/ci and the
plan survive), installs the consumer flake, drops lock+templates, is
idempotent; post-init the stub resolves checkout and PATH channels,
errors helpfully when none exists, and make test degrades to a pointer
at the engine repo.  Guarded to skip inside the Nix sandbox, where only
scripts/ is present.  Suite: 111 -> 115.

AI-assisted: Claude Fable 5 (Anthropic)
README: the quickstart gains the one-time 'make init' detach step; the
re-vendor section is replaced by the three consumption channels (flake
input with deliberate 'nix flake update github-project' upgrades;
GHPROJECT_DIR checkout for non-Nix use; installed CLI planned — the
Makefile already resolves gh-project-* from PATH, so that channel needs
no consumer-side change when it lands).  Layout and design notes now
state the engine is referenced, never copied.

CLAUDE.md replaces the re-vendor guidance with the single-home rule and
tells a session how to recognize a detached consumer tree.  The
project-setup skill gains the detach step.  scripts/VERSION - now also
the flake package version - bumps to 0.2.0 for the distribution-model
change (engine behavior itself is unchanged).

AI-assisted: Claude Fable 5 (Anthropic)
williamdemeo added a commit to formalverification/agda-native-air that referenced this pull request Aug 16, 2026
github-project's issue #2 shipped the engine as a flake package
(williamdemeo/github-project#4), so the GHPROJECT_DIR checkout wiring
this branch introduced is no longer the primary channel: the engine now
arrives as a flake input, pinned in flake.lock, and the project-*
targets run it through ghproject-{lint,update,update-check} apps this
flake re-exports.  GHPROJECT_DIR remains as an explicit escape hatch
(engine development, Nix-less machines).

The lock intentionally pins the engine PR's head revision (the input's
original URL already points at the repository default branch): after
williamdemeo/github-project#4 merges, a plain
'nix flake update github-project' moves the pin to main.

Verified: make project-lint and make project-update-check run the
pinned engine (plan lints clean, file current with GitHub), and the
GHPROJECT_DIR escape hatch still resolves.  One Make gotcha fixed en
route: '#' starts a comment even inside an assignment, so the flake
attrpaths are written '.\#ghproject-…'.

AI-assisted: Claude Fable 5 (Anthropic)
@williamdemeo

Copy link
Copy Markdown
Owner Author

The air side of the acceptance is done: formalverification/agda-native-air#93 now carries a commit switching its project-* targets to the flake input (re-exported ghproject-* apps, lock pinned to this PR's head, nix flake update github-project repoints to main after this merges). Verified end to end against air's real plan file.

Two environment-dependence bugs the local relaxed-sandbox nix hid:

- In a strict sandbox there is no /usr/bin/env, so the recorded fake
  gh's shebang died with an ENOENT that masqueraded as 'gh: No such
  file or directory'.  patchShebangs tests/fake_gh in the check phase
  rewrites it to the store python3.  Verified with a forced
  'nix build --option sandbox true': 115 tests, 4 skipped (the
  full-tree make tests, absent by design from the scripts-only src).
- Under 'make test' the stub tests' sub-makes inherit -w and print
  Entering/Leaving directory lines into the single-line outputs the
  assertions read; --no-print-directory on every test invocation.

AI-assisted: Claude Fable 5 (Anthropic)
williamdemeo added a commit to formalverification/agda-native-air that referenced this pull request Aug 16, 2026
The previously pinned revision's flake check failed on strictly
sandboxed nix (missing /usr/bin/env for the test fake's shebang), which
would have broken 'nix run .#ghproject-*' for any consumer building the
engine there.  Still to be repointed to main with
'nix flake update github-project' once williamdemeo/github-project#4
merges.

AI-assisted: Claude Fable 5 (Anthropic)
@williamdemeo
williamdemeo requested a balanced review from Copilot August 16, 2026 19:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Packages the roadmap engine via Nix and replaces vendored consumer copies with channel-based resolution.

Changes:

  • Adds the engine package, apps, checks, and consumer flake.
  • Introduces make init and engine-channel resolution.
  • Updates CI, documentation, tests, and versioning.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
flake.nix Defines the engine package, apps, and checks.
templates/consumer/flake.nix Provides consumer-side Nix integration.
Makefile Adds initialization and channel resolution.
scripts/VERSION Bumps the engine to 0.2.0.
scripts/tests/test_makefile_stub.py Tests detachment and engine modes.
scripts/ci/project-plan-update.sh Resolves the engine dynamically.
.github/workflows/ci.yml Adapts CI for detached consumers.
.github/workflows/project-plan-update.yml Fetches an engine for update jobs.
README.md Documents the new distribution model.
CLAUDE.md Updates repository guidance.
.claude/skills/project-setup/SKILL.md Adds engine detachment to setup.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +41 to +44
uses: actions/checkout@v4
with:
repository: williamdemeo/github-project
path: .github-project-engine

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — the sharpest finding of this review, and the write permissions make it a genuine supply-chain hazard rather than a determinism nit: an upstream push to main became unreviewed code execution with contents:write / pull-requests:write in every detached consumer. Fixed in cb42872: the fetch is now pinned, resolved in order — the github-project rev in the consumer's own flake.lock (so Nix consumers manage exactly one pin, upgraded by the same deliberate nix flake update github-project), the GHPROJECT_ENGINE_REF repository variable, then the default release tag baked into the workflow. Floating main is no longer reachable from either workflow.

Comment thread Makefile Outdated

populate-dry: guard-gh ## Preview what populate would create (no mutations)
$(PYTHON) scripts/gh_project_populate.py $(PLAN) $(FLAGS) --dry-run
populate-dry: guard-engine guard-gh ## Preview what populate would create (no mutations)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — the path channel's whole point is that the resolved CLI is self-contained (the Nix wrapper prefixes gh onto its own PATH), so probing the caller's ambient PATH produced a false negative for exactly the nix profile install mode the Makefile advertises. Fixed in cb42872: guard-gh now applies only in local/checkout modes, where bare python3 scripts genuinely need an ambient gh; in path mode the CLI's own runtime is authoritative. Verified with a deliberately gh-less PATH (shims + a bare venv): the guard no longer blocks, and execution proceeds into the CLI, which reports its own runtime errors.

Comment thread .github/workflows/ci.yml
Comment on lines +43 to +46
uses: actions/checkout@v4
with:
repository: williamdemeo/github-project
path: .github-project-engine

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — read-only permissions make this the lower-stakes sibling of the freshness-workflow finding, but the determinism argument stands on its own: a consumer commit must not change CI behavior because upstream moved. Fixed in cb42872 with the same pin resolution as there (consumer's flake.lock rev → GHPROJECT_ENGINE_REF variable → default release tag), so both workflows share one story and Nix consumers still have a single pin to manage.

Comment on lines +30 to +33
packages = [
pkgs.gnumake
pkgs.gh
github-project.packages.${system}.gh-project-engine

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — the primary acceptance artifact had only manual verification: nix flake check builds from src = ./scripts and the stub tests skip without the full tree, so nothing automated ever evaluated templates/consumer/flake.nix. Fixed in cb42872: the nix CI job now assembles a detached consumer (make init INIT_YES=1 on a copy), points its github-project input at the engine under test via --override-input path:$GITHUB_WORKSPACE, asserts the Makefile resolves the path channel inside nix develop, and runs lint through both the consumer's dev shell and its re-exported apps. The step script was executed verbatim locally before landing.

…gh guard, smoke-test the consumer flake

All four findings were legitimate:

- Both workflows' engine fetch was floating upstream main — in the
  freshness workflow with contents:write and pull-requests:write, so an
  upstream change became unreviewed code execution with mutation
  privileges in every detached consumer; in ci.yml it made consumer CI
  behavior change under an unchanged commit.  The fetch is now pinned,
  resolved in order: the github-project rev in the consumer's own
  flake.lock (one pin to manage for Nix consumers, upgraded by the same
  deliberate 'nix flake update github-project'), the
  GHPROJECT_ENGINE_REF repository variable, the default release tag.
- guard-gh probed the caller's ambient PATH even in path mode, failing
  a 'nix profile install' user whose wrapped CLI carries its own gh.
  The guard now applies only to local/checkout modes (bare python3
  scripts genuinely need ambient gh); verified with a gh-less PATH —
  execution reaches the CLI, whose own runtime is authoritative.
- templates/consumer/flake.nix had no automated coverage (the engine
  package builds from ./scripts, and the stub tests skip without the
  full tree).  The nix CI job now assembles a detached consumer,
  points its input at the engine under test via --override-input, and
  runs lint through the consumer's dev shell and re-exported apps;
  the step script was executed verbatim locally before landing.

AI-assisted: Claude Fable 5 (Anthropic)
@williamdemeo
williamdemeo merged commit 5e5b15e into main Aug 16, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ship engine as a package (Nix flake first); stop vendoring scripts with the template

2 participants