Skip to content
Closed
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to codex_yolo will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- Maintainers can draft a Keep a Changelog-style release section from an
explicit git ref range with `scripts/generate_changelog.sh`

## [1.1.0] - 2026-01-31

### Added - Product Perspective
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,29 @@ CODEX_VERBOSE=1 codex_yolo

For more examples and use cases, see [EXAMPLES.md](EXAMPLES.md).

## Maintainer Changelog Drafts

Maintainers can generate a draft changelog section directly from git commits with
`scripts/generate_changelog.sh`. The helper is intentionally scoped to
maintainer workflows: it prints markdown to stdout for manual review instead of
editing `CHANGELOG.md` or publishing a release automatically.

```bash
scripts/generate_changelog.sh --from 6d8555b --to HEAD
```

The `--from` ref is required because this repository does not currently use
release tags, so the script cannot infer a safe release boundary on its own.
`--to` defaults to `HEAD`.

```bash
scripts/generate_changelog.sh --from main~5
```

The generated markdown uses Keep a Changelog-style `Added`, `Changed`, `Fixed`,
and `Security` sections. Treat the output as a draft and review it before
copying entries into `CHANGELOG.md`.

## Login

The first run will prompt you to sign in. You can also log in explicitly:
Expand Down
42 changes: 41 additions & 1 deletion TECHNICAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ codex_yolo is a bash wrapper that runs OpenAI's Codex CLI in an isolated Docker
- Shell profile integration
- File deployment

**Changelog Synthesis** (`scripts/generate_changelog.sh`)
- Explicit git ref range parsing
- Commit subject normalization
- Deterministic changelog section bucketing

**Container** (`.codex_yolo.Dockerfile`, `.codex_yolo_entrypoint.sh`)
- Node.js 20 base image
- Codex CLI installation
Expand Down Expand Up @@ -78,6 +83,36 @@ Optional files downloaded separately:

**Rationale**: Ensures users get critical fixes while allowing graceful degradation for optional features. Backward compatible with v1.0.x auto-update logic.

### Changelog Draft Generation

The repository now includes a maintainer-only helper at
`scripts/generate_changelog.sh` for synthesizing a draft changelog section from
git history.

**Why explicit refs are required**:
- The repository does not currently have stable release tags to define implicit
changelog boundaries.
- Requiring `--from <ref>` keeps the output deterministic and prevents the tool
from guessing which commits belong to a release.
- `--to <ref>` defaults to `HEAD` so maintainers can draft a section for the
current branch without extra arguments.

**Bucketing rules**:
- `Added`: subjects with prefixes or keywords like `feat`, `add`, `implement`,
`support`, or `enable`
- `Fixed`: subjects with prefixes or keywords like `fix`, `bug`, `regression`,
`repair`, or `resolve`
- `Security`: subjects mentioning `security`, `vulnerability`, `cve`, `harden`,
or related auth/sanitization terms
- `Changed`: fallback bucket for everything else, including docs, chores,
refactors, and version housekeeping

**Normalization rules**:
- Collapse repeated whitespace and remove trailing periods
- Strip common conventional-commit prefixes before rendering
- Preserve pull request references such as `(#18)` when present
- Skip merge commit subjects to reduce duplicate release-note entries

## Code Organization

### File Structure
Expand Down Expand Up @@ -133,13 +168,18 @@ Location: `tests/integration_tests.sh`
8. Configuration files
9. Config file loading
10. Config priority (3 locations)
11. Changelog synthesizer syntax and execute permissions
12. Deterministic changelog grouping over a temporary git fixture
13. Clean empty-range output for explicit ref boundaries

**Running Tests**:
```bash
./tests/integration_tests.sh
```

Expected output: All tests pass (14/14)
The changelog synthesizer tests create temporary git repositories, make
representative commits, and assert that the generated markdown stays stable
across `Added`, `Changed`, `Fixed`, and `Security` sections.

### Manual Testing Checklist

Expand Down
186 changes: 186 additions & 0 deletions scripts/generate_changelog.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
cat <<'EOF'
Usage: scripts/generate_changelog.sh --from <ref> [--to <ref>]

Generate a Keep a Changelog-style markdown draft from git commits in an explicit
ref range. The output is intended for maintainer review and manual editing.

Options:
--from <ref> Required lower bound git ref (exclusive)
--to <ref> Optional upper bound git ref (inclusive), defaults to HEAD
--help Show this help text
EOF
}

die() {
echo "Error: $*" >&2
exit 1
}

trim_whitespace() {
printf '%s' "$1" | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g; s/^[[:space:]]+//; s/[[:space:]]+$//'
}

normalize_subject() {
local subject
local pr_suffix=""
local prefix_stripped

subject="$(trim_whitespace "$1")"
if [[ "${subject}" =~ ^(.*)( \(#[0-9]+\))$ ]]; then
subject="${BASH_REMATCH[1]}"
pr_suffix="${BASH_REMATCH[2]}"
fi

subject="$(printf '%s' "${subject}" | sed -E 's/^[[:space:]]*[*-][[:space:]]+//')"
prefix_stripped="$(printf '%s' "${subject}" | sed -E 's/^[[:alnum:]_.\/-]+(\([^)]+\))?!?:[[:space:]]*//')"
if [[ -n "${prefix_stripped}" && "${prefix_stripped}" != "${subject}" ]]; then
subject="${prefix_stripped}"
fi

subject="$(trim_whitespace "${subject}")"
subject="${subject%.}"

if [[ -n "${subject}" ]]; then
subject="${subject^}"
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

${subject^} is a Bash 4+ expansion. On macOS, /usr/bin/env bash often resolves to Bash 3.2, which will fail with bad substitution and break the script. Either avoid this expansion (use a portable capitalization approach) or explicitly document/enforce a minimum Bash version at runtime (with a clear error).

Suggested change
subject="${subject^}"
# Capitalize the first character in a Bash 3-compatible way
local first_char rest upper_first
first_char=${subject:0:1}
rest=${subject:1}
upper_first=$(printf '%s' "${first_char}" | tr '[:lower:]' '[:upper:]')
subject="${upper_first}${rest}"

Copilot uses AI. Check for mistakes.
fi

printf '%s%s' "${subject}" "${pr_suffix}"
}

categorize_subject() {
local lower

lower="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"

if [[ "${lower}" =~ (^|[^[:alpha:]])(security|cve|vuln|vulnerability|harden|hardening|sanitize|auth)([^[:alpha:]]|$) ]]; then
printf 'Security'
elif [[ "${lower}" =~ ^(feat|add|introduce|implement|support|enable|create|new)(\(|:|!|[[:space:]]) ]] || \
[[ "${lower}" =~ (^|[^[:alpha:]])(add|introduce|implement|support|enable|create)([^[:alpha:]]|$) ]]; then
printf 'Added'
elif [[ "${lower}" =~ ^(fix|bugfix|hotfix)(\(|:|!|[[:space:]]) ]] || \
[[ "${lower}" =~ (^|[^[:alpha:]])(fix|fixed|bug|bugs|regression|repair|resolve|resolved|correct)([^[:alpha:]]|$) ]]; then
printf 'Fixed'
else
printf 'Changed'
fi
}

append_entry() {
local category="$1"
local entry="$2"

case "${category}" in
Added)
added_entries+=("${entry}")
;;
Changed)
changed_entries+=("${entry}")
;;
Fixed)
fixed_entries+=("${entry}")
;;
Security)
security_entries+=("${entry}")
;;
*)
changed_entries+=("${entry}")
;;
esac
}

print_section() {
local title="$1"
shift

if [[ "$#" -eq 0 ]]; then
return 0
fi

printf '### %s\n' "${title}"
for entry in "$@"; do
printf -- '- %s\n' "${entry}"
done
printf '\n'
}

ensure_repo() {
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || die "must be run inside a git repository"
}

ensure_ref() {
local ref="$1"
git rev-parse --verify "${ref}^{commit}" >/dev/null 2>&1 || die "unknown git ref: ${ref}"
}

from_ref=""
to_ref="HEAD"

while [[ "$#" -gt 0 ]]; do
case "$1" in
--from)
shift
[[ "$#" -gt 0 ]] || die "--from requires a git ref"
from_ref="$1"
;;
--to)
shift
[[ "$#" -gt 0 ]] || die "--to requires a git ref"
to_ref="$1"
;;
--help|-h)
usage
exit 0
;;
*)
usage >&2
die "unknown argument: $1"
;;
esac
shift
done

[[ -n "${from_ref}" ]] || die "--from is required"

ensure_repo
ensure_ref "${from_ref}"
ensure_ref "${to_ref}"

resolved_from="$(git rev-parse "${from_ref}^{commit}")"
resolved_to="$(git rev-parse "${to_ref}^{commit}")"

declare -a added_entries=()
declare -a changed_entries=()
declare -a fixed_entries=()
declare -a security_entries=()

while IFS= read -r -d $'\x1e' raw_subject; do
raw_subject="$(trim_whitespace "${raw_subject}")"
[[ -n "${raw_subject}" ]] || continue
[[ "${raw_subject}" == Merge\ pull\ request* ]] && continue

normalized_subject="$(normalize_subject "${raw_subject}")"
[[ -n "${normalized_subject}" ]] || continue

category="$(categorize_subject "${raw_subject}")"
append_entry "${category}" "${normalized_subject}"
done < <(git log --no-merges --reverse --format='%s%x1e' "${resolved_from}..${resolved_to}")

printf '<!-- Draft changelog generated from %s..%s. Review and edit before publishing. -->\n\n' "${resolved_from}" "${resolved_to}"
printf '## [Unreleased]\n\n'

if [[ "${#added_entries[@]}" -eq 0 ]] && \
[[ "${#changed_entries[@]}" -eq 0 ]] && \
[[ "${#fixed_entries[@]}" -eq 0 ]] && \
[[ "${#security_entries[@]}" -eq 0 ]]; then
printf '_No notable changes in this range._\n'
exit 0
fi

print_section "Added" "${added_entries[@]}"
print_section "Changed" "${changed_entries[@]}"
print_section "Fixed" "${fixed_entries[@]}"
print_section "Security" "${security_entries[@]}"
Loading
Loading