Skip to content

Internal Guard - #74

Closed
aaronsmulktis wants to merge 8 commits into
facebookresearch:mainfrom
aaronsmulktis:aaronsmulktis/internal-guard
Closed

Internal Guard#74
aaronsmulktis wants to merge 8 commits into
facebookresearch:mainfrom
aaronsmulktis:aaronsmulktis/internal-guard

Conversation

@aaronsmulktis

@aaronsmulktis aaronsmulktis commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Three-layer guard against pushing internals into the public:

  1. Ruleset rejects int/* branches at push time (manual config in Github)
  2. Pre-push hook rejects int/* branches, internal commits & internal strings (except w/--no-verify)
  3. PR Workflow rejects int/* head branches, internal-only paths, Internal-Only commits

Defining internals:

  • Branches: int, int/<name>, internal, internal/<name>. Not int-x or internal-x.
  • Paths: Set in .gitattributes, .githooks/patterns.txt or just anything in internal/
  • Strings: One regex per line in .githooks/patterns.txt. Hook scans patch text and commit messages not already public

Configuration

  1. add internal git remote
  2. ./scripts/setup-dev.sh installs
  3. ./scripts/setup-dev.sh --check status, exits if broken/missing guard

Admins only:

  1. Import .github/rulesets/block-internal-refs.json under Settings -> Rules -> Rulesets on public repo

Notes:

  1. pre-push exits immediately if clone has no internal remote and no int/* branches. Anyone who clones facebookresearch/OpenApps never sees the hook fire, is never warned, is never asked for setup.
  2. ShipIt was considered, but ruled out due to the public repo being the source-of-truth, its archive anyway

Definitions

  1. Source of Truth - In this context, we're referring to a merge base. So if a repo accept writes (ie oss, int, fork), and there is no shared Source of Truth, the divergence is unreconcilable by any automated means. Identifying the SoT repo is to determine the merge base which can be safetly targeted from PRs.
  2. Merge Driver - An optional preference on which repo's content to prefer during a PR merge across repos. Similar to -X ours/theirs merge strategy but configured at the file-level.

aaronsmulktis and others added 2 commits August 5, 2026 11:42
- theme design vars
- set default layout
- set default theme
- use design vars in todo app
- update README
- add theme & layout to appserver
- retain appearance for legacy usage
- add theme & layout to registry and session
- 44 tests passing
@aaronsmulktis
aaronsmulktis requested review from marksibrahim and a lite review from Copilot August 19, 2026 01:34
@aaronsmulktis aaronsmulktis self-assigned this Aug 19, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 19, 2026

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 multi-layer “internal content guard” to prevent internal-only branches/commits/strings from being pushed or merged into the public repository, combining local git hooks, a CI workflow, and an importable GitHub ruleset, plus developer setup/docs.

Changes:

  • Adds per-clone setup script to configure core.hooksPath and a custom merge driver for divergent internal/public paths.
  • Introduces local hooks (pre-push, commit-msg) and a merge driver (keep-internal) to block/internalize content and reduce merge conflicts.
  • Adds a PR workflow and a server-side ruleset JSON to detect/block internal refs and internal-only artifacts.

Reviewed changes

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

Show a summary per file
File Description
scripts/setup-dev.sh New setup script to install/check hooksPath and merge driver configuration.
CONTRIBUTING.md Documents required dev setup step and reserved branch namespace.
.github/workflows/guard-internal.yml PR-time checks to reject internal branch names, internal-only paths, and Internal-Only commits.
.github/rulesets/block-internal-refs.json Importable ruleset to reject pushes of internal branch refs server-side.
.githooks/README.md Documents the hook system, patterns, and limitations.
.githooks/pre-push Implements local push guard with ref-name, lineage, and content-based checks.
.githooks/keep-internal Merge driver to keep internal version of divergent files and emit drift notices.
.githooks/commit-msg Stamps commits made on internal branches with an Internal-Only trailer.

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

Comment thread scripts/setup-dev.sh Outdated
Comment thread .githooks/pre-push
Comment on lines +76 to +80
if ! git remote get-url internal >/dev/null 2>&1 && [ -z "$(
git for-each-ref --count=1 --format='%(refname)' \
refs/heads/int refs/remotes/internal/int 2>/dev/null)" ]; then
exit 0
fi
Comment thread .githooks/pre-push
Comment on lines +120 to +122
internal_refs=$(git for-each-ref --format='%(refname)' \
refs/heads/int refs/remotes/internal/int 2>/dev/null | tr '\n' ' ')

Comment thread .githooks/pre-push

# Everything below here is a push to a PUBLIC remote.

tmpdir=$(mktemp -d) || die "could not create temp dir"
Comment on lines +35 to +38
case "$HEAD_REF" in
int|int/*)
echo "::error::Head branch '$HEAD_REF' is an internal branch and cannot be merged to a public branch."
exit 1
Comment on lines +25 to +33
"conditions": {
"ref_name": {
"include": [
"refs/heads/int",
"refs/heads/int/**"
],
"exclude": []
}
},
Comment thread .githooks/pre-push
Comment on lines +190 to +197
case "$local_ref" in
refs/heads/int|refs/heads/int/*)
banner "$local_ref" "$remote_ref"
printf ' internal branch pushed to a public remote\n' >&2
failed=1
continue
;;
esac
Comment thread .githooks/commit-msg
Comment on lines +20 to +23
case "$branch" in
int|int/*) ;;
*) exit 0 ;;
esac
Keeps internal-only work on int/* branches and out of the public remotes,
without prefixing filenames or maintaining two clones.

- .githooks/pre-push: when the destination is a public remote, refuses int/*
  refs, commits reachable from int/* that are not yet public, and content
  matching an internal pattern list. Inert in a clone that has neither an
  internal remote nor any int/* ref.
- .githooks/keep-internal: merge driver for files that deliberately differ at
  the same path between public and int/*. Keeps the internal copy and reports
  drift when public also changed that path.
- .githooks/commit-msg: stamps int/* commits with an Internal-Only trailer so
  they remain identifiable after a cherry-pick, which defeats the lineage
  check by producing a fresh SHA.
- scripts/setup-dev.sh: per-clone setup plus a --check mode. Required rather
  than automatic, because git does not let a repository install its own hooks
  at clone time.
- .github/workflows/guard-internal.yml: pull-request equivalents of the branch
  name, artifact, and trailer checks.
- .github/rulesets/block-internal-refs.json: importable ruleset rejecting
  int/* refs server-side. The only layer here that prevents rather than
  detects; the hook and the workflow are both bypassable.
The guard rejects int/* pushes and int/* pull requests, but nothing told an
outside contributor that the namespace was reserved or why, so the failure
would read as an unexplained CI error.

Explains the reservation in terms of what it buys -- a file at the same path
on both sides, so cluster configs stop being renamed -- and states plainly
that normal contributions never touch any of it.

Also adds internal/ to the workflow's internal-only path list, alongside
.gitattributes and .githooks/patterns.txt.
Merging a public-safe branch into int/* before pushing it to a public remote
makes its commits reachable from int/* , so check 2 puts them in the taint set
and refuses the push that would have made them public. The old message sent
people the wrong way -- 'internal work belongs on int/*' -- when the commits
were public-safe all along, and the only reading left was --no-verify.

The check is correct; the recovery is an ordering rule. Push the source branch
public first and the commits subtract out of the taint set. Hook now says so at
the point of failure, and .githooks/README.md states the rule up front, since
the order is easier to remember than the recovery.

Hit this while setting up int/main, which is how it surfaced.
setup-dev.sh keyed 'clone shape' on the internal remote alone, so a clone
holding int/* branches without that remote was told 'public-only clone --
nothing internal can leak from here'. That is the reassuring message in the one
shape where every configured remote is public and internal commits are sitting
right there.

.githooks/pre-push already arms on either condition -- remote OR int/* refs --
precisely because removing the remote should not disarm it. Match that here, and
name the mismatched shape as its own warning rather than folding it into either
of the other two. The merge-driver check moves onto the same combined condition,
since a clone with int/* branches needs the driver whether or not the remote is
wired up.

Found by running --check from int/main in a clone with no internal remote.
int/* was the only guarded namespace, so a branch named internal/my-work got
nothing: no ref block, no Internal-Only stamp, no taint, no ruleset. That is the
likeliest wrong guess now that the repo has an internal/ directory, and an
unguarded near-miss is worse than a second guarded spelling.

Adds internal and internal/* to all four layers, plus the two ref queries in
pre-push and the one in setup-dev.sh. int/* stays canonical -- the trunk is
still int/main and patterns.txt is still read from there. internal/* is blocked,
not promoted.

int-* and internal-* are deliberately left alone. They are not namespaces, and
git's ref matching only breaks at slashes, so widening to them would have to be
a prefix match -- which would catch aaronsmulktis/internal-guard, the branch
carrying this very change. Verified refs/heads/internal matches internal/work
but not internal-guard or internal2.

The namespace list now appears in five files. pre-push holds the canonical
comment and the others point at it.
@aaronsmulktis
aaronsmulktis force-pushed the aaronsmulktis/internal-guard branch from aaaa4be to 10c5a63 Compare August 24, 2026 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants