Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Release Formula

on:
push:
tags:
- 'v*'

permissions:
contents: read

jobs:
bump-formula:
runs-on: ubuntu-latest
steps:
- name: Checkout fessctl at tag
uses: actions/checkout@v4

- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Install uv
run: pip install uv

- name: Install fessctl deps (incl. dev for jinja2)
run: uv sync --extra dev

- name: Validate tag matches pyproject version
run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/v}"
PROJECT_VERSION=$(uv run python -c \
"import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
if [ "$TAG" != "$PROJECT_VERSION" ]; then
echo "::error::Tag v$TAG does not match pyproject.toml version $PROJECT_VERSION"
exit 1
fi
echo "VERSION=$TAG" >> "$GITHUB_ENV"

- name: Export runtime requirements
run: uv export --no-dev --no-hashes --format requirements-txt > /tmp/deps.txt

- name: Render formula
env:
REPO: ${{ github.repository }}
run: |
set -euo pipefail
uv run python tools/render_formula.py \
--version "$VERSION" \
--src-tarball-url "https://github.com/${REPO}/archive/refs/tags/v${VERSION}.tar.gz" \
--uv-lock uv.lock \
--deps-file /tmp/deps.txt \
--template tools/fessctl.rb.j2 \
> /tmp/fessctl.rb
echo "--- rendered formula ---"
cat /tmp/fessctl.rb

- name: Generate GitHub App token for tap repo
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.HOMEBREW_TAP_APP_ID }}
private-key: ${{ secrets.HOMEBREW_TAP_APP_PRIVATE_KEY }}
owner: codelibs
repositories: homebrew-tap

- name: Open PR to homebrew-tap
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
BRANCH="bump-fessctl-${VERSION}"
WORKDIR=$(mktemp -d)
git clone "https://x-access-token:${GH_TOKEN}@github.com/codelibs/homebrew-tap.git" "$WORKDIR"
cd "$WORKDIR"
APP_SLUG="${{ steps.app-token.outputs.app-slug }}"
GIT_USER_ID=$(gh api "/users/${APP_SLUG}[bot]" -q .id)
git config user.name "${APP_SLUG}[bot]"
git config user.email "${GIT_USER_ID}+${APP_SLUG}[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
mkdir -p Formula
cp /tmp/fessctl.rb Formula/fessctl.rb
git add Formula/fessctl.rb
git commit -m "fessctl ${VERSION}"
git push -u origin "$BRANCH"
gh pr create \
--repo codelibs/homebrew-tap \
--base main \
--head "$BRANCH" \
--title "fessctl ${VERSION}" \
--body "Auto-generated bump for fessctl v${VERSION}.

Triggered by push to https://github.com/${REPO} tag v${VERSION}.

Source: https://github.com/${REPO}/releases/tag/v${VERSION}"
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Fess is an open-source enterprise search server based on OpenSearch.

## Installation and Usage

There are three ways to use fessctl:
There are four ways to use fessctl:

### Method 1: Using Pre-built Docker Image

Expand Down Expand Up @@ -94,9 +94,24 @@ fessctl user list
fessctl webconfig create --name TestConfig --url https://test.config.com/
```

### Method 4: Homebrew (macOS / Linuxbrew)

For a one-line install on macOS or Linuxbrew:

```bash
brew tap codelibs/tap
brew install fessctl
```

Then export the environment variables (see below) and run:

```bash
fessctl --help
```

## Environment Variables

All three methods require the following environment variables:
All four installation methods require the following environment variables:

- `FESS_ENDPOINT`: The URL of your Fess server's API endpoint (default: `http://localhost:8080`)
- `FESS_ACCESS_TOKEN`: Bearer token for API authentication (required)
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "fessctl"
version = "0.2.0.dev"
version = "0.2.0"
description = "CLI tool to manage Fess using the admin API"
authors = [{ name = "CodeLibs, Inc.", email = "info@codelibs.co" }]
requires-python = ">=3.13"
Expand All @@ -18,6 +18,7 @@ dev = [
"pytest>=7.1.0",
"testcontainers>=3.9.0",
"requests>=2.28.0",
"jinja2>=3.1.0",
]

[tool.pytest.ini_options]
Expand All @@ -26,6 +27,7 @@ testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
pythonpath = ["."]

[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
Expand Down
6 changes: 6 additions & 0 deletions tests/tools/fixtures/deps.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# This file was autogenerated by uv via the following command:
# uv export --no-dev --no-hashes --format requirements-txt
anyio==4.11.0
# via httpx
idna==3.10
# via anyio
31 changes: 31 additions & 0 deletions tests/tools/fixtures/expected_formula.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class Fessctl < Formula
include Language::Python::Virtualenv

desc "CLI tool to manage Fess via the admin API"
homepage "https://github.com/codelibs/fessctl"
url "https://github.com/codelibs/fessctl/archive/refs/tags/v0.2.0.tar.gz"
sha256 "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
license "Apache-2.0"
head "https://github.com/codelibs/fessctl.git", branch: "main"

depends_on "python@3.13"
depends_on "libyaml"

resource "anyio" do
url "https://files.pythonhosted.org/packages/c6/78/anyio-4.11.0.tar.gz"
sha256 "82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"
end

resource "idna" do
url "https://files.pythonhosted.org/packages/idna-3.10.tar.gz"
sha256 "12ac4ec9a0f0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"
end

def install
virtualenv_install_with_resources
end

test do
assert_match "fessctl", shell_output("#{bin}/fessctl --help")
end
end
25 changes: 25 additions & 0 deletions tests/tools/fixtures/uv.lock.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 95 additions & 0 deletions tests/tools/test_render_formula.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import hashlib
from pathlib import Path

import pytest

from tools.render_formula import (
NoSdistError,
compute_tarball_sha256,
find_sdist,
parse_requirements,
parse_uv_lock,
render_formula,
)

FIXTURES = Path(__file__).parent / "fixtures"


def test_parse_uv_lock_returns_dict_keyed_by_name():
pkgs = parse_uv_lock(FIXTURES / "uv.lock.toml")
assert "anyio" in pkgs
assert pkgs["anyio"]["version"] == "4.11.0"
assert pkgs["idna"]["version"] == "3.10"


def test_find_sdist_returns_url_and_sha256():
pkgs = parse_uv_lock(FIXTURES / "uv.lock.toml")
sdist = find_sdist(pkgs, "anyio")
assert sdist.url.startswith("https://files.pythonhosted.org/")
assert sdist.url.endswith("anyio-4.11.0.tar.gz")
assert sdist.sha256 == "82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"


def test_find_sdist_raises_when_no_sdist():
pkgs = parse_uv_lock(FIXTURES / "uv.lock.toml")
with pytest.raises(NoSdistError, match="wheel-only-pkg"):
find_sdist(pkgs, "wheel-only-pkg")


def test_find_sdist_raises_when_unknown_package():
pkgs = parse_uv_lock(FIXTURES / "uv.lock.toml")
with pytest.raises(KeyError, match="missing-pkg"):
find_sdist(pkgs, "missing-pkg")


def test_parse_requirements_returns_name_version_pairs():
deps = parse_requirements(FIXTURES / "deps.txt")
assert deps == [("anyio", "4.11.0"), ("idna", "3.10")]


def test_parse_requirements_ignores_comments_and_continuations():
# Lines starting with `# via` or `#` are not requirements.
deps = parse_requirements(FIXTURES / "deps.txt")
names = [n for n, _ in deps]
assert "via" not in names
assert "" not in names


def test_compute_tarball_sha256_hashes_streamed_content():
payload = b"hello fessctl source tarball"
expected = hashlib.sha256(payload).hexdigest()

def fake_stream(url: str):
# Mimic httpx.Response.iter_bytes() chunked stream
yield payload[:10]
yield payload[10:]

actual = compute_tarball_sha256(
"https://example.com/v0.2.0.tar.gz",
fetcher=fake_stream,
)
assert actual == expected


def test_render_formula_matches_golden(tmp_path):
template = Path(__file__).parents[2] / "tools" / "fessctl.rb.j2"
rendered = render_formula(
template=template,
homepage="https://github.com/codelibs/fessctl",
src_url="https://github.com/codelibs/fessctl/archive/refs/tags/v0.2.0.tar.gz",
src_sha256="deadbeef" * 8,
resources=[
{
"name": "anyio",
"url": "https://files.pythonhosted.org/packages/c6/78/anyio-4.11.0.tar.gz",
"sha256": "82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4",
},
{
"name": "idna",
"url": "https://files.pythonhosted.org/packages/idna-3.10.tar.gz",
"sha256": "12ac4ec9a0f0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0",
},
],
)
expected = (FIXTURES / "expected_formula.rb").read_text()
assert rendered == expected
Empty file added tools/__init__.py
Empty file.
26 changes: 26 additions & 0 deletions tools/fessctl.rb.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class Fessctl < Formula
include Language::Python::Virtualenv

desc "CLI tool to manage Fess via the admin API"
homepage "{{ homepage }}"
url "{{ src_url }}"
sha256 "{{ src_sha256 }}"
license "Apache-2.0"
head "https://github.com/codelibs/fessctl.git", branch: "main"

depends_on "python@3.13"
depends_on "libyaml"
{% for r in resources %}
resource "{{ r.name }}" do
url "{{ r.url }}"
sha256 "{{ r.sha256 }}"
end
{% endfor %}
def install
virtualenv_install_with_resources
end

test do
assert_match "fessctl", shell_output("#{bin}/fessctl --help")
end
end
Loading
Loading