From 093417c052d3fd3ae415eee00315fc5206ce2c20 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:04:14 +0900 Subject: [PATCH 01/14] chore(deps): add jinja2 dev dependency for formula renderer --- pyproject.toml | 1 + uv.lock | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e2c29f8..455c6f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/uv.lock b/uv.lock index e04ccb2..9f6e848 100644 --- a/uv.lock +++ b/uv.lock @@ -112,6 +112,7 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "jinja2" }, { name = "pytest" }, { name = "requests" }, { name = "testcontainers" }, @@ -120,6 +121,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "httpx", specifier = "==0.28.1" }, + { name = "jinja2", marker = "extra == 'dev'", specifier = ">=3.1.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.1.0" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "requests", marker = "extra == 'dev'", specifier = ">=2.28.0" }, @@ -183,6 +185,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -195,6 +209,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" From 8ce82a4ae495db0924fa5c1309821c453346b47e Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:06:21 +0900 Subject: [PATCH 02/14] feat(tools): add Homebrew formula Jinja2 template --- tools/__init__.py | 0 tools/fessctl.rb.j2 | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 tools/__init__.py create mode 100644 tools/fessctl.rb.j2 diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/fessctl.rb.j2 b/tools/fessctl.rb.j2 new file mode 100644 index 0000000..9ee2ede --- /dev/null +++ b/tools/fessctl.rb.j2 @@ -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 From b5cd46bbc0d2beaafe1c7b939cb05ce49b50a0f5 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:13:42 +0900 Subject: [PATCH 03/14] feat(tools): add uv.lock parser for formula renderer --- pyproject.toml | 1 + tests/tools/fixtures/uv.lock.toml | 25 +++++++++++++++++++ tests/tools/test_render_formula.py | 38 ++++++++++++++++++++++++++++ tools/render_formula.py | 40 ++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 tests/tools/fixtures/uv.lock.toml create mode 100644 tests/tools/test_render_formula.py create mode 100644 tools/render_formula.py diff --git a/pyproject.toml b/pyproject.toml index 455c6f2..b57d99e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] +pythonpath = ["."] [build-system] requires = ["setuptools>=61.0.0", "wheel"] diff --git a/tests/tools/fixtures/uv.lock.toml b/tests/tools/fixtures/uv.lock.toml new file mode 100644 index 0000000..33220ae --- /dev/null +++ b/tests/tools/fixtures/uv.lock.toml @@ -0,0 +1,25 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094 } + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/idna-3.10.tar.gz", hash = "sha256:12ac4ec9a0f0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0", size = 190000 } + +[[package]] +name = "wheel-only-pkg" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +# No sdist — wheel-only diff --git a/tests/tools/test_render_formula.py b/tests/tools/test_render_formula.py new file mode 100644 index 0000000..45d8c99 --- /dev/null +++ b/tests/tools/test_render_formula.py @@ -0,0 +1,38 @@ +from pathlib import Path + +import pytest + +from tools.render_formula import ( + NoSdistError, + find_sdist, + parse_uv_lock, +) + +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") diff --git a/tools/render_formula.py b/tools/render_formula.py new file mode 100644 index 0000000..7dbc4e6 --- /dev/null +++ b/tools/render_formula.py @@ -0,0 +1,40 @@ +"""Render Homebrew formula for fessctl from uv.lock.""" + +from __future__ import annotations + +import tomllib +from dataclasses import dataclass +from pathlib import Path + + +class NoSdistError(Exception): + """Raised when a package has no sdist available in uv.lock.""" + + +@dataclass(frozen=True) +class Sdist: + url: str + sha256: str + + +def parse_uv_lock(path: Path) -> dict[str, dict]: + """Read uv.lock and return packages keyed by name.""" + with open(path, "rb") as f: + data = tomllib.load(f) + return {pkg["name"]: pkg for pkg in data.get("package", [])} + + +def find_sdist(packages: dict[str, dict], name: str) -> Sdist: + """Extract sdist URL and sha256 for `name`. Raise if missing.""" + if name not in packages: + raise KeyError(name) + sdist = packages[name].get("sdist") + if not sdist: + raise NoSdistError( + f"package {name!r} has no sdist in uv.lock; " + "Language::Python::Virtualenv requires sdist tarballs" + ) + raw_hash = sdist["hash"] + if raw_hash.startswith("sha256:"): + raw_hash = raw_hash[len("sha256:"):] + return Sdist(url=sdist["url"], sha256=raw_hash) From fd328802940704653eb354af31b857ce43043d15 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:18:13 +0900 Subject: [PATCH 04/14] docs(tools): clarify find_sdist error contract in docstring --- tools/render_formula.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/render_formula.py b/tools/render_formula.py index 7dbc4e6..f0e2088 100644 --- a/tools/render_formula.py +++ b/tools/render_formula.py @@ -25,7 +25,11 @@ def parse_uv_lock(path: Path) -> dict[str, dict]: def find_sdist(packages: dict[str, dict], name: str) -> Sdist: - """Extract sdist URL and sha256 for `name`. Raise if missing.""" + """Extract sdist URL and sha256 for `name`. + + Raises KeyError if `name` is not in packages. + Raises NoSdistError if the package exists but has no sdist entry. + """ if name not in packages: raise KeyError(name) sdist = packages[name].get("sdist") From 35f8308c89df7b33a2d0e4759b3264fe47096b57 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:19:30 +0900 Subject: [PATCH 05/14] feat(tools): parse uv-exported requirements for formula renderer --- tests/tools/fixtures/deps.txt | 6 ++++++ tests/tools/test_render_formula.py | 14 ++++++++++++++ tools/render_formula.py | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 tests/tools/fixtures/deps.txt diff --git a/tests/tools/fixtures/deps.txt b/tests/tools/fixtures/deps.txt new file mode 100644 index 0000000..3baef9d --- /dev/null +++ b/tests/tools/fixtures/deps.txt @@ -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 diff --git a/tests/tools/test_render_formula.py b/tests/tools/test_render_formula.py index 45d8c99..82436d9 100644 --- a/tests/tools/test_render_formula.py +++ b/tests/tools/test_render_formula.py @@ -7,6 +7,7 @@ find_sdist, parse_uv_lock, ) +from tools.render_formula import parse_requirements FIXTURES = Path(__file__).parent / "fixtures" @@ -36,3 +37,16 @@ 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 diff --git a/tools/render_formula.py b/tools/render_formula.py index f0e2088..c6d02da 100644 --- a/tools/render_formula.py +++ b/tools/render_formula.py @@ -42,3 +42,22 @@ def find_sdist(packages: dict[str, dict], name: str) -> Sdist: if raw_hash.startswith("sha256:"): raw_hash = raw_hash[len("sha256:"):] return Sdist(url=sdist["url"], sha256=raw_hash) + + +def parse_requirements(path: Path) -> list[tuple[str, str]]: + """Parse `name==version` lines from a uv-exported requirements file. + + Ignores blank lines, comments, and indented continuation lines (`# via ...`). + """ + results: list[tuple[str, str]] = [] + for raw_line in path.read_text().splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + if "==" not in line: + continue + name, _, rest = line.partition("==") + # Strip trailing inline comments and whitespace + version = rest.split(";", 1)[0].split("#", 1)[0].strip() + results.append((name.strip(), version)) + return results From ece837c409cb2e02c6191559d6d46abb17d85555 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:21:24 +0900 Subject: [PATCH 06/14] feat(tools): add streaming sha256 fetcher for source tarball --- tests/tools/test_render_formula.py | 22 ++++++++++++++++++++++ tools/render_formula.py | 23 +++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/tests/tools/test_render_formula.py b/tests/tools/test_render_formula.py index 82436d9..109e668 100644 --- a/tests/tools/test_render_formula.py +++ b/tests/tools/test_render_formula.py @@ -50,3 +50,25 @@ def test_parse_requirements_ignores_comments_and_continuations(): names = [n for n, _ in deps] assert "via" not in names assert "" not in names + + +import hashlib +from io import BytesIO + +from tools.render_formula import compute_tarball_sha256 + + +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 diff --git a/tools/render_formula.py b/tools/render_formula.py index c6d02da..1b4a311 100644 --- a/tools/render_formula.py +++ b/tools/render_formula.py @@ -2,10 +2,14 @@ from __future__ import annotations +import hashlib import tomllib +from collections.abc import Callable, Iterable from dataclasses import dataclass from pathlib import Path +import httpx + class NoSdistError(Exception): """Raised when a package has no sdist available in uv.lock.""" @@ -61,3 +65,22 @@ def parse_requirements(path: Path) -> list[tuple[str, str]]: version = rest.split(";", 1)[0].split("#", 1)[0].strip() results.append((name.strip(), version)) return results + + +def _default_fetcher(url: str) -> Iterable[bytes]: + """Stream bytes from a URL, yielding chunks.""" + with httpx.stream("GET", url, follow_redirects=True, timeout=60.0) as resp: + resp.raise_for_status() + yield from resp.iter_bytes() + + +def compute_tarball_sha256( + url: str, + *, + fetcher: Callable[[str], Iterable[bytes]] = _default_fetcher, +) -> str: + """Stream a tarball from `url` and return its sha256 hex digest.""" + h = hashlib.sha256() + for chunk in fetcher(url): + h.update(chunk) + return h.hexdigest() From c4d056a08774a02018ef5d9e8fcd4a91021b23a2 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:24:36 +0900 Subject: [PATCH 07/14] feat(tools): render Homebrew formula via Jinja2 template --- tests/tools/fixtures/expected_formula.rb | 31 +++++++++++++++++++++ tests/tools/test_render_formula.py | 35 +++++++++++++++++++----- tools/render_formula.py | 26 ++++++++++++++++++ 3 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 tests/tools/fixtures/expected_formula.rb diff --git a/tests/tools/fixtures/expected_formula.rb b/tests/tools/fixtures/expected_formula.rb new file mode 100644 index 0000000..d19acb4 --- /dev/null +++ b/tests/tools/fixtures/expected_formula.rb @@ -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 diff --git a/tests/tools/test_render_formula.py b/tests/tools/test_render_formula.py index 109e668..7203bbf 100644 --- a/tests/tools/test_render_formula.py +++ b/tests/tools/test_render_formula.py @@ -1,13 +1,16 @@ +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, ) -from tools.render_formula import parse_requirements FIXTURES = Path(__file__).parent / "fixtures" @@ -52,12 +55,6 @@ def test_parse_requirements_ignores_comments_and_continuations(): assert "" not in names -import hashlib -from io import BytesIO - -from tools.render_formula import compute_tarball_sha256 - - def test_compute_tarball_sha256_hashes_streamed_content(): payload = b"hello fessctl source tarball" expected = hashlib.sha256(payload).hexdigest() @@ -72,3 +69,27 @@ def fake_stream(url: str): 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 diff --git a/tools/render_formula.py b/tools/render_formula.py index 1b4a311..7ece13b 100644 --- a/tools/render_formula.py +++ b/tools/render_formula.py @@ -9,6 +9,7 @@ from pathlib import Path import httpx +from jinja2 import Environment, FileSystemLoader, StrictUndefined class NoSdistError(Exception): @@ -84,3 +85,28 @@ def compute_tarball_sha256( for chunk in fetcher(url): h.update(chunk) return h.hexdigest() + + +def render_formula( + *, + template: Path, + homepage: str, + src_url: str, + src_sha256: str, + resources: list[dict], +) -> str: + """Render the Homebrew formula text from a Jinja2 template.""" + env = Environment( + loader=FileSystemLoader(template.parent), + undefined=StrictUndefined, + keep_trailing_newline=True, + trim_blocks=False, + lstrip_blocks=False, + ) + tmpl = env.get_template(template.name) + return tmpl.render( + homepage=homepage, + src_url=src_url, + src_sha256=src_sha256, + resources=sorted(resources, key=lambda r: r["name"]), + ) From 6bdaa0cede5fa251d1ea58f3c085b5496a850429 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:27:28 +0900 Subject: [PATCH 08/14] feat(tools): add CLI entry point to formula renderer --- tools/render_formula.py | 61 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tools/render_formula.py b/tools/render_formula.py index 7ece13b..90ef51d 100644 --- a/tools/render_formula.py +++ b/tools/render_formula.py @@ -2,7 +2,9 @@ from __future__ import annotations +import argparse import hashlib +import sys import tomllib from collections.abc import Callable, Iterable from dataclasses import dataclass @@ -110,3 +112,62 @@ def render_formula( src_sha256=src_sha256, resources=sorted(resources, key=lambda r: r["name"]), ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Render the fessctl Homebrew formula from uv.lock and a deps file.", + ) + parser.add_argument("--version", required=True, help="fessctl version, e.g. 0.2.0") + parser.add_argument( + "--src-tarball-url", + required=True, + help="URL of the source release tarball", + ) + parser.add_argument( + "--uv-lock", + type=Path, + default=Path("uv.lock"), + help="Path to uv.lock (default: ./uv.lock)", + ) + parser.add_argument( + "--deps-file", + type=Path, + required=True, + help="Path to uv-exported requirements.txt", + ) + parser.add_argument( + "--template", + type=Path, + default=Path("tools/fessctl.rb.j2"), + help="Jinja2 template path", + ) + parser.add_argument( + "--homepage", + default="https://github.com/codelibs/fessctl", + ) + args = parser.parse_args(argv) + + packages = parse_uv_lock(args.uv_lock) + runtime_deps = parse_requirements(args.deps_file) + + resources = [] + for name, _version in runtime_deps: + sdist = find_sdist(packages, name) + resources.append({"name": name, "url": sdist.url, "sha256": sdist.sha256}) + + src_sha256 = compute_tarball_sha256(args.src_tarball_url) + + rendered = render_formula( + template=args.template, + homepage=args.homepage, + src_url=args.src_tarball_url, + src_sha256=src_sha256, + resources=resources, + ) + sys.stdout.write(rendered) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 519c6dc71f3da74ae104c0892fd0633630288782 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:29:48 +0900 Subject: [PATCH 09/14] ci: add release workflow that bumps Homebrew formula on v* tags --- .github/workflows/release.yml | 90 +++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7aba3d5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +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: Open PR to homebrew-tap + env: + GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + if [ -z "${GH_TOKEN}" ]; then + echo "::error::HOMEBREW_TAP_TOKEN secret is not configured" + exit 1 + fi + BRANCH="bump-fessctl-${VERSION}" + WORKDIR=$(mktemp -d) + git clone "https://x-access-token:${GH_TOKEN}@github.com/codelibs/homebrew-tap.git" "$WORKDIR" + cd "$WORKDIR" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[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}" From a26908e08d03380f496512557ba015634b1f0676 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:33:12 +0900 Subject: [PATCH 10/14] docs(readme): add Homebrew installation method --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 835fd6a..f7b9d11 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,21 @@ 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: From a23fe56e1a83a51d37db2c31da25e3cfd716ae2f Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:33:52 +0900 Subject: [PATCH 11/14] docs(readme): update method count to four --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7b9d11..1473adf 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ 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) From 79fc5475b08e46e7b3ed12d5d313177ed93d6682 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:34:29 +0900 Subject: [PATCH 12/14] chore(release): bump version to 0.2.0 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b57d99e..33f13f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/uv.lock b/uv.lock index 9f6e848..0bbe6aa 100644 --- a/uv.lock +++ b/uv.lock @@ -102,7 +102,7 @@ wheels = [ [[package]] name = "fessctl" -version = "0.2.0.dev0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "httpx" }, From f23e64c6477b1a99fe68686c8e67843017217850 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Sun, 3 May 2026 22:40:04 +0900 Subject: [PATCH 13/14] docs(readme): update install method count in intro --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1473adf..662de89 100644 --- a/README.md +++ b/README.md @@ -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 From b3129a6f456268ff61413bfeb93430cf15852d70 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Mon, 4 May 2026 08:02:38 +0900 Subject: [PATCH 14/14] ci(release): use GitHub App token for cross-repo PR Switch from a fine-grained PAT (HOMEBREW_TAP_TOKEN) to a GitHub App installation token via actions/create-github-app-token. This avoids PAT rotation and scopes access to the tap repo only. Requires repo settings on codelibs/fessctl: - variable HOMEBREW_TAP_APP_ID - secret HOMEBREW_TAP_APP_PRIVATE_KEY --- .github/workflows/release.yml | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7aba3d5..bf42911 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,22 +56,29 @@ jobs: 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: ${{ secrets.HOMEBREW_TAP_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} REPO: ${{ github.repository }} run: | set -euo pipefail - if [ -z "${GH_TOKEN}" ]; then - echo "::error::HOMEBREW_TAP_TOKEN secret is not configured" - exit 1 - fi BRANCH="bump-fessctl-${VERSION}" WORKDIR=$(mktemp -d) git clone "https://x-access-token:${GH_TOKEN}@github.com/codelibs/homebrew-tap.git" "$WORKDIR" cd "$WORKDIR" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + 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