• v0.5.0 4bae59797f

    v0.5.0
    All checks were successful
    release / prepare (push) Successful in 4s
    release / build (catthehacker/ubuntu:act-22.04, linux-x86_64, docker, x86_64-unknown-linux-gnu) (push) Successful in 2m20s
    release / build (darwin-aarch64, macos-15, aarch64-apple-darwin) (push) Successful in 3m55s
    release / build (darwin-x86_64, macos-15, x86_64-apple-darwin) (push) Successful in 3m44s
    release / publish (push) Successful in 20s
    release / guard (push) Has been skipped
    ci / check (push) Successful in 11m6s
    ci / live-e2e (push) Successful in 2m8s
    ci / coverage (push) Successful in 2m30s
    Stable

    stephen released this 2026-09-13 18:32:57 +00:00 | 3 commits to main since this release

    Signed by stephen
    GPG key ID: 67F885778E01BF16

    Added

    • fj stack ship restacks the rest of the stack after a squash land. A
      squash merge replaces the item's commits, so every branch above it was left
      parented on history the trunk no longer had: the PRs above showed the landed
      diff a second time and the stack had to be repaired by hand. Ship now replays
      the remainder onto the new trunk tip with a single
      git rebase --onto <new trunk> <landed tip> <top branch> --update-refs (the
      plumbing stack absorb already used), force-pushes each branch with an
      explicit --force-with-lease, and re-syncs the PR bases. A conflicting
      replay rolls the rebase back and force-pushes nothing, then ship stops with
      the commands to finish by hand. The PR directly above the landed item is
      still re-pointed at the trunk, because the base branch it was targeting has
      merged away and leaving it aimed at a deleted branch would be worse than the
      no-restack end state. Merge-commit lands are unaffected, since the original
      commits stay reachable. --no-restack opts out. The replay needs git 2.38 or
      newer.
    • fj stack ship pre-flights the stack's shape in git. Before merging
      anything it verifies that each unmerged branch descends from the one below
      it, that the lowest one contains the trunk's fetched tip, and that each
      local branch tip matches the head of the PR that would merge. A stale or
      non-linear stack is refused up front with a "restack, then fj stack sync"
      message rather than failing mid-merge with the bottom already landed.
    • fj stack ship re-validates between merges. After an item lands, the
      next one is re-checked (still open, still mergeable, still approved, still at
      the head fj most recently put there) before it is merged. If the ground
      shifted, ship stops cleanly, reports what already landed, and leaves the rest
      for a re-run. Checks that ship itself re-armed by force-pushing a restacked
      branch are not re-gated. A forge still serving the pre-restack head is a
      lagging read rather than a shift, so ship re-reads a few times; if it never
      catches up, that is a stop in its own right, since every field on that
      snapshot describes a shape the restack has already replaced.
    • stack ship --json now reports the branches restacked after each item
      (restacked), and stack review --json carries each PR's head sha
      (head_sha). Both fields are additive.
    • fj run logs prints Actions job logs. The singular fj run log stays as
      an alias, so existing invocations keep working. A personal access token is
      enough for log reads on this Forgejo build; the auth docs no longer claim a
      browser session cookie is required. A session is still needed for the
      cookie-gated web routes, run watch, run rerun, and run cancel.
    • fj instances lists the Fjord Commons. fj read only
      /me/forge-instances, which by design excludes the Commons, so there was no
      instance id to address and no way to reach it. The Commons is now read
      alongside your own instances and routed through the platform gateway, with no
      personal access token anywhere: the gateway mints the forge token server-side
      and nothing minted reaches the client. A Commons you have not joined is
      withheld from instances use but still named in fj instances, on a dimmed
      line, so "not joined" does not read as "no such forge".
    • Host-scoped FJ_TOKEN_<HOST> and FJ_SESSION_<HOST>. The host suffix is
      H followed by the uppercase hex of the host bytes, so rasterhub.com is
      FJ_TOKEN_H7261737465726875622E636F6D. The encoding is injective, so
      foo-bar.com and foo.bar.com cannot collide onto one variable, and CI can
      set a credential for one host without enumerating the rest.
    • fj pr merge --style fast-forward-only. Forgejo's fast-forward-only
      merge style is accepted both as an explicit --style and as a repository's
      default_merge_style, which previously failed as unsupported. A PR that is
      not a fast-forward onto its base is reported as such, with the rebase to do,
      instead of a raw forge error.

    Changed

    • fj pr merge follows the repository's default merge style. An omitted
      --style resolved to merge regardless of how the repository was
      configured, so merging from the CLI could produce a merge commit on a repo
      whose web UI squashes. It now reads the repository's default_merge_style,
      and fails explicitly if that cannot be read or is not a style fj supports
      rather than falling back to a guess. Scripts that relied on the implicit
      merge need --style merge spelled out.
      Merge-style is also reported in
      push-rejection diagnostics, so a signed-commit protection failure is no
      longer misread as something else.
    • A generic FJ_TOKEN is no longer sent to a host it was not meant for.
      It used to be picked up for any host. It is now accepted for the default or
      current host, including when you spell that same host with --host, and
      ignored when --host names a different configured host. Use
      FJ_TOKEN_<HOST> for that case. FJ_SESSION follows the same rule.
    • The default Fjord platform URL is https://fjord.sh, replacing the old
      preview URL. An HTML response on a platform call now names --platform-url
      instead of surfacing an opaque JSON decode error.
    • Global --host reaches the auth subcommands. fj auth status, token,
      refresh, session, logout, and setup-git now honor it, so
      fj auth status --host X reports host X alone.

    Fixed

    • cargo test --all now passes on macOS. Four defects kept it from
      finishing there, and each one was a test reading the developer's real machine
      instead of its own scratch state. ProjectDirs ignores XDG_CONFIG_HOME on
      macOS, so tests that thought they had redirected the config directory were
      loading the real hosts.toml; when that named a host whose token lives in the
      login keychain, the lookup then blocked forever in securityd, because a
      keychain item's ACL names the binaries allowed to read it and a freshly built
      test binary is not one of them. FJ_CONFIG_DIR and FJ_NO_KEYCHAIN give
      those tests, and any scripted run that wants its own state, a way to say so.
      The editor test hardcoded /bin/false, which exists on Linux but not on
      macOS, so it asserted on a spawn failure rather than the nonzero exit it was
      written for. And the loopback sign-in test drove real sockets on a paused
      clock, which cannot work: tokio's auto-advance does not wait for socket
      readiness, so it jumped the clock past the five-minute callback deadline while
      the silent connection was still in the accept queue. That one now runs on the
      real clock with the request-line bound shortened for the test; the shipped
      ten-second bound is unchanged.

    • The pager no longer swallows short, write, and machine output. The
      decision to page was made per top-level command family, so every subcommand
      under repo, issue, pr, release, label, milestone, tag, run,
      search, status, and api was piped into less, including one-line write
      confirmations (pr merge, issue close, repo star, tag create, ...),
      fixed status blocks (fj status, fj pr status), short reads (tag view,
      milestone view, repo topics), and every --json and --web invocation.
      Destructive commands that prompt for confirmation (repo delete,
      release delete, tag delete) were prompting on a stdout already redirected
      into the pager. Paging is now decided per subcommand by
      cli::paging::should_page, which pages only genuinely long read-only views:
      list and view commands, pr diff / commits / files / checks,
      run log, search, and fj api GET dumps. --no-pager, FJ_NO_PAGER,
      FJ_PAGER / PAGER, and the non-TTY check are unchanged.

    • fj pr merge no longer discards the pull request description. With no
      --message, fj sent an empty merge-commit body, and Forgejo does not fall
      back to the description the way its web merge dialog does. Every pull request
      merged through the CLI therefore landed with its body gone, silently: the
      merge succeeded and the commit looked normal. The message now defaults to the
      pull request body. An explicit --message still wins, and --message ""
      still produces a deliberately empty body.

    • Ctrl+C works when fj is blocked in a synchronous call. SIGINT was raced
      against the command future, so it only took effect while that future was
      still pollable. Several paths parked the thread inside a blocking syscall for
      an unbounded time, and because tokio had already replaced SIGINT's default
      disposition the signal did not terminate the process either: Ctrl+C was not
      slow, it was inert. The two confirmed states were an OS keychain read waiting
      on an authorization dialog it cannot present (over ssh, in CI, from a git
      hook, which is what deadlocked the pre-push hook) and the synchronous stdin
      reads behind --body -, --input -, --with-token, and the interactive
      Fjord Account prompts. A backstop now gives the graceful path 250 ms to
      unwind and then exits 130. A child that owns the terminal's foreground
      process group still gets to decide what SIGINT means, so less absorbing it
      is unaffected.

    • Ctrl+C exits during an OIDC sign-in. fj auth login waited for the
      loopback redirect on a blocking task, so an interrupted sign-in printed
      error: interrupted and left the process alive. The wait is now async and
      actually cancellable. A stalled connection no longer wedges the sign-in
      either: a socket that connected and sent nothing (a browser preconnect, a
      port scanner) blocked the accept loop forever and the real redirect behind it
      was never served. Request lines are now bounded at 10s, and the overall 300s
      budget also covers connection handling.

    • FJ_TOKEN no longer blocks fj auth login --fjord. The variable was
      bound to --token by clap, so having one set forced the token flow even when
      --fjord was spelled out. Explicit --fjord now wins. FJ_SESSION was
      bound to fj auth session --cookie the same way, where it conflicted with
      --clear; that binding is gone too.

    • fj pr checks handles a head with no reported checks. Forgejo returns
      statuses: null there, which failed to decode. It now prints
      no checks reported on <sha> and exits 0; --json still emits a normal
      combined-status object with statuses: [].

    • fj run view --log reads logs on Forgejo 16. Logs are resolved through
      the run-jobs and job-log APIs, and displayed run numbers are mapped to API
      run ids first, including for runs past the first page. FJ_TOKEN precedence
      is preserved, so an env-token-only environment does not touch an unreadable
      token store. A Forgejo 15.x host, which does not expose the
      token-authenticated job log API, is now reported as such by version rather
      than as a parse error.

    • fj run view --log no longer downloads every task in the repository on
      Forgejo 16.
      The native job-log path fetched the workflow task list first
      and discarded it, and on 16 that endpoint ignores its run filter: about
      3 MB and 30 seconds on every log read against a busy repository. The task
      list is now loaded only by the Loki fallback, which is the one path that
      needs it, and that fallback also decodes the workflow_runs envelope 16
      returns instead of aborting on an unexpected response shape.

    • fj pr list and fj pr view decode Forgejo's system actors. Forgejo
      uses negative sentinel user ids (-1 for Ghost, -2 for forgejo-actions),
      and the shared User model read id as unsigned, so any repository with a
      system actor as reviewer, assignee, poster, or author failed the whole decode
      with invalid value: integer -2, expected u64. The field is signed now,
      which covers every actor position at once.

    • A failed list decode names where it failed. List responses are decoded
      through serde_path_to_error, so the message carries the endpoint and the
      JSON field path (for example GET /api/v1/repos/o/r/pulls?... and
      field [0].id) instead of only the type error.

    • Tag releases publish their assets, and the generated Homebrew formula
      installs.
      The v0.4.1 release was published with zero assets, so
      brew install fj served 0.3.0 from 22 to 30 July: one build leg failed, and
      re-running could not recover because artifact keys are scoped by run id, so
      the legs that had already uploaded hit "artifact already exists" and the
      publish job never ran. The assets were uploaded on 30 July by the recovery
      dispatch these changes made possible, and the tap has served 0.4.1 since.
      Artifacts are overwritten on a re-run now, and a release can be re-run by
      workflow_dispatch with a tag input, from a branch carrying a fix, without
      moving the tag. The formula could not have installed even if publish had run:
      it did cd fj-<version>-<target>, but Homebrew has already entered the
      tarball's top-level directory and that directory keeps the tag's v prefix,
      which the formula's version drops, so brew install ended in
      Errno::ENOENT: chdir_path. Three failures that used to pass quietly now
      stop the run: a missing matrix leg rendering sha256 "", an asset glob
      matching nothing, and a missing CHANGELOG section for the tag (release
      bodies were always empty, and are now taken from that section). A dispatched
      tag also reaches the shell as data rather than as a substituted expression,
      must exist in the repository's tags, and is checked out as refs/tags/<tag>
      with the resolved sha asserted in every job, so a branch sharing a
      version-shaped name cannot be built and published as if it were the tag.

    Downloads
  • v0.4.1 62d4d7f670

    v0.4.1
    Some checks failed
    ci / check (push) Successful in 10m25s
    ci / coverage (push) Successful in 1m41s
    ci / live-e2e (push) Successful in 1m58s
    release / build (catthehacker/ubuntu:act-22.04, linux-x86_64, docker, x86_64-unknown-linux-gnu) (push) Failing after 1m44s
    release / build (darwin-aarch64, macos-15, aarch64-apple-darwin) (push) Successful in 3m49s
    release / build (darwin-x86_64, macos-15, x86_64-apple-darwin) (push) Failing after 3m40s
    release / publish (push) Has been skipped
    Stable

    stephen released this 2026-07-22 20:19:49 +00:00 | 38 commits to main since this release

    Fixed

    • Silent Fjord Account session refresh across every command. An expired
      Fjord access token is now renewed transparently at the shared HTTP client: on
      a 401 from a host signed with a Fjord bearer, fj refreshes via the stored
      refresh token and retries the request once, so fj api, fj pr, fj issue,
      fj repo, and the Actions log routes no longer fail with "sign in again" once
      the short-lived token lapses. Previously only fj instances and fj auth
      refreshed. PAT hosts (whose tokens are not refreshable) and the case with no
      usable refresh token surface the original 401 unchanged, and the refresh is
      bounded to one attempt per request so a persistently-401 endpoint cannot loop.
    Downloads
  • v0.3.0 e81052ecd1

    v0.3.0
    All checks were successful
    release / build (catthehacker/ubuntu:act-22.04, linux-x86_64, docker, x86_64-unknown-linux-gnu) (push) Successful in 2m35s
    release / build (darwin-aarch64, macos-15, aarch64-apple-darwin) (push) Successful in 3m54s
    release / build (darwin-x86_64, macos-15, x86_64-apple-darwin) (push) Successful in 3m53s
    release / publish (push) Successful in 15s
    ci / check (push) Successful in 10m18s
    ci / coverage (push) Successful in 1m53s
    ci / live-e2e (push) Successful in 1m53s
    Stable

    stephen released this 2026-06-29 22:45:47 +00:00 | 51 commits to main since this release

    Signed by stephen
    SSH key fingerprint: SHA256:eja0m2LEwLSHxRnbQQg/d3CUnVWjxpjuMHSz6S/HHAM
    Downloads
  • v0.2.0 335deac982

    v0.2.0
    All checks were successful
    release / build (catthehacker/ubuntu:act-22.04, linux-x86_64, docker, x86_64-unknown-linux-gnu) (push) Successful in 1m28s
    ci / check (push) Successful in 1m50s
    ci / coverage (push) Successful in 2m0s
    release / build (darwin-aarch64, macos-15, aarch64-apple-darwin) (push) Successful in 3m49s
    release / build (darwin-x86_64, macos-15, x86_64-apple-darwin) (push) Successful in 3m40s
    release / publish (push) Successful in 16s
    Stable

    stephen released this 2026-06-02 22:58:18 +00:00 | 128 commits to main since this release

    Signed by stephen
    GPG key ID: 67F885778E01BF16
    Downloads
  • v0.1.3 0d218188dd

    v0.1.3
    Some checks failed
    release / build (rust:1.95-bookworm, linux-x86_64, docker, x86_64-unknown-linux-gnu) (push) Failing after 1m12s
    release / build (darwin-aarch64, macos, aarch64-apple-darwin) (push) Has been cancelled
    release / build (darwin-x86_64, macos, x86_64-apple-darwin) (push) Has been cancelled
    release / publish (push) Has been cancelled
    Stable

    stephen released this 2026-05-14 21:31:13 +00:00 | 187 commits to main since this release

    Signed by stephen
    GPG key ID: 67F885778E01BF16

    What's new

    • Linux x86_64 binary ships in this release. fj-v0.1.3-linux-x86_64.tar.gz is a statically-linked (against glibc ≥ 3.2) ELF that runs on Debian, Ubuntu, Arch, Fedora, NixOS, and most other modern distros.
    • Notarization scripts land for the next release. scripts/notarize.sh submits a signed darwin tarball to Apple's notary service; scripts/sign.sh --notarize chains signing → notarization in one step. The v0.1.3 macOS binaries are signed but not yet notarized — same signing identity as v0.1.2, so no new keychain prompt.
    • Windows claim removed from FAQ. Replaced with an honest "nobody's tried" + link to file an issue. The code still has #[cfg(windows)] stubs but ships no binary.

    Install

    # macOS via Homebrew:
    brew tap rasterandstate/tap
    brew upgrade fj
    
    # Linux:
    curl -fsSL https://rasterhub.com/rasterstate/fj/releases/download/v0.1.3/fj-v0.1.3-linux-x86_64.tar.gz | tar -xz
    sudo mv fj-v0.1.3-linux-x86_64/fj /usr/local/bin/fj
    fj --version
    

    Or download a tarball below. Verify with shasum -a 256 -c SHA256SUMS.

    Build it yourself

    cargo install --git https://rasterhub.com/rasterstate/fj --tag v0.1.3
    

    Full changelog: CHANGELOG.md.

    Downloads
  • v0.1.2 9bcd54d7a3

    v0.1.2
    Some checks failed
    release / build (rust:1.95-bookworm, linux-x86_64, docker, x86_64-unknown-linux-gnu) (push) Failing after 12m6s
    release / build (darwin-aarch64, macos, aarch64-apple-darwin) (push) Has been cancelled
    release / build (darwin-x86_64, macos, x86_64-apple-darwin) (push) Has been cancelled
    release / publish (push) Has been cancelled
    Stable

    stephen released this 2026-05-14 21:31:13 +00:00 | 188 commits to main since this release

    Signed by stephen
    GPG key ID: 67F885778E01BF16

    What's new

    Signed macOS binaries. Both darwin-aarch64 and darwin-x86_64 tarballs ship binaries signed with the Raster & State Developer ID (62Y3FRM8PD, hardened runtime enabled).

    The visible win: macOS keychain ACLs are scoped to code identity, not binary path. Now that releases share a stable signing identity, you click "Always Allow" on the keychain prompt once and don't get re-prompted on every fj upgrade.

    Notarization is a follow-up. Signing alone solves the prompt-storm; notarization is what makes Gatekeeper not warn on first run for binaries downloaded fresh.

    Also

    • scripts/sign.sh ships in-tree so contributors who build from source can sign their local binary and stop getting prompted. Reads FJ_APPLE_DEVELOPER_ID from .env / .env.local.
    • scripts/preflight.sh audits .env* files for secrets and grep-scans the working tree for PEM markers / Apple-password format / common token shapes. Wired into the pre-push hook.
    • .env.example documents the release/signing variable names. Real values stay out of the repo.
    • support@rasterstate.com is now the canonical contact email; personal-email references removed from tracked files.

    Install

    brew tap rasterandstate/tap
    brew upgrade fj
    

    Or download a tarball below. Verify with shasum -a 256 -c SHA256SUMS.

    Full changelog: CHANGELOG.md.

    Downloads
  • v0.1.1 86e9d8b795

    v0.1.1
    Some checks failed
    ci / check (push) Has been cancelled
    release / build (rust:1.95-bookworm, linux-x86_64, docker, x86_64-unknown-linux-gnu) (push) Failing after 3s
    release / build (darwin-aarch64, macos, aarch64-apple-darwin) (push) Has been cancelled
    release / build (darwin-x86_64, macos, x86_64-apple-darwin) (push) Has been cancelled
    release / publish (push) Has been cancelled
    Stable

    stephen released this 2026-05-14 16:36:39 +00:00 | 191 commits to main since this release

    Signed by stephen
    SSH key fingerprint: SHA256:p38f5YjX8z3zNU1r8/Digt/9pRcXFv2JYwhvVelC/cE

    What's fixed

    • fj auth setup-git installed a broken credential helper. Git invokes the helper via sh -c '<helper> "$@"' sh <verb>, so sed 's/^/password=/' consumed the trailing get / store / erase verb as a filename. Every git push / git pull against the configured host failed with sed: get: No such file or directory. Resolved in b68fb98, tracked in #1.

      The new helper wraps the pipeline in a shell function:

      !f() { fj auth token --host <h> | sed 's/^/password=/'; }; f
      

      Three regression tests cover the structural shape, the verb-positional simulation, and a real sh -c invocation with a stub fj on PATH.

    Recovery

    If you installed v0.1.0 via Homebrew and ran fj auth setup-git, re-run after upgrading:

    brew upgrade fj
    fj auth setup-git --host <hostname>
    

    This overwrites the broken credential.https://<host>.helper git config value with the new function-wrapped form.

    Install

    brew tap rasterandstate/tap
    brew install fj
    

    Or download a tarball below and put fj on your PATH. Verify with shasum -a 256 -c SHA256SUMS.

    Downloads
  • v0.1.0 71e536ffd8

    v0.1.0
    Some checks failed
    ci / check (push) Waiting to run
    release / build (rust:1.95-bookworm, linux-x86_64, docker, x86_64-unknown-linux-gnu) (push) Failing after 1m13s
    release / build (darwin-aarch64, macos, aarch64-apple-darwin) (push) Has been cancelled
    release / build (darwin-x86_64, macos, x86_64-apple-darwin) (push) Has been cancelled
    release / publish (push) Has been cancelled
    Stable

    stephen released this 2026-05-13 22:16:54 +00:00 | 207 commits to main since this release

    First tagged release of fj, a CLI for Forgejo in the spirit of gh.

    See CHANGELOG.md for the full list of features.

    Install

    brew tap rasterandstate/tap
    brew install fj
    

    Or download a tarball below and put fj on your PATH.

    Verify

    shasum -a 256 -c SHA256SUMS
    
    Downloads