chore: bump version to 0.4.0
CHANGELOG.md updated with the audit-driven hardening pass spanning 13 findings across cmd/ and internal/. Adds CLAUDE.md documenting dev workflow, codex review pattern, release process, and homebrew tap update steps.
This commit is contained in:
parent
373c769d2c
commit
74d6ed699f
3 changed files with 268 additions and 1 deletions
101
CHANGELOG.md
101
CHANGELOG.md
|
|
@ -5,6 +5,107 @@ All notable changes to this project 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).
|
||||
|
||||
## [0.4.0] - 2026-05-02
|
||||
|
||||
Audit-driven hardening pass. Three reviewers (Codex + two Claude agents
|
||||
with non-overlapping focuses) found 13 issues across cmd/ and internal/;
|
||||
this release ships fixes for all 13.
|
||||
|
||||
### BREAKING
|
||||
|
||||
- `--json=fields` syntax removed. The flag was a string with
|
||||
`NoOptDefVal=" "` sentinel — `--json` alone meant "everything",
|
||||
`--json=fields` projected. That produced `--json string[=" "]` in
|
||||
`--help` and required a literal `=` because `--json fields` was parsed
|
||||
as the bare flag plus a positional. **Migration**: `--json=fields` →
|
||||
`--json-fields fields`. Bare `--json` still means "all fields as JSON".
|
||||
`--json` and `--json-fields` are mutually exclusive; `--jq` composes
|
||||
with either.
|
||||
|
||||
### Added
|
||||
|
||||
- `fj api --json` / `--json-fields` / `--jq` — projection and jq filtering
|
||||
for raw API responses. Routes through the same `addJSONFlags` helpers
|
||||
as the other list commands. Closes the inconsistency where `fj api`
|
||||
was the only command returning raw JSON without these knobs.
|
||||
- `fj api --paginate` — follows RFC 5988 `Link: rel="next"` headers and
|
||||
concatenates JSON array pages, gh-compatible. Validates same-origin
|
||||
before forwarding the bearer token to the next URL.
|
||||
- `cmd/paginate.go` — generic `paginateGitea[T any]` helper. Applied to
|
||||
`repo list`, `pr list`, `issue list`. Previously only `release list`
|
||||
walked pages; the others passed `PageSize: limit` directly to the
|
||||
gitea SDK, which silently caps PageSize at 50, so `--limit > 50` was
|
||||
truncated without warning.
|
||||
- `CLAUDE.md` — guide for Claude Code sessions: layout, codex review
|
||||
pattern, release process, homebrew tap update steps.
|
||||
|
||||
### Changed
|
||||
|
||||
- `--json` flag rebuilt as a plain `Bool`. `--json-fields` keeps
|
||||
comma-separated projection. Both registered via `addJSONFlags` and
|
||||
marked `MutuallyExclusive`.
|
||||
- `cmd/actions.go` — `run` and `workflow` subtrees converted from
|
||||
package-level `var`s to factory functions (`newRunCmd`,
|
||||
`newWorkflowCmd`, ...). `cmd/aliases.go` shrank from 142 → 17 lines
|
||||
and now calls those same factories with a `parentLabel` parameter that
|
||||
disambiguates the alias variant. Result: `diff` of `fj run list
|
||||
--help` flags vs `fj actions run list --help` flags is now empty.
|
||||
Drift between the two paths is structurally impossible.
|
||||
- `fj api` now uses `internal/api.SharedHTTPClient` (30s timeout, pooled
|
||||
connections) instead of a zero-value `&http.Client{}` with no timeout.
|
||||
A hung Forgejo no longer pins the CLI indefinitely.
|
||||
- `fj api` response body bounded by `io.LimitReader` at 64 MB to prevent
|
||||
OOM-on-self.
|
||||
- `cmd/auth.go` removed redundant local `--hostname` declarations on
|
||||
three subcommands. The persistent flag on rootCmd is now the only
|
||||
declaration; previously local declarations shadowed it, so
|
||||
`fj --hostname=X auth login` and `fj auth login --hostname=X` went
|
||||
through different code paths.
|
||||
- `--token` on `auth login` emits a stderr warning when used (visible
|
||||
in `ps auxe` and shell history). Flag not removed; just discoverable.
|
||||
- Error handling: `Hint` is now a structured field on `CLIError`.
|
||||
JSON-error consumers get clean structure; the human renderer still
|
||||
appends `\nHint: ...`. Dropped substring matching of `"401"`/`"403"`
|
||||
against rendered error strings (would match issue #403); now relies
|
||||
exclusively on typed `*api.APIError`.
|
||||
- Network errors (`no such host`, `connection refused`, `i/o timeout`)
|
||||
return a structured `CLIError` with code `ErrNetworkError` and a hint.
|
||||
- Config dir created with mode 0700 instead of 0755.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `--config <path>` now actually honored. Previously fed only into
|
||||
Viper; every command that touched config went through
|
||||
`internal/config.Load()` / `Save()` which always read the default
|
||||
path. So `fj --config other.yaml auth login` writes to other.yaml now.
|
||||
- `fj run list --json`, `fj workflow list --json`, `fj wiki view --json`
|
||||
now produce JSON. `cmd/aliases.go` registered `--json` as `Bool` but
|
||||
handlers called `wantJSON()` which does `GetString("json")` — pflag
|
||||
returned a type-error that `wantJSON` silently swallowed.
|
||||
`cmd/wiki.go` had the inverse bug (`GetBool` against an
|
||||
`addJSONFlags`-registered string flag). Both routed through
|
||||
`addJSONFlags`/`wantJSON`/`outputJSON` consistently now.
|
||||
- `migrateConfigDir` opens dst with `O_TRUNC`. Previously a partially-
|
||||
pre-existing dst file would have legacy contents overwrite a prefix
|
||||
and leave stale tail bytes — silent YAML/token corruption. Refactored
|
||||
close handling into `copyOneConfigFile`.
|
||||
|
||||
### Security
|
||||
|
||||
- `fj api` endpoint path traversal closed. `fj api '/../admin/users'`
|
||||
previously normalized through `http.NewRequest` to
|
||||
`https://host/admin/users` — silently sending authenticated traffic
|
||||
to non-API paths. Endpoint is now parsed via `url.Parse`, `..`
|
||||
segments rejected, then `JoinPath` onto the `/api/v1` base.
|
||||
URL-encoded `%2E%2E` is also caught because Go decodes before our
|
||||
split.
|
||||
- `fj api --paginate` validates same-origin before forwarding the
|
||||
bearer token to a `Link: rel="next"` URL. Refuses to reattach
|
||||
`Authorization` if the next URL's scheme isn't `https` or its host
|
||||
doesn't match the configured one.
|
||||
- `initConfig` warns on stderr if the resolved config file is world or
|
||||
group readable (`mode & 0o077 != 0`).
|
||||
|
||||
## [0.3.0c] - 2026-03-21
|
||||
|
||||
### Added
|
||||
|
|
|
|||
166
CLAUDE.md
Normal file
166
CLAUDE.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# fj — guide for Claude Code sessions
|
||||
|
||||
`fj` is a personal Forgejo/Gitea CLI tool, modeled on GitHub's `gh`. It targets `forgejo.zerova.net` (and Codeberg). The user (sid) owns it; the canonical repo is `public/fj` on forgejo.zerova.net (mirrored from there to nowhere else).
|
||||
|
||||
This file is read first by Claude Code when working in `~/repos/fj`. Goal: get a session productive quickly without re-deriving the dev workflow each time.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
~/repos/fj/
|
||||
├── cmd/ cobra command definitions, one file per subject area
|
||||
│ ├── root.go rootCmd, --config plumbing, OnInitialize
|
||||
│ ├── auth.go login/status/logout/token (uses persistent --hostname)
|
||||
│ ├── api.go raw API access; --json/--json-fields/--jq/--paginate
|
||||
│ ├── json.go shared JSON output helpers (addJSONFlags/wantJSON/outputJSON)
|
||||
│ ├── paginate.go generic paginateGitea[T] helper for list commands
|
||||
│ ├── errors.go CLIError with structured Hint field
|
||||
│ ├── actions.go Forgejo Actions; runs/workflows via factory functions
|
||||
│ ├── aliases.go top-level `fj run` / `fj workflow` aliases — calls actions.go factories
|
||||
│ ├── repo.go pr.go issue.go release.go wiki.go label.go milestone.go
|
||||
│ └── ...
|
||||
├── internal/
|
||||
│ ├── api/client.go SharedHTTPClient (30s timeout); GetJSON/DoJSON/DownloadFile
|
||||
│ ├── config/config.go YAML config; honors --config via SetExplicitConfigPath
|
||||
│ ├── git/ repo + host detection from `git remote`
|
||||
│ ├── iostreams/ wrapped stdin/stdout/stderr + spinner + pager + colors
|
||||
│ └── text/ formatting helpers
|
||||
├── main.go thin entrypoint; ContextualError + JSON-error rendering
|
||||
├── Makefile build / lint / test (no release automation)
|
||||
├── CHANGELOG.md Keep-a-Changelog format
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Build, install, test
|
||||
|
||||
```bash
|
||||
go build ./... # quick build check
|
||||
go test ./... # unit tests
|
||||
go install . # build + install to ~/go/bin/fj (the binary that's on PATH)
|
||||
make lint # golangci-lint, if you have it
|
||||
```
|
||||
|
||||
After any change in cmd/ or internal/, run `go install .` and the global `fj` reflects it immediately. There's no daemon/restart.
|
||||
|
||||
## Auth
|
||||
|
||||
The user is authenticated as `sid` on `forgejo.zerova.net`. Token lives in `~/.config/fj/config.yaml` (mode 0600). For HTTPS git pushes from this host, the token can be injected via `git -c "http.extraHeader=Authorization: token <T>" push` — the local SSH key (`sid@debian` on forgejo) is also registered, so `git@forgejo.zerova.net:public/fj.git` works directly.
|
||||
|
||||
## Code review pattern (use this for non-trivial changes)
|
||||
|
||||
For audits or significant refactors, run **three reviewers in parallel** with non-overlapping focuses (we did this in the v0.4.0 cycle and it found bugs none would have caught alone):
|
||||
|
||||
- **Codex** — read-only sandbox, peer-AI cross-check
|
||||
```bash
|
||||
codex exec --skip-git-repo-check --sandbox read-only \
|
||||
-m gpt-5.4-mini --config model_reasoning_effort="medium" "<prompt>" 2>/dev/null
|
||||
```
|
||||
For follow-up rounds resume the same session: `echo "<prompt>" | codex exec --skip-git-repo-check resume --last 2>/dev/null`. Codex remembers prior critique.
|
||||
|
||||
- **Claude general-purpose agent A** — architecture / UX / code-quality
|
||||
- **Claude general-purpose agent B** — security / correctness / error handling
|
||||
|
||||
Tell each reviewer what the **siblings** are covering so they don't duplicate. Cap reports at ~600 words. Consolidate findings by severity (HIGH / MEDIUM / LOW) before presenting to the user.
|
||||
|
||||
## Release process
|
||||
|
||||
We use semver. **Pre-1.0**: breaking change → minor bump (e.g. v0.3.x → v0.4.0).
|
||||
|
||||
1. **Bump version**
|
||||
```go
|
||||
// cmd/root.go
|
||||
Version: "0.4.0",
|
||||
```
|
||||
|
||||
2. **Update CHANGELOG.md** — prepend a new section. Format:
|
||||
```markdown
|
||||
## [0.4.0] - YYYY-MM-DD
|
||||
|
||||
### BREAKING
|
||||
- <thing that broke>
|
||||
|
||||
### Added
|
||||
- ...
|
||||
### Changed
|
||||
- ...
|
||||
### Fixed
|
||||
- ...
|
||||
### Security
|
||||
- ...
|
||||
```
|
||||
|
||||
3. **Commit** the version+changelog bump as a single commit:
|
||||
```bash
|
||||
git commit -m "chore: bump version to 0.4.0"
|
||||
```
|
||||
|
||||
4. **Tag** the commit:
|
||||
```bash
|
||||
git tag -a v0.4.0 -m "Release v0.4.0: <one-line summary>"
|
||||
```
|
||||
|
||||
5. **Push** commits and tag:
|
||||
```bash
|
||||
git push origin main
|
||||
git push origin v0.4.0
|
||||
```
|
||||
|
||||
6. **Create the Forgejo release page** via fj itself:
|
||||
```bash
|
||||
fj release create v0.4.0 \
|
||||
--title "v0.4.0: <summary>" \
|
||||
--notes "$(awk '/^## \[0.4.0\]/{flag=1;next} /^## /{flag=0} flag' CHANGELOG.md)"
|
||||
```
|
||||
(The awk one-liner extracts the just-added CHANGELOG section as release notes.)
|
||||
|
||||
7. **Update the homebrew tap** — see the next section.
|
||||
|
||||
## Updating the homebrew tap (`public/homebrew-sid`)
|
||||
|
||||
The tap lives at `~/repos/homebrew-sid` (or `git@forgejo.zerova.net:public/homebrew-sid.git`). The `Formula/fj.rb` formula references the source by `tag:` + `revision:` (SHA), so a release bump touches three lines:
|
||||
|
||||
```ruby
|
||||
url "ssh://git@forgejo.zerova.net/public/fj.git",
|
||||
tag: "v0.4.0", # was v0.3.2
|
||||
revision: "<SHA of v0.4.0 tag>" # update
|
||||
|
||||
test do
|
||||
assert_match "0.4.0", shell_output("#{bin}/fj --version") # update
|
||||
end
|
||||
```
|
||||
|
||||
To get the SHA:
|
||||
```bash
|
||||
git -C ~/repos/fj rev-parse v0.4.0
|
||||
```
|
||||
|
||||
Then in `~/repos/homebrew-sid`:
|
||||
```bash
|
||||
# edit Formula/fj.rb (the three lines above)
|
||||
git commit -am "fj: bump to v0.4.0"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
After push, users can `brew update && brew upgrade fj` to pick up the new version.
|
||||
|
||||
## Common footguns
|
||||
|
||||
- **`fj` reads the current dir's `git origin`** to detect the host. In a directory whose origin points at github.com (e.g. /opt/stacks/claude-code-proxy/build), bare `fj api ...` errors with "no configuration found for host github.com". Pass `--hostname forgejo.zerova.net` explicitly, or `cd` somewhere else.
|
||||
- **`--json=fields` was removed in v0.4.0** in favor of `--json-fields fields` (or `--json-fields=fields`). The old `=fields` form was a `NoOptDefVal=" "` sentinel hack. `--json` is now a plain Bool meaning "as JSON".
|
||||
- **`--config` was silently ignored before v0.4.0.** Old fj versions read --config into Viper but `internal/config.Load()` always read the default path. Fixed; `fj --config other.yaml auth login` now writes to other.yaml.
|
||||
- **The `actions` and `run`/`workflow` command trees share factory functions** in `cmd/actions.go` (`newRunCmd`, `newWorkflowCmd`). Don't add flags directly to `runListCmd` style globals — they don't exist anymore. Edit the factory and both `fj actions run list` and `fj run list` get the change.
|
||||
|
||||
## Useful commands
|
||||
|
||||
```bash
|
||||
# Live test against forgejo (using the new flags)
|
||||
fj --hostname forgejo.zerova.net api repos/public/fj --json-fields full_name,description
|
||||
|
||||
# Walk paginated endpoints
|
||||
fj --hostname forgejo.zerova.net api 'repos/public/fj/commits?limit=10' --paginate --jq '.[].sha[0:8]'
|
||||
|
||||
# Confirm both command trees stay in sync after edits
|
||||
diff <(fj run list --help | grep -E "^ -|^ --" | sort) \
|
||||
<(fj actions run list --help | grep -E "^ -|^ --" | sort)
|
||||
# Empty diff = trees agree. Any output = factory drift.
|
||||
```
|
||||
|
|
@ -22,7 +22,7 @@ var rootCmd = &cobra.Command{
|
|||
Short: "Forgejo CLI tool - work seamlessly with Forgejo from the command line",
|
||||
Long: `fj is a command line tool for Forgejo instances (including Codeberg).
|
||||
It brings pull requests, issues, and other Forgejo concepts to the terminal.`,
|
||||
Version: "0.3.2",
|
||||
Version: "0.4.0",
|
||||
SilenceErrors: true,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue