Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 65 additions & 10 deletions scripts/git/create-branch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,72 @@
set -euo pipefail

# codex-os-managed
if [[ $# -lt 2 ]]; then
echo "usage: $0 <type> <task description>"
exit 2
#
# Promoted from AssistSupport, which was the only repository whose version
# validated the branch type, bounded the slug, and reused an existing branch
# instead of failing. The one thing changed on the way up is the base: that
# version hardcoded origin/master, which is right for AssistSupport and wrong
# for most of the portfolio, so the default branch is resolved instead.
#
# Usage: create-branch.sh "task summary" [type] [base]

task="${1:-}"
kind="${2:-feat}"
base="${3:-}"

types='feat|fix|chore|refactor|docs|test|perf|ci|spike|hotfix'

if [[ -z "$task" ]]; then
echo "Usage: $0 \"task summary\" [${types//|/|}] [base]"
exit 1
fi

if ! [[ "$kind" =~ ^($types)$ ]]; then
echo "Invalid branch type: $kind"
echo "Expected one of: ${types//|/, }"
exit 1
fi

branch_type="$1"
shift
# Resolve the base branch rather than assuming a name. origin/HEAD is the
# repository's own answer; the named fallbacks cover a clone that never fetched
# it, and the final fallback is the current commit, which always exists.
resolve_base() {
local ref
if ref="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)"; then
echo "$ref"
return
fi
for ref in origin/main origin/master main master; do
if git rev-parse --verify --quiet "$ref" >/dev/null; then
echo "$ref"
return
fi
done
echo "HEAD"
}

task="$*"
slug="$(echo "$task" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g; s/-+/-/g')"
branch="codex/${branch_type}/${slug}"
# A branch name is a path, so an unbounded slug can exceed the filesystem's
# limit on a single component and fail at checkout rather than at validation.
slug="$(echo "$task" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')"
slug="${slug:0:48}"
slug="${slug%-}"

git switch -c "$branch"
echo "$branch"
if [[ -z "$slug" ]]; then
echo "Task summary produced an empty slug: $task"
exit 1
fi

branch="codex/${kind}/${slug}"

git fetch origin --quiet 2>/dev/null || true
[[ -n "$base" ]] || base="$(resolve_base)"

# Reusing the branch when it already exists is the difference between resuming
# a task and getting a fatal error partway through one.
if git show-ref --verify --quiet "refs/heads/$branch"; then
git checkout --quiet "$branch"
echo "Resumed branch: $branch"
else
git checkout --quiet -b "$branch" "$base"
echo "Created branch: $branch (from $base)"
fi
18 changes: 16 additions & 2 deletions scripts/git/guard-atomic.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,29 @@
set -euo pipefail

# codex-os-managed
#
# An intentional exception has to be expressible. Without one, the only way past
# this guard is to bypass the hook that runs it, which switches off the branch,
# generated-file, large-file and secret guards at the same time. AssistSupport
# was the only repository in the portfolio carrying this escape hatch.
if [[ "${GIT_GUARD_ALLOW_LARGE_COMMIT:-0}" == "1" ]]; then
echo "Atomicity check skipped: GIT_GUARD_ALLOW_LARGE_COMMIT=1."
exit 0
fi

max_files="${GIT_GUARD_MAX_FILES:-25}"
count="$(git diff --cached --name-only | sed '/^$/d' | wc -l | tr -d ' ')"

if (( count == 0 )); then
if ((count == 0)); then
echo "No staged files; skipping atomicity check."
exit 0
fi

if (( count > max_files )); then
# Deletions are counted deliberately. Removing forty files is a large commit
# whichever direction the change runs, and the escape hatch above is how to say
# that it is intentional.
if ((count > max_files)); then
echo "Too many staged files ($count > $max_files). Split into atomic commits."
echo "For a deliberately large commit, set GIT_GUARD_ALLOW_LARGE_COMMIT=1."
exit 1
fi
65 changes: 62 additions & 3 deletions scripts/git/guard-branch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,75 @@
set -euo pipefail

# codex-os-managed
#
# Consolidated from the ten variants this script had drifted into across the
# portfolio. Every rule below already existed in at least one of them; none is
# new. scripts/git/tests/test-guard-branch.sh pins each one.
#
# Accepted branch shapes:
# <type>/<slug> fix/null-pointer
# codex/<type>/<slug> codex/fix/null-pointer
# <agent>/<task-slug> cc/rebuild-index, for peer agents

typed_pattern='^(codex/)?(feat|fix|chore|refactor|docs|test|perf|ci|spike|hotfix)/[a-z0-9]+(-[a-z0-9]+)*$'
peer_pattern='^(codex|cc)/[a-z0-9]+(-[a-z0-9]+)+$'
Comment on lines +15 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep accepted branch names aligned with PR validation

When a developer uses either newly accepted form, such as fix/foo or cc/rebuild-index, this guard allows commits, but the branch-name job in .github/workflows/git-hygiene.yml lines 29-41 still accepts only codex/<type>/<slug>. Every PR from these branches will therefore fail the repository's branch-name check despite passing the local guard; update the workflow pattern alongside this expansion or retain the stricter local pattern.

Useful? React with 👍 / 👎.


in_ci() { [[ "${CI:-}" == "true" || "${GITHUB_ACTIONS:-}" == "true" ]]; }

branch="$(git rev-parse --abbrev-ref HEAD)"
pattern='^codex/(feat|fix|chore|refactor|docs|test|perf|ci|spike|hotfix)/[a-z0-9]+(-[a-z0-9]+)*$'

# A CI checkout is usually detached, so the real branch name lives in the event
# environment rather than in HEAD. GITHUB_REF_NAME also carries tag names, so
# it is trusted only when the ref really is a branch. Outside CI these
# variables are ignored entirely: a stray value in a local shell should not be
# able to talk this guard into approving a name that is not checked out.
if in_ci; then
if [[ -n "${GITHUB_HEAD_REF:-}" ]]; then
branch="$GITHUB_HEAD_REF"
elif [[ "${GITHUB_REF_TYPE:-branch}" == "branch" && -n "${GITHUB_REF_NAME:-}" ]]; then
branch="$GITHUB_REF_NAME"
fi
fi

if [[ "$branch" == "HEAD" ]]; then
if in_ci; then
echo "Detached HEAD in CI; skipping branch-name enforcement."
exit 0
fi
echo "Detached HEAD is not allowed for local development."
echo "Check out a <type>/<slug> or codex/<type>/<slug> branch."
exit 1
fi

if [[ "$branch" == "main" || "$branch" == "master" ]]; then
if in_ci; then
echo "Protected branch $branch in a CI checkout; not local work."
exit 0
fi
# A verification run against a clean checkout of the default branch is not
# somebody editing main. At commit time something is always staged, so this
# cannot excuse a real commit.
if [[ -z "$(git status --porcelain)" ]]; then
echo "Clean $branch checkout; no direct work detected."
exit 0
fi
echo "Direct work on $branch is blocked."
exit 1
fi

if ! [[ "$branch" =~ $pattern ]]; then
# Automation names its own branches and cannot rename them to fit a convention.
if [[ "$branch" == dependabot/* ]]; then
echo "Dependabot automation branch; skipping branch-name enforcement."
exit 0
fi

if [[ "$branch" == release-please--branches--* ]]; then
echo "Release Please automation branch; skipping branch-name enforcement."
exit 0
fi

if ! [[ "$branch" =~ $typed_pattern || "$branch" =~ $peer_pattern ]]; then
echo "Invalid branch: $branch"
echo "Expected: codex/<type>/<slug>"
echo "Expected: <type>/<slug>, codex/<type>/<slug>, or <agent>/<task-slug>"
exit 1
fi
5 changes: 4 additions & 1 deletion scripts/git/guard-generated.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ set -euo pipefail

# codex-os-managed
forbidden='(^|/)(node_modules|dist|build|out|coverage|\.next|target)/'
if git diff --cached --name-only | grep -E "$forbidden" >/dev/null; then
# --diff-filter=d excludes deletions. Without it, staging the removal of build
# output that was committed by mistake trips this guard, so the only way to act
# on the message below is to bypass the hook that prints it.
if git diff --cached --name-only --diff-filter=d | grep -E "$forbidden" >/dev/null; then
echo "Generated artifacts are staged. Unstage them before commit."
exit 1
fi
12 changes: 8 additions & 4 deletions scripts/git/guard-large-files.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ set -euo pipefail
# codex-os-managed
max_bytes="${GIT_GUARD_MAX_BYTES:-2097152}"
fail=0
while IFS= read -r file; do
[[ -f "$file" ]] || continue
size=$(wc -c <"$file")
# -z emits NUL-separated paths verbatim. Without it git renders any path that
# is not plain ASCII in quoted escaped form, so a file named "café.txt" comes
# back as "caf\303\251.txt" and the cat-file lookup below fails with a fatal
# error. Under set -e that aborts the guard, and the commit is refused with a
# message about a path not existing rather than anything about file size.
while IFS= read -r -d '' file; do
size=$(git cat-file -s ":$file")
if (( size > max_bytes )); then
echo "Large file staged (>${max_bytes} bytes): $file"
fail=1
fi
done < <(git diff --cached --name-only --diff-filter=AM)
done < <(git diff --cached --name-only -z --diff-filter=AM)
exit $fail
20 changes: 20 additions & 0 deletions scripts/git/guard-no-main-push.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,23 @@ if [[ "$branch" == "main" || "$branch" == "master" ]]; then
echo "Pushing from $branch is blocked."
exit 1
fi

# The check above only knows which branch you are standing on. It does not see
# where the push is going, so `git push origin feature:main` passes it while
# writing straight to the protected branch. Git hands a pre-push hook one line
# per ref on stdin, "<local ref> <local sha> <remote ref> <remote sha>", which
# is the only place the destination appears. Read it and refuse by destination.
#
# Skip the read entirely on a terminal. Run by hand as
# `pnpm git:guard:no-main-push`, an unguarded read would sit waiting for input
# that never comes, turning a guard into a hang. Outside a pre-push hook the
# branch check above is the whole guard, which is the pre-existing behaviour.
if [[ ! -t 0 ]]; then
while IFS=' ' read -r _local_ref _local_sha remote_ref _remote_sha; do
[[ -z "$remote_ref" ]] && continue
if [[ "$remote_ref" =~ ^refs/heads/(main|master)$ ]]; then
echo "Push to protected branch (${remote_ref#refs/heads/}) is blocked."
exit 1
fi
done
fi
22 changes: 22 additions & 0 deletions scripts/git/guard-secrets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@ set -euo pipefail

# codex-os-managed
if ! command -v gitleaks >/dev/null 2>&1; then
# Three repositories reach this branch for real: their CI runs this guard
# through the verify bundle on a runner that never installs gitleaks, so
# refusing here would fail every pull request without scanning anything.
# Everywhere else the bundle is CI-gated or its calling workflow never runs,
# so this branch costs nothing. Both variable spellings are accepted because
# different repos in the portfolio checked different ones.
#
# The value is checked, not merely the variable's presence. Every system that
# reaches this branch for real sets exactly "true" (GitHub Actions, and GitLab
# for the one repo that has a pipeline). Testing presence instead would let
# CI=false, or a developer who exports CI for unrelated tooling, silently turn
# off the only secret check a local commit gets.
if [[ "${CI:-}" == "true" || "${GITHUB_ACTIONS:-}" == "true" ]]; then
echo "gitleaks not found in this CI job; skipping the local secret guard."
# Deliberately not claiming CI scanning covers this. In much of the
# portfolio the gitleaks workflow triggers on pull_request only, so a
# direct push to the default branch is scanned by nothing at all.
echo "Secret scanning in CI belongs to the git-hygiene workflow; confirm it runs on this event."
exit 0
fi
# Outside CI, refuse. A missing scanner is not a clean scan, and this hook is
# the only secret check a commit reaches when it never becomes a pull request.
echo "gitleaks not found. Install gitleaks to enforce secret scanning."
exit 1
fi
Expand Down
75 changes: 60 additions & 15 deletions scripts/git/propose-commit-message.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
// Promoted from AssistSupport, which was the only repository whose version
// derived a real scope from the staged paths and named the file it changed,
// instead of always proposing "update N files" against a guessed scope.
//
// Kept from the majority version: the output path, which 49 repositories use.
// Nothing anywhere reads either file, so the name is free, and churn is not.
import { execFileSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import path from "node:path";

const SUBJECT_LIMIT = 72;
const OUT = ".git/CODEX_COMMIT_MSG_PROPOSAL";

const staged = execFileSync(
"/usr/bin/git",
Expand All @@ -16,26 +26,61 @@ if (staged.length === 0) {
}

const lower = staged.map((f) => f.toLowerCase());
const hasDocs = lower.some((f) => f.endsWith(".md") || f.includes("docs/"));
const hasTests = lower.some((f) => f.includes("test") || f.includes("spec"));

// The most common top-level directory, which is a scope the reader can act on.
// A file at the repository root has no directory to name, and using its
// filename would produce chore(package.json): update package.json, so those
// count as "repo". A leading dot is dropped so .github reads as github.
const counts = new Map();
for (const file of staged) {
const top = file.includes("/") ? file.split("/")[0] : "repo";
counts.set(top, (counts.get(top) ?? 0) + 1);
}
const scope = [...counts.entries()]
.sort((a, b) => b[1] - a[1])[0][0]
.toLowerCase()
.replace(/^\.+/, "");

// every, not some: staging one Markdown file alongside source is a source
// change with a note attached, not a docs change.
const allDocs = lower.every((f) => f.endsWith(".md"));
// Anchored on path segments and filename markers. Matching the bare substring
// "test" also matches latest.ts and contest.js.
const hasTests = lower.some(
(f) => f.includes("/tests/") || f.includes(".test.") || f.includes(".spec."),
);
const hasCi = lower.some((f) => f.startsWith(".github/workflows/"));
const hasPerf = lower.some((f) => f.includes("/perf/") || f.includes("lighthouserc"));
const hasDeps = lower.some((f) => f.endsWith("package.json") || f.includes("lock"));
const hasPerf = lower.some(
(f) => f.includes("/perf/") || f.includes("lighthouserc"),
);
// Named explicitly rather than matching "lock" anywhere, which also matches
// blocklist.ts, while still covering the ecosystems this portfolio uses.
const LOCKFILES = [
"package.json",
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
"cargo.toml",
"cargo.lock",
"uv.lock",
"poetry.lock",
];
const hasDeps = lower.some((f) => LOCKFILES.includes(path.basename(f)));

let type = "feat";
if (hasCi) type = "ci";
if (allDocs) type = "docs";
else if (hasCi) type = "ci";
else if (hasPerf) type = "perf";
else if (hasTests) type = "test";
else if (hasDocs) type = "docs";
else if (hasDeps) type = "build";
else if (hasDeps) type = "chore";

const focus =
staged.length === 1
? `update ${path.basename(staged[0])}`
: `update ${staged.length} files for ${scope} changes`;

let scope = "repo";
if (lower.some((f) => f.includes("scripts/git/"))) scope = "git";
else if (lower.some((f) => f.includes("scripts/perf/"))) scope = "perf";
else if (lower.some((f) => f.startsWith(".github/"))) scope = "ci";
const summary = `${type}(${scope}): ${focus}`.slice(0, SUBJECT_LIMIT);

const summary = `${type}(${scope}): update ${staged.length} file${staged.length === 1 ? "" : "s"}`;
const out = `.git/CODEX_COMMIT_MSG_PROPOSAL`;
writeFileSync(out, `${summary}\n`);
writeFileSync(OUT, `${summary}\n`);
console.log(summary);
console.log(`written: ${out}`);
console.log(`written: ${OUT}`);
5 changes: 3 additions & 2 deletions scripts/git/session-resume.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
set -euo pipefail

# codex-os-managed
if [[ ! -f .git/CODEX_LAST_WIP ]]; then
marker_path="$(git rev-parse --git-path CODEX_LAST_WIP)"
if [[ ! -f "$marker_path" ]]; then
echo "No saved WIP tag found."
exit 1
fi

tag="$(cat .git/CODEX_LAST_WIP)"
tag="$(cat "$marker_path")"
git stash list | grep -F "$tag" >/dev/null
git stash apply "stash^{/$tag}"
echo "Restored WIP: $tag"
3 changes: 2 additions & 1 deletion scripts/git/session-save.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ set -euo pipefail

# codex-os-managed
tag="codex-wip/$(git rev-parse --abbrev-ref HEAD)/$(date +%Y%m%d-%H%M%S)"
marker_path="$(git rev-parse --git-path CODEX_LAST_WIP)"
git stash push -u -m "$tag"
echo "$tag" > .git/CODEX_LAST_WIP
echo "$tag" > "$marker_path"
echo "Saved WIP: $tag"
Loading