• v0.4.0 0069198ca6

    v0.4.0: audit-driven hardening
    Some checks failed
    CI / lint (push) Has been cancelled
    CI / build (push) Has been cancelled
    CI / test (push) Has been cancelled
    CI / functional (push) Has been cancelled
    Stable

    sid released this 2026-05-02 16:05:22 -06:00 | 0 commits to main since this release

    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.gorun and workflow subtrees converted from
      package-level vars 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).
    Downloads