diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1e5c995 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,47 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# Go files - use tabs (Go standard) +[*.go] +indent_style = tab +indent_size = 4 + +# Go mod files +[go.{mod,sum}] +indent_style = tab +indent_size = 4 + +# Makefiles - must use tabs +[{Makefile,*.mk}] +indent_style = tab +indent_size = 4 + +# YAML files +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +# JSON files +[*.json] +indent_style = space +indent_size = 2 + +# Markdown files +[*.md] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = false + +# Shell scripts +[*.sh] +indent_style = space +indent_size = 2 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index cd989f4..ca2b95d 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: - name: Build production binary run: | make build - echo "Binary built at: $(pwd)/bin/fgj" + echo "Binary built at: $(pwd)/bin/fj" - name: Run functional tests run: go test -v -race -tags=functional ./tests/functional/... diff --git a/.gitea/workflows/nightly.yml b/.gitea/workflows/nightly.yml new file mode 100644 index 0000000..ef53290 --- /dev/null +++ b/.gitea/workflows/nightly.yml @@ -0,0 +1,35 @@ +name: Nightly Functional Tests + +on: + schedule: + # Runs every day at midnight UTC + - cron: '0 0 * * *' + workflow_dispatch: # Allows manual trigger from the UI + +jobs: + functional: + runs-on: codeberg-small + steps: + - name: Checkout code + uses: https://github.com/actions/checkout@v4 + with: + ref: main # Always test the main branch + + - name: Set up Go + uses: https://github.com/actions/setup-go@v5 + with: + go-version: '1.21' + cache: true + + - name: Build production binary + run: | + make build + echo "Binary built at: $(pwd)/bin/fj" + + - name: Run functional tests + run: go test -v -race -tags=functional ./tests/functional/... + env: + CODEBERG_TEST_TOKEN: ${{ secrets.CODEBERG_TEST_TOKEN }} + CODEBERG_TEST_OWNER: ${{ vars.CODEBERG_TEST_OWNER }} + CODEBERG_TEST_REPO: ${{ vars.CODEBERG_TEST_REPO }} + CODEBERG_TEST_HOSTNAME: ${{ vars.CODEBERG_TEST_HOSTNAME || 'codeberg.org' }} diff --git a/.gitignore b/.gitignore index f46cde4..58e31fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Binaries -fgj +fj +bin/ *.exe *.exe~ *.dll @@ -28,3 +29,8 @@ Thumbs.db # Config (contains tokens) config.yaml + +# Git worktrees +.worktrees/ +# Workspace (scratch data, cloned repos, analysis) +.workspace/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e6edd83..170c5ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,53 +5,292 @@ 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 ` 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 + +#### Label Management +- `fj label list` - List repository labels +- `fj label create` - Create a label with color and description +- `fj label edit` - Edit label name, color, or description +- `fj label delete` - Delete a label + +#### Milestone Management +- `fj milestone list` - List milestones with state filtering +- `fj milestone view` - View milestone details +- `fj milestone create` - Create a milestone with description and due date +- `fj milestone edit` - Edit milestone title, description, due date, or state +- `fj milestone delete` - Delete a milestone + +#### Wiki Management +- `fj wiki list` - List wiki pages +- `fj wiki view` - View wiki page content +- `fj wiki create` - Create a wiki page from flag or file +- `fj wiki edit` - Edit a wiki page +- `fj wiki delete` - Delete a wiki page + +#### Issue Dependencies +- `fj issue edit --add-dependency ` - Add issue dependency +- `fj issue edit --remove-dependency ` - Remove issue dependency + +## [0.3.0b] - 2026-03-21 + +### Added + +#### Repository Management +- `fj repo edit` - Edit repository settings (visibility, description, homepage, default branch) + +### Fixed +- `fj repo create --public` flag was defined but never read; now properly wired up + +## [0.3.0a] - 2026-03-21 + +### Added + +#### Raw API Access +- `fj api ` - Make authenticated REST API requests to any Forgejo/Gitea endpoint + - HTTP method selection (`--method`/`-X`), auto-switches to POST when fields are provided + - JSON field assembly (`--field`/`-f`) with type inference (bool, int, float, null, string) + - Raw string fields (`--raw-field`/`-F`) + - Request body from file or stdin (`--input`) + - Custom headers (`--header`/`-H`) + - Path interpolation (`{owner}`, `{repo}`) from git context + - Response header display (`--include`/`-i`) + +#### Pull Request Management +- `fj pr diff ` - View the diff for a pull request + - Colorized output (`--color auto/always/never`) + - Changed file names only (`--name-only`) + - Diffstat summary (`--stat`) +- `fj pr comment ` - Add a comment to a pull request + - Body from flag (`--body`/`-b`) or file (`--body-file`, `-` for stdin) + - JSON output (`--json`) +- `fj pr review ` - Submit a review on a pull request + - Approve (`--approve`/`-a`), request changes (`--request-changes`/`-r`), or comment (`--comment`/`-c`) + - Body from flag or file + - JSON output (`--json`) + +#### Agentic / Machine-Readable Output +- `--json-errors` global flag for structured JSON error output on stderr + - Error codes: `auth_required`, `not_found`, `api_error`, `invalid_input`, `git_detection_failed`, `network_error` + - HTTP status code and detail included when available + - Automatic mapping of API errors (401/403 → `auth_required`, 404 → `not_found`) + +## [0.3.0] - 2026-03-13 + +### Added + +#### Forgejo Actions +- `fj actions run watch ` - Poll a run until completion +- `fj actions run rerun ` - Trigger a rerun of a workflow run +- `fj actions run cancel ` - Cancel an in-progress workflow run +- `fj actions workflow enable ` - Enable a workflow +- `fj actions workflow disable ` - Disable a workflow + +#### Repository Management +- `fj repo create ` - Create a new repository with full option set: `--private`/`--public`, `--description`, `--add-readme`, `--gitignore`, `--license`, `--homepage`, `--clone`, `--team` + +#### Issue Management +- `fj issue create -l