Repository navigation
Conversation
wchilders-nvidia
left a comment
There was a problem hiding this comment.
First thanks for making progress on this; I opened #164 but haven't had time to make progress on it.
The comments attached I would use as possible agent feedback (possibly including some AGENTS.md and related updates?) or guiding principles to consider (I believe this is primarily LLM authored?).
I think what I'd actually like to see here after sitting with PR for a bit is to take a Unix-like approach to abstraction. Let's just move most of the pre-push.py logic to edg-check-policy and then have pre-push.py be a wrapper script that invokes edg-check-policy with the correct commit range and flags asking for an interactive review.
In that situation we don't need to try and wrangle effectively two different command line tools into having a similar textual-interface with common abstraction in a library ... we just have one command line tool, with one interface, and sometimes the hooks use it, sometimes CI uses it, and sometimes the user (human or agent) might use it directly. edg-check-file-changes still needs to exist as a lower level command-line tool in this model ... but that's not all that different from what we've done with other tools (having a higher level tool -- e.g., edg-docker-shell -- and a lower level tool -- e.g., edg-exec).
| for reference in tag_body.split(',') | ||
| ) | ||
|
|
||
| def commit_subject_error(subject: str) -> Optional[str]: |
There was a problem hiding this comment.
I'd prefer a name that implies some decision is being made vs a concrete action being taken.
Like check_for_subject_errors and then maybe return a List[str] just to future proof a bit?
| # and the policy CI job both read this one flag, so there is nothing else | ||
| # to change. | ||
| # | ||
| ENFORCE_COMMIT_SUBJECT_POLICY = False |
There was a problem hiding this comment.
I think I'd side with ... let's keep the policy and remove codifying indecision. We can always relax the rules later.
| Use `--strict` to also fail on the spelling findings that the workflow only | ||
| reports as warnings. See `.github/rulesets/README.md` for how the checks are | ||
| tied to branch protection. | ||
|
|
There was a problem hiding this comment.
I'm not sure we want this note here. Someone reading HACKING.md wouldn't even know what dev-init.py is yet (at this point in the file), wouldn't have git push --no-verify access to the main repository (I hope), and probably wouldn't care about the minutia of policy enforcement.
| ```sh | ||
| edg-check-policy --base-rev origin/main | ||
| ``` | ||
|
|
There was a problem hiding this comment.
This seems like a good section to have, but we probably need to make it more general (not just to the new policy check workflow). I can help write up some copy once we've decided precisely what we're enforcing.
| ) | ||
| ) | ||
|
|
||
| return findings |
There was a problem hiding this comment.
This is kind of an odd function; it's basically repacking the dict and it loses some of the decisions made in the existing pre-push.py for presentation.
For instance I think the overlong lines reporting (while now more general) is a bit of a regression. The previous code:
overlong_lines = file_results['overlong_lines']
if len(overlong_lines) != 0:
for line_no, (line_start, lines) in overlong_lines.items():
print(f" Line {line_no} too long:")
line_end = line_start + (len(lines) - 1)
print(f" {line_start} -> {line_end} ".center(79, '='))
for line in lines:
print(line, end = '')
print('=' * 79)
would actually print context in a window with = added to visually show what was too long, e.g.:
/*snip*/ =====
/*snip*/ this is too long
/*snip*/ =====
Co-authored-by: Cursor <cursoragent@cursor.com>
9c6d7dd to
ce68be2
Compare
|
Claude writes:
I'll also fold in the specific points: rename to Four things I'd like to confirm before I push the restructure, because each 1. Does
|
|
Quick comment, not a full pass review, noticed the sys.stdin update was removed, we probably need to make sure we're opening |
cb732b4 to
ce68be2
Compare
git supplies the refs being pushed on a pre-push hook's standard input,
so the checker cannot inherit that stream and still read the user's
answers to the interactive prompts. Reopening sys.stdin inside the
checker rebound only the Python level object and left file descriptor 0
pointing at git's pipe, and it also required a general purpose tool to
know how git invokes hooks. Open the terminal in the hook instead and
pass it as the child's standard input, which fixes the descriptor for the
whole process tree and keeps that knowledge in the one place that needs
it.
Pushing with no terminal attached previously raised an unhandled OSError
from open('/dev/tty'), so such a push died with a traceback rather than
reporting anything. Decide what it should do instead of leaving it to
chance: when there is nowhere to ask a question or nowhere to read the
answer, fall back to the accounting used when nobody is watching, which
is what CI already does. Advisory findings are then reported without
blocking and enforced ones still fail, so a push from a GUI client or a
script reaches the same verdict as the pull request it turns into. This
also settles the case of a push whose output is redirected, which counted
an error without trying to prompt.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Good catch, and you're right about the fix even though the update survived: Opening the terminal at process launch is the better answer regardless, for
So the hook now opens This also closed a latent crash that predates the restructure. Opening Verified by feeding the hook a real ref list on stdin under a pty, the way because the first ref line is consumed as an answer and the rest of the Fixing the crash forced a decision about what a terminal-less push should The reasoning, in case you'd have drawn the line elsewhere:
The test is Verified across the four cases: a push from a terminal still prompts and Happy to reverse this if you'd rather a terminal-less push be refused |
Today the project's commit and coding-style policies live entirely in a
client-side
pre-pushhook. That hook is good at telling you about problemsearly, but it cannot enforce anything: it only exists if you ran
dev-init.py,git push --no-verifyskips it, and it never runs for workdone through the GitHub web UI. Neither of the two working copies used while
preparing this PR had it installed.
This PR moves the enforcement of those rules to a GitHub Actions job, while
keeping the hook as fast local feedback. Both now read the same rules from
one shared library, so they cannot drift apart.
Important
Merging this PR enforces nothing on its own. It makes a check exist
and go green or red. A repository admin has to mark that check as required
afterwards. See What a maintainer has to do by hand.
Background: why not a server-side hook?
The request that prompted this was for "server-side scripts" to enforce
commit restrictions, and the objection was that those need GitHub
Enterprise. That objection is half right, and the conclusion drawn from it
was wrong.
True server-side
pre-receivehooks are GitHub Enterprise Server only.But they are not the mechanism this project needs. A required status
check gives the same guarantee — it runs code the project controls, on
GitHub's infrastructure, and a contributor cannot skip it — and rulesets,
required status checks, required reviews and
CODEOWNERSare all free onpublic repositories.
The one capability genuinely out of reach on a standard account is push
rulesets (restricting file paths, file extensions, or file sizes at push
time), which need an organization on GitHub Team or Enterprise Cloud. If
anyone specifically wants "block pushes containing file X", that has to be a
CI check instead.
What was actually in place before this PR
.git-hooks/pre-push.py. Nopre-commit, nocommit-msg.Default Branch Protection(id24309424), containing onlydeletionandnon_fast_forward.mainwere allowed.CODEOWNERSFour workflows already ran on
pull_request, but because no check wasrequired, a red X blocked nothing.
So yes: opening a regular pull request bypassed every policy check. It was
in fact worse than that, because a direct push to
mainbypassed them too.The three categories, and where the overlap bites
It is worth being precise about this, because the middle case is the one
that causes confusion.
1. Things that are purely files in the repo. Workflows, the hook, the
checker code. Fully version controlled, and this PR delivers them outright.
2. Things that only exist in GitHub's settings. Rulesets and branch
protection. GitHub does not read these from the repository. An admin has
to apply them. This is the unavoidable click-click part.
3. The overlap — and it cuts both ways.
nothing until its job name is listed as a required status check in a
ruleset (category 2).
CODEOWNERSis the mirror image: the file is version controlled, but itgates nothing until a ruleset turns on
require_code_owner_review(category 2).
To get as close to file-based management as GitHub allows, this PR commits
the intended ruleset as JSON at
.github/rulesets/default-branch-protection.json, with the exactgh api --inputcommand to apply it.Note
That JSON file is documentation plus a payload, not configuration GitHub
reads. Editing it changes nothing until someone applies it. Its value is
that a change to project enforcement gets proposed, reviewed and recorded
like a code change, instead of appearing silently in a settings page with
no history and no discussion.
What changed
dev_tools/pylibs/edgpolicy/__init__.pydev_tools/bin/edg-check-policy.github/workflows/policy-check.yml.github/rulesets/default-branch-protection.json.github/rulesets/README.md.github/CODEOWNERS.git-hooks/pre-push.pyedgpolicy.dev_tools/typecheck-python.shCONTRIBUTING.md,HACKING.mdedgpolicyis almost entirely a move, not a rewrite. The excluded pathprefixes come verbatim from the hook, the 79-column limit and all the file
checks come from
edg-check-file-changes, and the commit subject rule is thehook's regex. There are exactly three deliberate behavioural changes, all
called out below: the subject rule no longer accepts an empty
[], deletedfiles are no longer checked, and binary files are skipped.
CODEOWNERSships entirely commented out because the@edgcpp/*teams donot exist yet and GitHub reports an invalid
CODEOWNERSfile if they arereferenced. Uncomment as the teams are created.
Running it yourself
Flags:
--head-rev,--skip-commits,--skip-files,--strict,--no-docker.Design decision: what blocks, and what only warns
CI has no human to answer a prompt, so every check the hook treated as
"warn and ask y/n" had to be classified as blocking or advisory. Rather
than guess, I measured the ten most recent commits:
[GH #213][GH #197][][][][][][]#endif/#else/ closing-comment / repeated-word-typo findings were zeroacross all ten.
The mechanical checks fire 0–3 times on a normal commit and are enforceable.
aspellfires 1–52 times on every commit, because there is no projectdictionary and it flags ordinary identifiers. Dogfooding this PR produced
142 spelling warnings (
aspell,bool,cwd,stdout,tempfile, …) andzero real violations.
Blocking: lines over 79 columns;
#endif/#elsewithout a comment;closing-comment errors; repeated-word typos.
Advisory (reported, does not fail): new misspellings; spell-check
unavailable; commit subject, see below.
--strictpromotes everything to blocking for anyone who wants it locally.A project
aspelldictionary would be the natural follow-up that letsspelling become blocking.
Design decision: the commit subject rule is advisory for now
The old regex was:
The inner group can repeat zero times, so a literal empty
[]satisfiedit. That is not theoretical: a third of recent subjects end in
[], andcommit
5ecc08269c("Accept bracketed GH #XYZ as a valid commit message[]") made it deliberate. The requirement that commits reference an issuewas, in practice, unenforced.
The new rule requires at least one real reference:
Fix thing [GH #213]Fix thing [EDGcpfe/29032]Fix bug [EDGcpfe/29055,EDGcpfe/28999]Fix bug [GH #1, GH #2]Mixed [nope] and [GH #7]...valid commit message []No tag at allBad ref [GH 213]Comma junk [,,]Note
Picking up the maintainer comment that "we could also drop the requirement
that non-issue linked commits need an empty
[]attached to them" — thischeck is wired up as a warning, not an error, pending that discussion.
Re-enabling it is a one-line change, and both the hook and the CI job read
that single flag:
With the flag
Falsethe CI job reports the subject as a warning and thehook asks for confirmation; with it
Truethe CI job fails and the hookaborts the push. Verified both ways.
Open question for reviewers: if the
[]requirement is formallydropped, should a subject with no tag at all stay a warning, or should the
check only complain about malformed references such as
[GH 213]? Theawkward case is
[WIP], which is indistinguishable from a typo'd referencewithout heuristics, which is why this PR keeps one severity for the whole
check rather than splitting it.
Edge cases found and handled
Binary files crashed the checker.
edg-check-file-changesdecodesaspelloutput as UTF-8, so handing it an image raisesUnicodeDecodeErrorpart way through. The exception is caught and the file is dropped from the
results, so the run still reports success — a silent skip, with an alarming
Python traceback in the log. This is not hypothetical: the repo tracks four
binary files outside
tests/(doc/source/_static/doc-logo-{dark,light}.png,dev_tools/services/acknowledg/client.png, andclient/public/favicon.ico),so updating the doc logo would have triggered it.
collect_change_specnow skips blobs that contain a NUL byte or do notdecode as UTF-8. Verified: adding a 20 KB binary alongside a markdown file
now checks the markdown, skips the binary, and emits no traceback.
Silent skips are now visible. Because the checker drops a file from its
output if checking it raised, a missing result meant "unchecked", not
"passed". The CLI now diffs the requested files against the returned ones
and warns about any difference.
Deleted files are no longer checked.
--diff-filter=dwas added;previously a deletion was checked as an empty file.
Rebases and stale base branches. The CLI resolves the merge base of
--base-revand--head-revrather than diffing against the base tip, so abranch that has fallen behind
main, or has mergedmainin, is not blamedfor other people's commits.
Annotation safety. Findings can quote a line from the diff, so all line
breaks (
\nand\r) are flattened before being written as a workflowcommand.
Testing
flagging three overlong lines in
cmake/wrapper.cmake.edgpolicymodule (fixed); now passes with 142 spelling warnings.ENFORCE_COMMIT_SUBJECT_POLICYverified in both positions, for boththe CLI and the hook.
pre-pushhook driven through a pty: exits 0, checks 10files, groups prompts per finding kind per file as before.
GITHUB_ACTIONS=1produces well-formed::error/::warningannotations.
python:3.6-slimwithmypy==0.971.dev_toolsC++ helpers compile withg++ -std=c++14.Commit and file policymatches therequired_status_checkscontext.Warning
The workflow has never run on GitHub, because the branch did not exist
upstream. This PR is its own first smoke test. The step most likely to
need a tweak is the
cmakebuild of thedev_toolshelpers onubuntu-latest.Why the job runs on stock
ubuntu-latestrather thanedgcpp/ci-runner:the file checks need
aspell, which the CI image does not carry(
ci-runnerisbuild-baseplusnodejs;aspell-enis only in thedev-envimage).What a maintainer has to do by hand
Nothing here changes repository settings, and merging this alone enforces
nothing new.
Merge this PR and let
Policy Checkrun once. GitHub only offers astatus check by name after it has been seen at least once.
Apply the ruleset, as a repository admin:
gh api repos/edgcpp/compiler/rulesets --jq '.[] | "\(.id)\t\(.name)"' gh api --method PUT repos/edgcpp/compiler/rulesets/24309424 \ --input .github/rulesets/default-branch-protection.jsonOr in the UI, under Settings → Rules → Rulesets → Default Branch
Protection, keeping
DeletionandNon-fast-forwardticked and adding:Commit and file policytypecheck-python (3.6)typecheck-python (3.14)Also untick Settings → General → Pull Requests → Allow merge commits.
allowed_merge_methodsgoverns the merge button; unticking therepository option closes the gap properly. A
required_linear_historyrule is the stricter alternative.
Decide on bypass actors. The JSON ships with
bypass_actors: [], sothe rules apply to admins too. If maintainers need an escape hatch, add
them there rather than disabling the rule, so the exception is visible.
(Optional) Settings → Actions → General → Fork pull request
workflows. The default, "Require approval for first-time
contributors", is the right one; just be aware it means the policy check
does not report on a brand new contributor's PR until a maintainer
approves the run.
(Optional, later) Create the
@edgcpp/*teams, uncomment the matchinglines in
.github/CODEOWNERS, and enable code owner review.Checks deliberately not required
Build & Test CIandStrict Build Checkrun on every PR but are not inthe required list. The test legs carry a 360 minute timeout and the Windows
build pulls an external SoftFloat checkout, so making them blocking is a
scheduling and flakiness decision for the maintainers rather than a policy
one. They can be added later by job name, for example
Test edg_x86_64orLinux GCC debug.Pre-existing issues found but deliberately left alone
These are all visible from the code this PR touches, but fixing them would
change what is checked, which does not belong in a PR about where it is
checked. Happy to file them as issues.
edg-check-file-changes,_file_check_config():elif suffix == 'txt':is missing a dot. No suffix is ever'txt', so.txtfiles fallthrough to the
elsebranch and do get line-length checked, which lookslike the opposite of the intent.
CRLF costs a column. The 79-column check does
len(line.rstrip('\n').expandtabs()), so a line committed with CRLFcounts its carriage return. A Windows-authored file with exactly 79
columns would be reported at 80.
rstrip('\r\n')would fix it. Notracked file currently hits this.
The hook ignores its stdin.
pre-pushis handed<local ref> <local sha> <remote ref> <remote sha>lines on stdin, butcheck_commits()walksgit logfromHEAD, so pushing a branch thatis not the current checkout is not inspected correctly.
confirm_okay()is inverted for non-interactive use. When stdout isnot a TTY it records an error instead of prompting, which makes scripted
pushes stricter than interactive ones. This also means an advisory
finding can block a non-interactive push.