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
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:
parent
0e88da91d1
commit
71e536ffd8
24
.forgejo/issue_template/api-gap.md
Normal file
24
.forgejo/issue_template/api-gap.md
Normal 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 ...
|
||||
```
|
||||
43
.forgejo/issue_template/bug.md
Normal file
43
.forgejo/issue_template/bug.md
Normal 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. -->
|
||||
27
.forgejo/issue_template/feature.md
Normal file
27
.forgejo/issue_template/feature.md
Normal 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. -->
|
||||
39
.forgejo/pull_request_template.md
Normal file
39
.forgejo/pull_request_template.md
Normal 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]`
|
||||
193
.forgejo/workflows/release.yml
Normal file
193
.forgejo/workflows/release.yml
Normal 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"
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -6,6 +6,25 @@ All notable changes will be recorded here. The format follows
|
|||
|
||||
## [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)
|
||||
|
||||
- `fj issue edit-comment` / `delete-comment`. Lets an agent (or you)
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -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.
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<!-- [](https://asciinema.org/a/REPLACE) -->
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
# Homebrew (macOS + Linux):
|
||||
brew tap rasterandstate/tap
|
||||
brew install fj
|
||||
|
||||
# From source:
|
||||
cargo install --path .
|
||||
# or
|
||||
cargo build --release && cp target/release/fj ~/.local/bin/fj
|
||||
|
|
|
|||
42
dist/homebrew/fj.rb.tmpl
vendored
Normal file
42
dist/homebrew/fj.rb.tmpl
vendored
Normal 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
|
||||
|
|
@ -5,6 +5,8 @@
|
|||
- [`jq.md`](jq.md) — the `fj api --jq` syntax. Dot paths, brackets,
|
||||
negative indices, pipes.
|
||||
- [`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,
|
||||
scripting, plugins.
|
||||
- [`troubleshooting.md`](troubleshooting.md) — keychain prompts, hangs,
|
||||
|
|
|
|||
67
docs/compatibility.md
Normal file
67
docs/compatibility.md
Normal 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
38
scripts/_demo-session.sh
Executable 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
51
scripts/record-demo.sh
Executable 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"
|
||||
53
scripts/render-homebrew-formula.sh
Executable file
53
scripts/render-homebrew-formula.sh
Executable 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"
|
||||
|
|
@ -285,6 +285,13 @@ async fn login(args: LoginArgs) -> Result<()> {
|
|||
.await
|
||||
.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.
|
||||
let mut hosts = Hosts::load()?;
|
||||
let host = Host {
|
||||
|
|
@ -297,14 +304,63 @@ async fn login(args: LoginArgs) -> Result<()> {
|
|||
token_store::store_token(&hostname, &token)?;
|
||||
|
||||
println!(
|
||||
"{} Logged in to {} as {}",
|
||||
"{} Logged in to {} as {}{}",
|
||||
output::bold("✓"),
|
||||
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(())
|
||||
}
|
||||
|
||||
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> {
|
||||
let resolved = crate::client::ResolvedHost {
|
||||
name: host.to_string(),
|
||||
|
|
@ -473,4 +529,27 @@ mod setup_git_tests {
|
|||
assert!(validate_username("alice").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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue