Skip to content

feat: fallow CI gate#1440

Open
kawikabader wants to merge 35 commits into
nextfrom
feat/fallow-ci-gate
Open

feat: fallow CI gate#1440
kawikabader wants to merge 35 commits into
nextfrom
feat/fallow-ci-gate

Conversation

@kawikabader

@kawikabader kawikabader commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Pull requests now run a Fallow gate that catches dead code, unused dependencies, duplication, and complexity creep before they land. The gate is changeset-scoped: it fails only on issues a PR introduces. The ~340 pre-existing findings (mostly plugin-architecture false positives from dynamic loading, generator templates, and 316 plugin entry points) register as inherited and never block a PR until someone edits the file that carries them. No baseline file to maintain — the codebase drains gradually as edit traffic flows through it.

Every PR also gets a sticky health-score comment, so the repo-wide grade is visible and trends upward as touched files get cleaned. The gasket-plugin-https changes here are a worked example of that drain-on-edit model: a real package taken to zero findings while passing through the gate.

How the gate works

fallow audit runs new-only attribution against the PR base: it diffs the changeset, marks each finding introduced or inherited, and gates only on introduced error-severity findings. Verified against next — a PR reports dead_code_introduced: 0, dead_code_inherited: 151, verdict pass.

Three non-obvious things the workflow gets right:

  • fetch-depth: 0 — the default shallow clone lacks the base ref, so --base can't resolve.
  • origin/-prefixed basegithub.base_ref is the bare name next, which doesn't resolve in a CI checkout; only origin/next exists. Passed via env: so a branch name can never be read as shell.
  • Exit code is the gatefallow audit exits 0 (clean / warns only), 1 (introduced error-severity → fail), or 2 (runtime error → fail loudly). No JSON parsing.

Health-score comment

A second, non-gating step posts one sticky comment per PR, updated in place on every push. It renders the letter grade with a status emoji and a penalty breakdown table — non-zero penalties only, largest first — so reviewers see what's costing points repo-wide and watch the grade climb as cleanup lands.

  • if: always() + || true — reports even when the gate fails; a health hiccup never flips the job outcome.
  • Sticky via a hidden <!-- fallow-health-score --> marker: find-and-PATCH the existing comment, else POST a new one.
  • No-ops safely on fork PRs (read-only GITHUB_TOKEN); uses pull_request, never pull_request_target.

False positives

Suppressed at the source, closest to the cause:

Case Mechanism
One-off (dynamic require, intentional unused export) inline // fallow-ignore-next-line <issue-type>
Whole directory (generator/) ignorePatterns in .fallowrc.json
Reference-only dep (@gasket/plugin-metadata) ignoreDependencies in .fallowrc.json
Recurring shape .fallowrc.json over scattered inline comments

CONTRIBUTING.md documents the decision tree plus a manual fallow fix cleanup loop for draining existing debt on demand.

gasket-plugin-https cleanup

The first package run through the gate, as a reference for the drain-on-edit pattern:

  • Dead dependency dropped; @gasket/plugin-metadata false positive ignored at source.
  • Server-logging helpers extracted; lint warnings (loose Function, bare statements) cleared.
  • Internal types moved to internal.d.ts; terminus opts and startServer typed concretely against the public ActionHandler.
  • Tests parameterized and branches covered to 100%, with an 80% coverage floor added to vitest.config.js (lib/**/*.js only — index.d.ts has no runtime to exercise).

Notes

  • fallow added as a root dev-dependency (^2.91.0, caret per repo convention).
  • Unused deps removed across create-gasket-app, gasket-plugin-docs, gasket-plugin-swagger, gasket-react-intl, and others.
  • Known tradeoff: pnpm install's postinstall runs a full monorepo build on each gate run (same baseline as ci.yml). Acceptable for now; revisitable with --ignore-scripts if fallow proves it doesn't need build artifacts.

Test plan

Validate by opening throwaway PRs against the workflow:

  • PR adding an unused export in a touched file → gate fails, finding in the log.
  • PR touching only clean files → passes despite the ~340 repo-wide findings.
  • PR with a broken base ref → fails loudly (exit 2), not a silent pass.
  • Health comment appears once per PR and updates in place across pushes.

@kawikabader kawikabader requested a review from a team as a code owner June 9, 2026 21:15
@changeset-bot

changeset-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ad82e28

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new CI quality gate using the fallow tool to detect newly introduced dead code, unused dependencies, duplication, and complexity regressions, while avoiding failures on pre-existing (inherited) findings.

Changes:

  • Add fallow as a root dev dependency (and update the lockfile accordingly).
  • Introduce a new GitHub Actions workflow to run fallow audit on pull requests using base-branch attribution.
  • Document how to use and suppress fallow findings in CONTRIBUTING.md.

Reviewed changes

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

File Description
pnpm-lock.yaml Adds fallow and its platform-specific optional CLI packages to the lockfile.
package.json Adds a fallow script and fallow devDependency to the repo root.
CONTRIBUTING.md Documents the new gate, suppression directives, and local/manual cleanup commands.
.github/workflows/fallow.yml New PR workflow that installs deps and runs fallow audit --base origin/<base> to gate on introduced findings.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@godaddy godaddy deleted a comment from github-actions Bot Jun 10, 2026
@godaddy godaddy deleted a comment from github-actions Bot Jun 10, 2026
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

🌱 Fallow Health: 🟢 B · 77.1 / 100

What is costing points, repo-wide:

Check Penalty
Unit size −10.0
Duplication −10.0
Coupling −1.1
Circular dependencies −0.9
Dead files −0.5
Dead exports −0.4

Repo-wide score for trend visibility — not a merge gate. Watch the grade climb as PRs clean up touched files. Updated each push (ad82e28).

@godaddy godaddy deleted a comment from github-actions Bot Jun 10, 2026
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Fallow audit report

Found 1 finding.

Details
Severity Rule Location Description
major fallow/high-crap-score scripts/fallow-health-comment.js:26 'render' has CRAP score 56.0 (threshold: 30.0, cyclomatic 7)

Generated by fallow.

- pin fallow-rs/fallow to commit SHA (holds write token)
- extract inline node -e to scripts/fallow-health-comment.js, drop dead GITHUB_OUTPUT write
- scope sticky-comment lookup to bot author; skip on find failure (no dup comment)
- drop redundant actions/cache step (setup-node cache:pnpm covers it)
- correct portSuffix number-branch comment (unreachable for https/http2, not a no-op)

it('adds the beforeShutdown lifecycle', () => {
const handler: Hook<'beforeShutdown'> = async (gasket) => {
// await shutdownTheThing();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did this example code comment get caught because it looks like code?

Instead of leaving this with an empty body, could we drop a simple non-code sentence comment?

Something like:

// perform shutdown logic


it('adds the onShutdown lifecycle', () => {
const handler: Hook<'onShutdown'> = async (gasket) => {
// await cleanupStuff();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same a previous comment


on:
workflow_dispatch:
pull_request:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's limit fallow to the next branch to limit churn on main right now

Suggested change
pull_request:
pull_request:
branches:
- next

Comment thread .github/workflows/fallow.yml Outdated
`Watch the grade climb as PRs clean up touched files. Updated each push (\`${sha}\`).</sub>`
].join("\n");
}
fs.writeFileSync("fallow-comment.md", md + "\n");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To be safe, gitignore this temp fallow-comment.md file, or perhaps write it to the .fallow/ directory which you've already gitignored.

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.

4 participants