DEV Community

Blake Yang
Blake Yang

Posted on

Convert CONTRIBUTING.md Into an Executable Preflight Gate Before an OSS Patch Leaves Your Fork

A contributor spends a weekend on a null-dereference fix, opens the pull request on Monday morning, and watches CI fail on a rule that CONTRIBUTING.md stated in paragraph nine. The patch logic is correct, but the branch name lacks the expected prefix and the changelog entry is missing. Three review rounds later, the maintainer merges a change whose packaging cost more than its implementation.

That failure mode is rarely about code quality, and it is almost always about packaging. Contribution guidelines are written as prose for humans and enforced as machines somewhere else, so the gap gets paid for in review latency. The workflow below closes that gap with an executable preflight gate, a reproducible test step, and a narrow review role for free-tier models.

Why prose rules and CI checks drift apart

Most repositories accumulate contribution rules in three separate places, and none of them is authoritative. CONTRIBUTING.md explains intent, CI enforces a subset, and maintainers enforce the remainder from memory during review.

  • The document drifts after a tooling change, because nobody re-reads paragraph nine.
  • CI cannot check intent, so reviewers absorb the difference as unpaid coordination work.
  • Contributors learn the real rules only after a failed pull request, which is the most expensive classroom.

The practical fix is not stricter CI. It is a local gate that runs the rules the repository already claims to have, before a patch becomes other people's problem.

The snippets below are illustrative and were not run against a specific public repository; adapt paths and tool names to your target project before relying on them.

Step 1: Extract the rules into a reviewed YAML file

Convert prose into a small machine-readable file that a human approves once and CI can diff later. Keep every rule traceable to the sentence it came from, so a disagreement can be settled by reading the source document.

A free model tier handles the mechanical extraction well, because the task is reading and structuring, not deciding. The prompt below deliberately forbids inference, which is the failure mode that turns an extraction pass into invented policy.

You are extracting contribution rules from a repository document.
Return YAML only, with this schema:
  version: 1
  branch_pattern: <regex or null>
  required_when:
    - path: <file that must change>
      when_changed: [<glob>, ...]
Return a separate key `unresolved` for any sentence that is advisory rather than checkable.
For every rule, include `source_line`, the line number in the document.
Do not infer rules that are not written. Mark ambiguous rules with `needs_human: true`.
Enter fullscreen mode Exit fullscreen mode

The resulting file stays small on purpose. A gate with forty checks gets disabled within a week, while one with five honest checks survives contact with a real release cycle.

# .contrib-rules.yml — extracted from CONTRIBUTING.md, human-reviewed
version: 1
branch_pattern: '^(fix|feat|docs|chore)/[a-z0-9._-]+$'
required_when:
  - path: CHANGELOG.md
    when_changed: ['src/**/*.py', 'lib/**/*.js']
  - path: docs/
    when_changed: ['**/public_api.py']
unresolved:
  - "Prefer small pull requests."
Enter fullscreen mode Exit fullscreen mode

Step 2: Build the preflight gate

The gate has two jobs: reject packaging mistakes locally, and run the project's own test dialect exactly as the repository defines it. A shell wrapper handles ordering, while a small Python checker evaluates the file-level rules.

#!/usr/bin/env bash
# scripts/preflight.sh — run the repo's stated rules before pushing a patch
set -euo pipefail

BASE="${1:-origin/main}"
RULES=".contrib-rules.yml"

branch="$(git rev-parse --abbrev-ref HEAD)"
pattern="$(python3 -c "import yaml;print(yaml.safe_load(open('$RULES'))['branch_pattern'])")"

if [[ ! "$branch" =~ $pattern ]]; then
  echo "preflight: branch '$branch' does not match $pattern" >&2
  exit 1
fi

python3 scripts/preflight_rules.py "$BASE"
make fmt
make lint
make test
Enter fullscreen mode Exit fullscreen mode
#!/usr/bin/env python3
"""preflight_rules.py — verify file-level contribution rules locally."""
from __future__ import annotations

import fnmatch
import pathlib
import subprocess
import sys

import yaml

RULES = yaml.safe_load(pathlib.Path(".contrib-rules.yml").read_text())


def changed_files(base: str) -> list[str]:
    out = subprocess.run(
        ["git", "diff", "--name-only", f"{base}...HEAD"],
        capture_output=True,
        text=True,
        check=True,
    )
    return [line for line in out.stdout.splitlines() if line.strip()]


def check_required_files(changed: list[str]) -> list[str]:
    problems: list[str] = []
    for rule in RULES.get("required_when", []):
        triggers = rule.get("when_changed", [])
        touched = any(fnmatch.fnmatch(f, p) for f in changed for p in triggers)
        if touched and rule["path"] not in changed:
            problems.append(f"required file not updated: {rule['path']}")
    return problems


if __name__ == "__main__":
    base = sys.argv[1] if len(sys.argv) > 1 else "origin/main"
    issues = check_required_files(changed_files(base))
    for issue in issues:
        print(f"preflight: {issue}", file=sys.stderr)
    sys.exit(1 if issues else 0)
Enter fullscreen mode Exit fullscreen mode

Reproducing the failure in a clean checkout

A gate that only passes on a dirty working tree teaches nothing, so reproduce the failure once in a throwaway worktree. This isolates the patch from editor state, local build artifacts, and half-installed dependencies.

git worktree add ../oss-repro -b preflight-check origin/main
cd ../oss-repro
git cherry-pick <your-commit-sha>
bash scripts/preflight.sh
Enter fullscreen mode Exit fullscreen mode

If the gate passes in the clean worktree and fails in CI, the difference is environmental rather than logical, and the useful artifact becomes a pinned environment file instead of another code change.

Step 3: Use a free model tier as a second reviewer

Preflight catches mechanical rules, but it cannot notice that a diff touches a code path the rule file never anticipated. That is where a free model tier adds value, provided the review is constrained to the same rule file that the gate uses.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode provides free model access and a free server option, and the workflow below uses the free model access tier for the two reading-heavy steps.

Input A: RULES (the .contrib-rules.yml content)
Input B: DIFF (unified diff, no unrelated files)

List every rule from RULES that the diff may violate.
For each candidate, output: rule path, evidence line from the diff, confidence (low/medium/high),
and the exact question a human must answer.
Do not suggest preferences that are absent from RULES.
If no rule applies, return the single line: no rule-based findings.
Enter fullscreen mode Exit fullscreen mode

The prompt is intentionally boring, because an unconstrained reviewer produces stylistic noise that costs more to triage than it saves. Every finding is a question for a human, never an automatic change request.

Where the free server option fits

Some projects cannot run their test suite on a contributor laptop, because the suite needs containers, services, or a long wall-clock budget. In those cases the free server option can host the preflight and test run, so the gate executes in a consistent environment instead of a laptop that happens to have the right toolchain.

Availability limits, provisioning details, and whether the free tier covers a specific workload should be confirmed against the project's current documentation before a team depends on it. Treat the free path as a convenience for review and reproduction steps, not as a substitute for the project's own CI.

Decision table: what to automate, what to keep human

Task Delegate to a model? Human check required
Extract rules from CONTRIBUTING.md Yes, free tier is sufficient Every rule and its source line
Branch and file-naming checks No, run deterministically in preflight None beyond approving the rules
Test failure triage Yes, as a summarizer Reproduce the failure in a clean worktree
Writing the patch itself Optional, with a scoped prompt Full diff review plus test evidence
Changelog and docs wording Yes, as a first draft Accuracy of version and behavior claims
Security-sensitive changes No Maintainer review and threat modeling
Final merge decision Never Maintainer, always

Limitations and failure modes

  • Rule extraction can misread a conditional sentence, so needs_human markers matter more than tidy output.
  • Model review produces confident false positives, and each one consumes maintainer attention.
  • The rules file drifts from CONTRIBUTING.md unless a scheduled CI job re-extracts and diffs it.
  • A preflight gate hides failures only if contributors remember to run it, so wire it into a pre-push hook rather than a wiki page.
  • Free-tier availability is not a service guarantee, and long-running suites still need a fallback path.

Who should skip this workflow

Repositories that already expose one authoritative make check target do not need a parallel gate, because the rules are already executable. Projects with strict supply-chain requirements should keep model-assisted review out of the loop entirely, since prompts can leak unreleased code. Single-maintainer projects with low contribution volume usually gain more from a shorter CONTRIBUTING.md than from automation.

A short checklist before push

  1. Confirm which sentence in CONTRIBUTING.md each rule in the YAML file came from.
  2. Run the preflight script in a clean worktree, not in your editor's dirty tree.
  3. Restrict model review to rules that already exist, and convert every finding into a question.
  4. Re-extract the rules file when the underlying document changes, and review the diff.
  5. Revert any change that the gate cannot explain in one sentence.

The goal is not to automate contribution judgment; it is to stop paying review latency for rules that were written down months ago. Teams that want to try the free model access and free server path described above can evaluate it against their own repository rules before adopting any of it.

Top comments (0)