open-source readiness: release workflow, homebrew template, templates, compat doc
Some checks are pending
ci / check (push) Waiting to run
release / build (darwin-aarch64, macos, aarch64-apple-darwin) (push) Waiting to run
release / build (darwin-x86_64, macos, x86_64-apple-darwin) (push) Waiting to run
release / build (rust:1.95-bookworm, linux-x86_64, docker, x86_64-unknown-linux-gnu) (push) Waiting to run
release / publish (push) Blocked by required conditions

Release infrastructure:
* .forgejo/workflows/release.yml: on v* tags, builds darwin-aarch64,
  darwin-x86_64, linux-x86_64 tarballs, computes SHA256SUMS, uploads to
  the Forgejo release, and writes a ready-to-commit fj.rb formula.
* dist/homebrew/fj.rb.tmpl + scripts/render-homebrew-formula.sh for
  local rendering. Publishes into rasterandstate/homebrew-tap.

Issue + PR templates:
* .forgejo/issue_template/{bug,feature,api-gap}.md so triage isn't
  guessing at the user's environment.
* .forgejo/pull_request_template.md with a fmt/clippy/test checklist
  and a "what to update" surface-changes section.

README demo scaffolding:
* scripts/record-demo.sh drives asciinema through a representative
  ~30s session covering --version, repo view (auto-detect), issue/pr
  list, api, --json-fields, browse.
* README has a commented-out asciicast embed waiting for the v0.1.0
  recording.

Compatibility:
* docs/compatibility.md: tested Forgejo versions, caveats for older
  Gitea (≤1.19), Forgejo-only endpoints we expose.
* `fj auth login` now probes /api/v1/version once and warns to stderr
  when the server reports a pre-7.x version. Parser is pure-fn tested
  (modern, old, unparseable cases).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stephen Way 2026-05-13 15:09:18 -07:00
parent 0e88da91d1
commit 71e536ffd8
No known key found for this signature in database
14 changed files with 691 additions and 2 deletions

View file

@ -0,0 +1,24 @@
---
name: Forgejo API endpoint not wrapped
about: An /api/v1 endpoint fj doesn't expose yet.
labels: [enhancement, api-gap]
---
## Endpoint
<!-- e.g. `POST /repos/{owner}/{repo}/reactions` -->
## What it does
<!-- One sentence. Link the Forgejo Swagger entry if you can. -->
## Why you need it
<!-- What you're trying to script or automate. -->
## Suggested CLI surface
```sh
# Draft the fj command you'd want.
fj ...
```

View file

@ -0,0 +1,43 @@
---
name: Bug report
about: Something that should work doesn't.
labels: [bug]
---
## What happened
<!-- Describe the bug. One paragraph. -->
## What I expected
<!-- One paragraph. -->
## Reproduction
```sh
# The exact command(s). If a subcommand has dynamic input, paste it too.
fj ...
```
## Output
<details>
<summary>Stderr / error message</summary>
```
<!-- Paste the error here. Run with `--debug` if it helps. -->
```
</details>
## Environment
- `fj --version`:
- `fj api /version` (or your Forgejo version):
- OS + arch:
- Shell:
- Installed via: <!-- brew tap / cargo install / from source -->
## Anything else
<!-- Logs, screenshots, related issues. -->

View file

@ -0,0 +1,27 @@
---
name: Feature request
about: A capability fj doesn't have today.
labels: [enhancement]
---
## What
<!-- One sentence describing the feature. -->
## Why
<!-- What you're trying to accomplish. The user-level outcome, not the
implementation. -->
## How (optional)
<!-- If you've thought about the CLI surface, draft it here. e.g.
`fj <new-command> <args> --flag` -->
## Forgejo API
<!-- If this maps to a specific Forgejo API endpoint, link it. -->
## Alternatives considered
<!-- What you tried instead, or workarounds you've used. -->

View file

@ -0,0 +1,39 @@
<!--
Thanks for the PR. Before you submit:
1. `cargo fmt --all`
2. `cargo clippy --all-targets --all-features -- -D warnings`
3. `cargo test --all`
4. If you added a new subcommand or API wrapper, see CONTRIBUTING.md
for the walkthrough; also update README.md and docs/gh-to-fj.md if
the user-facing surface changed.
-->
## What
<!-- One sentence describing the change. -->
## Why
<!-- The motivation. Link any related issue. -->
## How
<!-- Implementation notes. Specific decisions you made that a reviewer
should know about. Skippable for one-liners. -->
## Test plan
- [ ] `cargo fmt --check`
- [ ] `cargo clippy --all-targets --all-features -- -D warnings`
- [ ] `cargo test --all`
- [ ] New code has a test (or here's why not):
## Surface changes
<!-- If you added/changed/removed a flag, subcommand, or output format,
list it here so the changelog entry writes itself. -->
- [ ] README.md updated (if the command table changed)
- [ ] docs/gh-to-fj.md updated (if the gh-equivalence shifted)
- [ ] CHANGELOG.md updated under `[Unreleased]`

View file

@ -0,0 +1,193 @@
name: release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- name: darwin-aarch64
runs-on: macos
target: aarch64-apple-darwin
- name: darwin-x86_64
runs-on: macos
target: x86_64-apple-darwin
- name: linux-x86_64
runs-on: docker
container: rust:1.95-bookworm
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.runs-on }}
container: ${{ matrix.container }}
steps:
- name: checkout
uses: actions/checkout@v4
- name: install rust target
run: |
rustup target add ${{ matrix.target }} || true
- name: cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: release-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
- name: build
run: |
cargo build --release --locked --target ${{ matrix.target }}
- name: package
run: |
mkdir -p dist
BIN=target/${{ matrix.target }}/release/fj
test -x "$BIN" || (echo "binary missing"; exit 1)
STAGE=fj-${{ github.ref_name }}-${{ matrix.name }}
mkdir -p "$STAGE"
cp "$BIN" "$STAGE/"
cp README.md LICENSE CHANGELOG.md "$STAGE/"
tar czf "dist/$STAGE.tar.gz" "$STAGE"
(cd dist && shasum -a 256 "$STAGE.tar.gz" > "$STAGE.tar.gz.sha256")
- name: upload artifacts
uses: actions/upload-artifact@v4
with:
name: fj-${{ matrix.name }}
path: dist/*
retention-days: 7
publish:
needs: build
runs-on: docker
container: alpine:3.20
steps:
- name: install tools
run: apk add --no-cache curl jq coreutils bash
- name: fetch artifacts
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: combined SHA256SUMS
run: |
cd dist
ls -la
# Concatenate per-artifact sha256 files into a single SHA256SUMS.
cat *.sha256 | sort > SHA256SUMS
cat SHA256SUMS
- name: create or update release
env:
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
API: ${{ github.server_url }}/api/v1
run: |
set -euo pipefail
# Look up the release by tag (Forgejo auto-creates one on tag push,
# but the API call above may run first); create if missing.
RELEASE_JSON=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \
"$API/repos/$REPO/releases/tags/$TAG" || echo "")
if [ -z "$RELEASE_JSON" ]; then
RELEASE_JSON=$(curl -sf \
-X POST \
-H "Authorization: token $FORGEJO_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"draft\":false,\"prerelease\":false}" \
"$API/repos/$REPO/releases")
fi
RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r .id)
echo "Release id: $RELEASE_ID"
cd dist
for f in fj-*.tar.gz SHA256SUMS; do
[ -f "$f" ] || continue
echo "Uploading $f"
curl -sf \
-X POST \
-H "Authorization: token $FORGEJO_TOKEN" \
-F "attachment=@$f" \
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$f"
done
- name: render homebrew formula
env:
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
HOST: ${{ github.server_url }}
run: |
set -euo pipefail
VERSION=${TAG#v}
ARM_SHA=$(awk '/darwin-aarch64\.tar\.gz$/{print $1}' dist/SHA256SUMS)
X64_SHA=$(awk '/darwin-x86_64\.tar\.gz$/{print $1}' dist/SHA256SUMS)
LIN_SHA=$(awk '/linux-x86_64\.tar\.gz$/{print $1}' dist/SHA256SUMS)
BASE="$HOST/$REPO/releases/download/$TAG"
mkdir -p homebrew-out
cat > homebrew-out/fj.rb <<RUBY
class Fj < Formula
desc "Command-line tool for Forgejo, in the spirit of gh"
homepage "$HOST/$REPO"
version "$VERSION"
license "MIT"
on_macos do
on_arm do
url "$BASE/fj-$TAG-darwin-aarch64.tar.gz"
sha256 "$ARM_SHA"
end
on_intel do
url "$BASE/fj-$TAG-darwin-x86_64.tar.gz"
sha256 "$X64_SHA"
end
end
on_linux do
url "$BASE/fj-$TAG-linux-x86_64.tar.gz"
sha256 "$LIN_SHA"
end
def install
cd "fj-$TAG-#{if OS.mac? then (Hardware::CPU.arm? ? "darwin-aarch64" : "darwin-x86_64") else "linux-x86_64" end}"
bin.install "fj"
end
test do
assert_match "fj #{version}", shell_output("#{bin}/fj --version")
end
end
RUBY
echo "Rendered formula:"
cat homebrew-out/fj.rb
- name: upload rendered formula as release asset
env:
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
API: ${{ github.server_url }}/api/v1
run: |
set -euo pipefail
RELEASE_ID=$(curl -sf \
-H "Authorization: token $FORGEJO_TOKEN" \
"$API/repos/$REPO/releases/tags/$TAG" | jq -r .id)
curl -sf \
-X POST \
-H "Authorization: token $FORGEJO_TOKEN" \
-F "attachment=@homebrew-out/fj.rb" \
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=fj.rb"

View file

@ -6,6 +6,25 @@ All notable changes will be recorded here. The format follows
## [Unreleased] ## [Unreleased]
### Added (distribution + open-source ready)
- `LICENSE` (MIT) at the repo root. Cargo.toml had always declared MIT;
the file was just missing.
- `.forgejo/workflows/release.yml`: on `v*` tags, builds for
`darwin-aarch64`, `darwin-x86_64`, `linux-x86_64`; uploads tarballs,
`SHA256SUMS`, and a pre-rendered `fj.rb` to the Forgejo release.
- Homebrew formula template at `dist/homebrew/fj.rb.tmpl` and
`scripts/render-homebrew-formula.sh` to fill SHA256s post-release.
Publishes into the existing `rasterandstate/homebrew-tap`.
- `.forgejo/issue_template/{bug,feature,api-gap}.md` and
`.forgejo/pull_request_template.md` to keep triage cheap.
- `scripts/record-demo.sh` + `scripts/_demo-session.sh` to record an
asciinema demo; README has a placeholder embed for the v0.1.0 cast.
- `docs/compatibility.md`: tested Forgejo versions, caveats for older
Gitea, and Forgejo-only endpoints.
- One-shot version probe during `fj auth login` that warns when the
server reports a pre-7.x version (with tests for the parser).
### Added (agent-focused Forgejo gaps) ### Added (agent-focused Forgejo gaps)
- `fj issue edit-comment` / `delete-comment`. Lets an agent (or you) - `fj issue edit-comment` / `delete-comment`. Lets an agent (or you)

View file

@ -4,9 +4,21 @@ A command-line tool for [Forgejo](https://forgejo.org) instances, in the spirit
Multi-host from day one. Tokens are stored in your OS keychain. Multi-host from day one. Tokens are stored in your OS keychain.
<!--
TODO: replace the line below with an asciinema embed once the v0.1.0
release is tagged. Run `./scripts/record-demo.sh` then
`asciinema upload dist/demo.cast` and paste the returned URL.
-->
<!-- [![asciicast](https://asciinema.org/a/REPLACE.svg)](https://asciinema.org/a/REPLACE) -->
## Install ## Install
```sh ```sh
# Homebrew (macOS + Linux):
brew tap rasterandstate/tap
brew install fj
# From source:
cargo install --path . cargo install --path .
# or # or
cargo build --release && cp target/release/fj ~/.local/bin/fj cargo build --release && cp target/release/fj ~/.local/bin/fj

42
dist/homebrew/fj.rb.tmpl vendored Normal file
View file

@ -0,0 +1,42 @@
class Fj < Formula
desc "Command-line tool for Forgejo, in the spirit of gh"
homepage "https://rasterhub.com/rasterstate/fj"
version "@@VERSION@@"
license "MIT"
on_macos do
on_arm do
url "https://rasterhub.com/rasterstate/fj/releases/download/v@@VERSION@@/fj-v@@VERSION@@-darwin-aarch64.tar.gz"
sha256 "@@SHA_DARWIN_AARCH64@@"
end
on_intel do
url "https://rasterhub.com/rasterstate/fj/releases/download/v@@VERSION@@/fj-v@@VERSION@@-darwin-x86_64.tar.gz"
sha256 "@@SHA_DARWIN_X86_64@@"
end
end
on_linux do
url "https://rasterhub.com/rasterstate/fj/releases/download/v@@VERSION@@/fj-v@@VERSION@@-linux-x86_64.tar.gz"
sha256 "@@SHA_LINUX_X86_64@@"
end
def install
target = if OS.mac?
Hardware::CPU.arm? ? "darwin-aarch64" : "darwin-x86_64"
else
"linux-x86_64"
end
cd "fj-v@@VERSION@@-#{target}"
bin.install "fj"
# Optional: completions and man pages if present.
bash_completion.install "completions/fj.bash" if File.exist?("completions/fj.bash")
zsh_completion.install "completions/_fj" if File.exist?("completions/_fj")
fish_completion.install "completions/fj.fish" if File.exist?("completions/fj.fish")
man1.install Dir["man/*.1"] if Dir.exist?("man")
end
test do
assert_match "fj @@VERSION@@", shell_output("#{bin}/fj --version")
assert_match "Command-line tool for Forgejo", shell_output("#{bin}/fj --help")
end
end

View file

@ -5,6 +5,8 @@
- [`jq.md`](jq.md) — the `fj api --jq` syntax. Dot paths, brackets, - [`jq.md`](jq.md) — the `fj api --jq` syntax. Dot paths, brackets,
negative indices, pipes. negative indices, pipes.
- [`gh-to-fj.md`](gh-to-fj.md) — command-by-command mapping from gh. - [`gh-to-fj.md`](gh-to-fj.md) — command-by-command mapping from gh.
- [`compatibility.md`](compatibility.md) — Forgejo version matrix and
known caveats on older Gitea.
- [`faq.md`](faq.md) — common questions about tokens, hosts, debug, - [`faq.md`](faq.md) — common questions about tokens, hosts, debug,
scripting, plugins. scripting, plugins.
- [`troubleshooting.md`](troubleshooting.md) — keychain prompts, hangs, - [`troubleshooting.md`](troubleshooting.md) — keychain prompts, hangs,

67
docs/compatibility.md Normal file
View file

@ -0,0 +1,67 @@
# Forgejo compatibility
fj is built against the **Forgejo 7.x API surface**. We test against
`7.0.16+gitea-1.21.11` on rasterhub.com. Most endpoints are inherited
unchanged from Gitea, so old Gitea instances will largely work too,
but there are sharp edges:
## Tested
| Server | Version | Status |
| -------------------- | -------------------------------- | -------------- |
| Forgejo | 7.0.x (we test 7.0.16) | full support |
| Forgejo | 8.x | expected to work |
| Forgejo | 9.x / 10.x | expected to work |
| Gitea | 1.21.x (the base of Forgejo 7.0) | mostly works |
| Gitea | 1.20.x and earlier | YMMV, see below |
If you run fj against an untested version and find something broken,
please open an [issue](../.forgejo/issue_template/bug.md) with
`fj api /version` output.
## Known caveats on older Gitea (≤ 1.19)
These features rely on endpoints introduced in or after Gitea 1.20.
`fj` will return an HTTP 404 with the endpoint path.
- `fj pr ready` (`PATCH /repos/.../pulls/{n}` with `draft: false`).
- `fj repo mirror-sync` (`POST /repos/.../mirror-sync`).
- `fj milestone` group on instances that didn't expose
`/repos/.../milestones/{id}` for editing.
- `fj search code` (`/repos/search/code` is Forgejo 7+ only).
For older instances, the typed API still works via `fj api <path>` if
you know the right path.
## Forgejo-only endpoints
These are fully implemented and exposed through fj. They are
Forgejo extensions of the Gitea base.
- Branch protection rules (`fj protect`)
- Mirrors (`fj repo mirror`)
- Topics (`fj repo topics`)
- Webhooks (`fj hook`) — present in Gitea too, identical surface
- Actions runs / secrets / variables (`fj run`, `fj secret`,
`fj variable`)
## How fj detects versions
On `fj auth login`, fj calls `/api/v1/version` once and stores the
version string. If the version looks pre-7.x, fj prints a warning to
stderr so you know what to expect. After login, the stored version
is consulted only by `fj auth status`.
If you want to know the version of a configured host:
```sh
fj api /version
# or
fj auth status | grep Version
```
## Bumping the supported floor
If we drop support for a Forgejo version, it'll be called out under
"BREAKING CHANGES" in CHANGELOG.md ahead of the release, and we'll
emit a stronger error (not just a warning) from the version probe.

38
scripts/_demo-session.sh Executable file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Drives an asciinema recording. Don't invoke directly; run record-demo.sh.
set -u
PS1='\$ '
export PS1
PAUSE_BETWEEN=0.6
PAUSE_AFTER=1.2
say() { printf '\033[1;34m# %s\033[0m\n' "$*"; sleep "$PAUSE_BETWEEN"; }
do_() { printf '\033[1m$ %s\033[0m\n' "$*"; sleep "$PAUSE_BETWEEN"; eval "$@"; sleep "$PAUSE_AFTER"; }
clear
say "fj: a CLI for Forgejo. Multi-host, tokens in the keychain."
do_ "fj --version"
say "Inside a clone, no flags needed: fj infers the repo from your git remote."
do_ "fj repo view | head -8"
say "Issues, PRs, releases all work the same way."
do_ "fj issue list --state all -L 5"
do_ "fj pr list --state all -L 5"
say "The api escape hatch with a jq-ish projector."
do_ "fj api /version"
do_ "fj api /user -q .login"
say "Selective JSON for scripting."
do_ "fj repo list -L 3 --json --json-fields full_name,private"
say "Or just browse on the web."
say " fj browse src/main.rs"
sleep 1.5
clear
say "60+ subcommands. Try fj --help."
sleep 2

51
scripts/record-demo.sh Executable file
View file

@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Record an asciinema demo of fj. The result is a .cast file you can:
# - Upload to asciinema.org via `asciinema upload dist/demo.cast`
# - Embed as a static SVG/GIF via `agg` (https://github.com/asciinema/agg)
# - Link from README.md
#
# Requires: asciinema, fj (installed and authenticated), `jq` for nicer output.
#
# Usage:
# ./scripts/record-demo.sh # records dist/demo.cast
# ./scripts/record-demo.sh foo.cast # records to foo.cast
#
# The session below is deliberately short (~30 seconds) and covers what
# someone reading the README in their first 10 seconds cares about.
set -euo pipefail
OUT="${1:-dist/demo.cast}"
mkdir -p "$(dirname "$OUT")"
if ! command -v asciinema >/dev/null 2>&1; then
cat >&2 <<EOF
asciinema not found.
macOS: brew install asciinema
Linux: pipx install asciinema
EOF
exit 1
fi
if ! command -v fj >/dev/null 2>&1; then
echo "fj not found on PATH; build it first (cargo build --release)" >&2
exit 1
fi
# Drive a representative session through asciinema.
# Idle pauses keep readability up; trim them in post if you want a snappier
# embedded GIF.
asciinema rec \
--overwrite \
--idle-time-limit 2 \
--title "fj: a CLI for Forgejo" \
--command "bash $(dirname "$0")/_demo-session.sh" \
"$OUT"
echo ""
echo "✓ Recorded $OUT"
echo ""
echo "Next:"
echo " asciinema play $OUT # preview"
echo " asciinema upload $OUT # publish (returns a URL)"
echo " agg $OUT $OUT.gif # render to GIF"

View file

@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Render dist/homebrew/fj.rb from the template + a release's SHA256SUMS.
#
# Usage:
# ./scripts/render-homebrew-formula.sh <version>
#
# Reads SHA256SUMS from the Forgejo release for the given version and writes
# a ready-to-commit fj.rb. Then paste that into rasterandstate/homebrew-tap.
#
# Requires: curl, awk, jq.
set -euo pipefail
VERSION="${1:?version required, e.g. 0.1.0}"
TAG="v${VERSION#v}"
VERSION="${VERSION#v}"
REPO="rasterstate/fj"
HOST="https://rasterhub.com"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
SUMS_URL="$HOST/$REPO/releases/download/$TAG/SHA256SUMS"
echo "fetching $SUMS_URL"
curl -fsSL "$SUMS_URL" -o "$TMP/SHA256SUMS"
extract() {
awk -v pat="$1" '$2 ~ pat {print $1; exit}' "$TMP/SHA256SUMS"
}
SHA_ARM=$(extract "darwin-aarch64\\.tar\\.gz$")
SHA_X64=$(extract "darwin-x86_64\\.tar\\.gz$")
SHA_LIN=$(extract "linux-x86_64\\.tar\\.gz$")
for s in "$SHA_ARM" "$SHA_X64" "$SHA_LIN"; do
[ -n "$s" ] || { echo "missing SHA in SHA256SUMS"; cat "$TMP/SHA256SUMS"; exit 1; }
done
OUT="dist/homebrew/fj.rb"
sed \
-e "s/@@VERSION@@/$VERSION/g" \
-e "s/@@SHA_DARWIN_AARCH64@@/$SHA_ARM/g" \
-e "s/@@SHA_DARWIN_X86_64@@/$SHA_X64/g" \
-e "s/@@SHA_LINUX_X86_64@@/$SHA_LIN/g" \
dist/homebrew/fj.rb.tmpl > "$OUT"
echo "wrote $OUT"
echo ""
echo "next:"
echo " cp $OUT ~/path/to/homebrew-tap/Formula/fj.rb"
echo " cd ~/path/to/homebrew-tap"
echo " git commit -am 'fj $VERSION'"
echo " git push"

View file

@ -285,6 +285,13 @@ async fn login(args: LoginArgs) -> Result<()> {
.await .await
.context("verifying token against /api/v1/user")?; .context("verifying token against /api/v1/user")?;
// One-shot version probe. Surfaces obvious version mismatches before
// they manifest as cryptic 404s on later commands. Failure here is
// non-fatal: some Forgejo-compatible servers may not expose /version
// identically.
let version_string = probe_version(&probe_client).await.unwrap_or_default();
warn_on_old_forgejo(&version_string);
// Persist host + token. // Persist host + token.
let mut hosts = Hosts::load()?; let mut hosts = Hosts::load()?;
let host = Host { let host = Host {
@ -297,14 +304,63 @@ async fn login(args: LoginArgs) -> Result<()> {
token_store::store_token(&hostname, &token)?; token_store::store_token(&hostname, &token)?;
println!( println!(
"{} Logged in to {} as {}", "{} Logged in to {} as {}{}",
output::bold(""), output::bold(""),
output::bold(&hostname), output::bold(&hostname),
output::bold(&me.login) output::bold(&me.login),
if version_string.is_empty() {
String::new()
} else {
format!(" ({})", output::dim(&version_string))
}
); );
Ok(()) Ok(())
} }
async fn probe_version(client: &Client) -> Result<String> {
#[derive(serde::Deserialize)]
struct V {
version: String,
}
let v: V = client
.json(reqwest::Method::GET, "/api/v1/version", &[], None::<&()>)
.await?;
Ok(v.version)
}
/// `version_string` looks like "7.0.16+gitea-1.21.11" or "1.21.11". We warn
/// when the leading numeric component looks pre-7.x. Anything we can't parse
/// gets a silent pass since exotic Forgejo-likes still mostly work.
fn warn_on_old_forgejo(version_string: &str) {
if let Some(msg) = version_warning(version_string) {
eprintln!("{} {msg}", output::bold("warning:"));
}
}
/// Pure helper. Returns the human-readable warning (without the "warning:"
/// prefix) when the server's version is older than the fj baseline. Returns
/// `None` when the version is unparseable or fresh enough.
fn version_warning(version_string: &str) -> Option<String> {
if version_string.is_empty() {
return None;
}
let head = version_string
.split(|c: char| !(c.is_ascii_digit() || c == '.'))
.next()
.unwrap_or("");
let major: u32 = head
.split('.')
.next()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if major == 0 || major >= 7 {
return None;
}
Some(format!(
"server reports version `{version_string}`, which is older than the Forgejo 7.x baseline fj is tested against. Some commands (pr ready, repo mirror-sync, search code, milestone) may 404. See docs/compatibility.md."
))
}
fn build_probe_client(host: &str, cfg: &Host, token: &str) -> Result<Client> { fn build_probe_client(host: &str, cfg: &Host, token: &str) -> Result<Client> {
let resolved = crate::client::ResolvedHost { let resolved = crate::client::ResolvedHost {
name: host.to_string(), name: host.to_string(),
@ -473,4 +529,27 @@ mod setup_git_tests {
assert!(validate_username("alice").is_ok()); assert!(validate_username("alice").is_ok());
assert!(validate_username("alice.bob_123").is_ok()); assert!(validate_username("alice.bob_123").is_ok());
} }
#[test]
fn version_warning_fires_on_old() {
assert!(version_warning("1.21.11").is_some());
assert!(version_warning("5.0.0").is_some());
assert!(version_warning("6.0.0+gitea-1.20.0").is_some());
}
#[test]
fn version_warning_silent_on_modern() {
assert!(version_warning("7.0.16+gitea-1.21.11").is_none());
assert!(version_warning("8.0.0").is_none());
assert!(version_warning("10.5.2-beta").is_none());
}
#[test]
fn version_warning_silent_on_unparseable() {
assert!(version_warning("").is_none());
assert!(version_warning("totally-unknown").is_none());
// Hardcoded 0.x major never triggers the warning either (likely a
// dev / preview build we don't want to be noisy about).
assert!(version_warning("0.99.0").is_none());
}
} }