Compare commits
3
Commits
ee7b266d45
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9123461dc | ||
|
|
949e8971d5 | ||
|
|
22011ce6ac |
@@ -0,0 +1,71 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
# One Linux runner produces all three archives. build.py normalises
|
||||
# everything that would otherwise vary by build host — file modes, tar
|
||||
# ownership, and launcher line endings — so a Windows zip built here is
|
||||
# equivalent to one built on Windows. That removes the three-runner matrix,
|
||||
# the artifact hand-off between jobs, and the race of three jobs trying to
|
||||
# create the same release.
|
||||
#
|
||||
# Multi-OS runners would still earn their keep for *testing*, which is a
|
||||
# different question from building.
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# build.py copies only what "git ls-files" reports, so it needs real
|
||||
# history rather than a tarball export.
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
# No pip install: build.py and publish_gitea.py are both stdlib-only, so
|
||||
# the release path cannot be broken by a dependency it does not ship.
|
||||
- name: Build all three archives
|
||||
run: python packaging/build.py --platform all
|
||||
|
||||
# Tag pushes publish; a manual run stops after the build above. Guarding
|
||||
# the step rather than the job is what keeps workflow_dispatch usable as
|
||||
# a dry run — the archives still get built and the logs still show their
|
||||
# sizes, without a release being created.
|
||||
- name: Publish to Gitea
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GITEA_URL: ${{ github.server_url }}
|
||||
GITEA_REPOSITORY: ${{ github.repository }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
# Leaves a draft. Review the archives, then publish from the web UI, or
|
||||
# add --publish here once you trust the pipeline.
|
||||
run: >-
|
||||
python packaging/publish_gitea.py
|
||||
--tag "${{ github.ref_name }}"
|
||||
dist/LM-Gambit-*.zip dist/LM-Gambit-*.tar.gz
|
||||
|
||||
# Without this the dry run is only half a test: the archives are built,
|
||||
# listed, and then destroyed with the job container, so nobody can open
|
||||
# one. On a tag the archives reach the release instead, which is why this
|
||||
# is scoped to the tagless path.
|
||||
- name: Upload archives for inspection
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: LM-Gambit-archives
|
||||
path: dist/LM-Gambit-*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Dry run note
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
echo "No tag — archives built and uploaded as artifacts, not published."
|
||||
ls -lh dist/
|
||||
@@ -0,0 +1,80 @@
|
||||
name: tests-macos
|
||||
|
||||
# Separate from tests.yml on purpose. This runs on a laptop, and a laptop is
|
||||
# sometimes asleep. Keeping it in its own workflow means a queued macOS job
|
||||
# leaves *this* run pending instead of holding the main test result hostage.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: tests-macos-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
macos:
|
||||
# Homebrew act_runner executes jobs directly on the host — there is no
|
||||
# container.
|
||||
#
|
||||
# Cased exactly as the runner declared it. Gitea compares runs-on against
|
||||
# the registered label string, and rather than depend on that comparison
|
||||
# being case-insensitive, matching the declaration is guaranteed either way.
|
||||
# The runner also carries Apple-Silicon if a job ever needs to be specific
|
||||
# about the architecture rather than the OS.
|
||||
runs-on: MacOS-Latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# No setup-python / setup-node here, deliberately. Those actions install
|
||||
# a toolchain into a container that does not exist in host mode. Using
|
||||
# whatever Homebrew provides is also the better test: it is the
|
||||
# environment an actual macOS user would have, rather than a pinned one
|
||||
# nobody runs.
|
||||
- name: Show toolchain
|
||||
run: |
|
||||
python3 --version
|
||||
node --version
|
||||
npm --version
|
||||
uname -m
|
||||
|
||||
# A venv rather than --user: Homebrew's Python is externally managed
|
||||
# (PEP 668), so a plain pip install into it is refused outright.
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python3 -m venv .ci-venv
|
||||
.ci-venv/bin/python -m pip install --quiet --upgrade pip
|
||||
.ci-venv/bin/python -m pip install -r requirements-ci.txt
|
||||
|
||||
# requirements-ci.txt omits llama-cpp-python, so this covers suite
|
||||
# loading, grading, reporting and the HTTP surface — not inference.
|
||||
# Exercising .apple_silicon needs model weights CI does not have, and
|
||||
# remains a manual check.
|
||||
- name: Run Python tests
|
||||
run: .ci-venv/bin/python tests_py/run_all.py
|
||||
|
||||
- name: Install web dependencies
|
||||
working-directory: web
|
||||
run: npm ci
|
||||
|
||||
- name: Type-check and build
|
||||
working-directory: web
|
||||
run: npm run build
|
||||
|
||||
- name: Run UI tests
|
||||
working-directory: web
|
||||
run: npm test
|
||||
|
||||
# Proves the shipped artifact actually works on macOS, which is a
|
||||
# different claim from "it built". Catches launcher and layout problems
|
||||
# that never show up in a source checkout.
|
||||
- name: Smoke-test the packaged archive
|
||||
run: |
|
||||
.ci-venv/bin/python packaging/build.py --platform macos --skip-web
|
||||
cd dist
|
||||
tar xzf LM-Gambit-*-macos.tar.gz
|
||||
cd LM-Gambit-*-macos
|
||||
test -x LM-Gambit.sh || { echo "launcher is not executable"; exit 1; }
|
||||
head -1 LM-Gambit.sh | grep -q '^#!/usr/bin/env bash$' || { echo "bad shebang"; exit 1; }
|
||||
../../.ci-venv/bin/python auto-test.py --suites
|
||||
@@ -0,0 +1,61 @@
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: tests-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
python:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# 3.11 is what this is developed against. 3.10 is the floor the README
|
||||
# advertises, so it is checked rather than assumed.
|
||||
python: ["3.10", "3.11"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
# requirements-ci.txt deliberately omits llama-cpp-python: it needs a C
|
||||
# compiler, dominates install time, and nothing under tests_py/ imports
|
||||
# it. The tests cover suite loading, grading, reporting and the HTTP
|
||||
# surface — none of which touch local inference.
|
||||
- name: Install
|
||||
run: python -m pip install -r requirements-ci.txt
|
||||
|
||||
# test_api.py skips itself when nothing is listening on :8765, so the
|
||||
# other eight files run and the suite still reports success.
|
||||
- name: Run tests
|
||||
run: python tests_py/run_all.py
|
||||
|
||||
web:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
|
||||
# Runs tsc as well as the bundler, so a type error fails here.
|
||||
- name: Type-check and build
|
||||
run: npm run build
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
@@ -467,7 +467,8 @@ from one origin.
|
||||
## Building a release
|
||||
|
||||
```bash
|
||||
python packaging/build.py # archive for the current platform
|
||||
python packaging/build.py --platform all # the whole release set
|
||||
python packaging/build.py # just the current platform
|
||||
python packaging/build.py --platform linux # or windows / macos
|
||||
python packaging/build.py --skip-web # reuse an existing web/dist
|
||||
```
|
||||
@@ -483,16 +484,27 @@ Only files `git ls-files` reports get copied, plus `web/dist`. Local models,
|
||||
results, settings and virtual environments cannot leak into an archive even
|
||||
if they are sitting in the working tree.
|
||||
|
||||
**Via GitHub Actions** — `.github/workflows/release.yml` builds all three on
|
||||
`windows-latest`, `ubuntu-latest` and `macos-latest`. Pushing a `v*` tag builds
|
||||
the archives and opens a **draft** release with them attached; `workflow_dispatch`
|
||||
builds and uploads artifacts without creating a release, which is the way to
|
||||
test the pipeline.
|
||||
### In CI
|
||||
|
||||
Workflows exist for both forges. Pushing a `v*` tag produces a **draft** release
|
||||
with the three archives attached; `workflow_dispatch` builds them without
|
||||
creating a release, which is how to test the pipeline before a tag exists.
|
||||
|
||||
```bash
|
||||
git tag v2.0.0 && git push origin v2.0.0
|
||||
```
|
||||
|
||||
| | Builds on | Publishes with |
|
||||
|---|---|---|
|
||||
| `.github/workflows/release.yml` | `windows-latest`, `ubuntu-latest`, `macos-latest` | `softprops/action-gh-release` |
|
||||
| `.gitea/workflows/release.yml` | one `ubuntu-latest` runner | `packaging/publish_gitea.py` |
|
||||
|
||||
The Gitea one needs only a single runner because the cross-build makes the
|
||||
archives independent of build host — see
|
||||
[packaging/GITEA-SETUP.md](packaging/GITEA-SETUP.md) for runner registration and
|
||||
the `GITEA_TOKEN` secret. `publish_gitea.py` is stdlib-only and idempotent:
|
||||
re-running on the same tag replaces assets rather than duplicating them.
|
||||
|
||||
---
|
||||
|
||||
## Extending
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# Running CI on a self-hosted Gitea
|
||||
|
||||
The workflows in `.gitea/workflows/` mirror the GitHub ones in
|
||||
`.github/workflows/`, with one structural difference: **releases build on a
|
||||
single Linux runner** rather than three.
|
||||
|
||||
That is not a compromise. `packaging/build.py` normalises everything that would
|
||||
otherwise vary by build host — tar member modes, uid/gid, and launcher line
|
||||
endings — so the Windows `.zip` produced on Linux is equivalent to one produced
|
||||
on Windows. Dropping the matrix also drops the artifact hand-off between jobs
|
||||
and the race of three jobs racing to create the same release.
|
||||
|
||||
Multi-OS runners would still be worth adding for *testing*. Building does not
|
||||
need them.
|
||||
|
||||
---
|
||||
|
||||
## 1. Enable Actions
|
||||
|
||||
In `app.ini`:
|
||||
|
||||
```ini
|
||||
[actions]
|
||||
ENABLED = true
|
||||
DEFAULT_ACTIONS_URL = https://github.com
|
||||
```
|
||||
|
||||
`DEFAULT_ACTIONS_URL` is where `actions/checkout` and friends are fetched from.
|
||||
Public repositories clone anonymously, so a locked or absent GitHub account does
|
||||
not affect this. Restart Gitea afterwards.
|
||||
|
||||
## 2. Register a runner
|
||||
|
||||
Get a registration token from **Site Administration → Actions → Runners**, then:
|
||||
|
||||
```bash
|
||||
docker run -d --restart always --name gitea-runner \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v /srv/gitea-runner:/data \
|
||||
-e GITEA_INSTANCE_URL=https://git.example.lan \
|
||||
-e GITEA_RUNNER_REGISTRATION_TOKEN=<token> \
|
||||
-e GITEA_RUNNER_LABELS=ubuntu-latest:docker://catthehacker/ubuntu:act-22.04 \
|
||||
gitea/act_runner:latest
|
||||
```
|
||||
|
||||
The label mapping is the part that matters. `runs-on: ubuntu-latest` in the
|
||||
workflow resolves through it to a container image. `catthehacker/ubuntu` images
|
||||
carry the tooling GitHub's hosted runners have; the bare `ubuntu:22.04` image
|
||||
does not, and `actions/setup-python` will fail on it for want of basic
|
||||
utilities.
|
||||
|
||||
Mounting the Docker socket lets the runner start job containers. It also grants
|
||||
effective root on the host, which is the usual trade — acceptable on a machine
|
||||
that only serves this purpose, worth knowing about regardless.
|
||||
|
||||
## 3. Add the release token
|
||||
|
||||
Releases are published through Gitea's own API by
|
||||
`packaging/publish_gitea.py`, so the job needs a token:
|
||||
|
||||
1. **Settings → Applications → Generate New Token**, scope `write:repository`.
|
||||
2. In the repository: **Settings → Actions → Secrets → Add Secret**, named
|
||||
`GITEA_TOKEN`.
|
||||
|
||||
`GITEA_URL` and `GITEA_REPOSITORY` come from `github.server_url` and
|
||||
`github.repository`, which Gitea populates for compatibility. Nothing else to
|
||||
configure.
|
||||
|
||||
## 4. If the runner cannot reach Gitea
|
||||
|
||||
Worth understanding before it bites, because the failure is confusing.
|
||||
|
||||
`ROOT_URL` is what Gitea advertises to the world, and job containers inherit it
|
||||
as `github.server_url` — so `actions/checkout` and `publish_gitea.py` both call
|
||||
the *public* address, even though the runner may be sitting on the same host as
|
||||
Gitea. When that address only resolves outside your network, jobs fail at
|
||||
checkout with a DNS or connection error while Gitea itself is plainly fine.
|
||||
|
||||
Two ways out.
|
||||
|
||||
**Let it hairpin.** If your router does NAT loopback, the request leaves and
|
||||
comes straight back to the reverse proxy, and nothing needs configuring. Try
|
||||
this first — it is the common case and costs nothing to test.
|
||||
|
||||
**Pin the name to a local address.** Map the public hostname to your reverse
|
||||
proxy's LAN address, on the runner and on the containers it spawns:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
gitea-runner:
|
||||
extra_hosts:
|
||||
- "git.example.lan:192.0.2.10" # the reverse proxy, not Gitea
|
||||
```
|
||||
|
||||
and in act_runner's `config.yaml`, so job containers inherit it:
|
||||
|
||||
```yaml
|
||||
container:
|
||||
options: --add-host=git.example.lan:192.0.2.10
|
||||
```
|
||||
|
||||
The trap is which host to point at. It must be whatever terminates the scheme
|
||||
in `ROOT_URL`. If `ROOT_URL` is `https://…` then the override has to reach
|
||||
something serving TLS on 443 — your reverse proxy. Pointing it straight at the
|
||||
Gitea host sends an HTTPS request to a port where Gitea is only speaking plain
|
||||
HTTP, and the connection is refused.
|
||||
|
||||
Also: do not put Gitea behind a forward-auth proxy (Authentik outposts,
|
||||
oauth2-proxy and friends). Runners authenticate with their own token and cannot
|
||||
satisfy an SSO interstitial, so registration and every job will fail. Use the
|
||||
identity provider as an OIDC source *inside* Gitea's own login instead.
|
||||
|
||||
## 5. Cut a release
|
||||
|
||||
Dry run first — `workflow_dispatch` builds all three archives and prints their
|
||||
sizes without creating anything:
|
||||
|
||||
**Actions → release → Run workflow**
|
||||
|
||||
Then tag:
|
||||
|
||||
```bash
|
||||
git tag v2.0.0 && git push origin v2.0.0
|
||||
```
|
||||
|
||||
That leaves a **draft** release with the three archives attached. Review, then
|
||||
publish from the web UI. Re-running the workflow on the same tag replaces the
|
||||
assets rather than duplicating them, so a half-finished publish is safe to
|
||||
retry.
|
||||
|
||||
To publish immediately instead of drafting, add `--publish` to the
|
||||
`publish_gitea.py` invocation in `.gitea/workflows/release.yml`.
|
||||
|
||||
---
|
||||
|
||||
## Notes for a DL380p Gen8
|
||||
|
||||
Those E5-2600 v1/v2 chips are **x86-64-v2** — they have AVX but not AVX2, which
|
||||
arrived with the v3 (Haswell) generation. Two consequences:
|
||||
|
||||
- **Pick the distribution with that in mind.** Debian, Ubuntu and RHEL 9 target
|
||||
a baseline these CPUs meet. RHEL 10 and recent Fedora raised their floor to
|
||||
x86-64-v3 and will not run. Node 22 and Docker are fine either way.
|
||||
|
||||
- **Don't plan to run benchmarks on this box.** Many prebuilt
|
||||
`llama-cpp-python` wheels assume AVX2, and the ones that don't will be slow
|
||||
on 2012 silicon regardless. It is a fine CI and git server; keep inference on
|
||||
the workstation. Nothing in `tests_py/` needs `llama-cpp-python`, which is
|
||||
exactly why `requirements-ci.txt` omits it.
|
||||
|
||||
Two dual-socket Sandy/Ivy Bridge sockets have plenty of throughput for Gitea
|
||||
plus a runner — this is an I/O and RAM workload, not a vector-math one.
|
||||
+21
-9
@@ -192,23 +192,35 @@ def _normalize(info: tarfile.TarInfo) -> tarfile.TarInfo:
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--platform", choices=sorted(PLATFORMS), default=current_platform())
|
||||
parser.add_argument(
|
||||
"--platform",
|
||||
choices=[*sorted(PLATFORMS), "all"],
|
||||
default=current_platform(),
|
||||
help="Target platform, or 'all' for a complete release set",
|
||||
)
|
||||
parser.add_argument("--skip-web", action="store_true", help="Reuse an existing web/dist")
|
||||
args = parser.parse_args()
|
||||
|
||||
version = read_version()
|
||||
print(f"LM-Gambit {version} -> {args.platform}")
|
||||
targets = sorted(PLATFORMS) if args.platform == "all" else [args.platform]
|
||||
|
||||
version = read_version()
|
||||
print(f"LM-Gambit {version} -> {', '.join(targets)}")
|
||||
|
||||
# Once, not once per target: the bundle is identical on every platform.
|
||||
if not args.skip_web:
|
||||
build_web()
|
||||
|
||||
staging = DIST / f"_staging-{args.platform}"
|
||||
stage(args.platform, staging)
|
||||
out = archive(args.platform, staging, version)
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
built = []
|
||||
for target in targets:
|
||||
staging = DIST / f"_staging-{target}"
|
||||
stage(target, staging)
|
||||
built.append(archive(target, staging, version))
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
|
||||
size_mb = out.stat().st_size / (1024 * 1024)
|
||||
print(f"\n {out.relative_to(ROOT)} ({size_mb:.1f} MB)")
|
||||
print()
|
||||
for out in built:
|
||||
size_mb = out.stat().st_size / (1024 * 1024)
|
||||
print(f" {out.relative_to(ROOT)} ({size_mb:.1f} MB)")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
"""Publish release archives to a Gitea instance.
|
||||
|
||||
python packaging/publish_gitea.py --tag v2.0.0 dist/*.zip dist/*.tar.gz
|
||||
|
||||
Creates the release if it does not exist, then uploads each file as an asset.
|
||||
Re-running replaces assets of the same name rather than erroring, so a failed
|
||||
half-finished publish can simply be run again.
|
||||
|
||||
Reads configuration from the environment, which Gitea Actions populates:
|
||||
|
||||
GITEA_URL instance root, e.g. https://git.example.lan
|
||||
(falls back to GITHUB_SERVER_URL, which Gitea sets)
|
||||
GITEA_REPOSITORY "owner/repo" (falls back to GITHUB_REPOSITORY)
|
||||
GITEA_TOKEN an access token with write:repository
|
||||
|
||||
**Why not a marketplace action.** ``softprops/action-gh-release`` targets
|
||||
GitHub's API and does not work against Gitea. The Gitea-flavoured forks exist
|
||||
but pin you to a third party for the one step that matters most, on a forge
|
||||
whose API is small enough to call directly. urllib covers it in ~150 lines with
|
||||
nothing to install and nothing to trust.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
TIMEOUT = 120
|
||||
|
||||
|
||||
class PublishError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _request(
|
||||
method: str,
|
||||
url: str,
|
||||
token: str,
|
||||
*,
|
||||
body: bytes | None = None,
|
||||
content_type: str | None = None,
|
||||
) -> dict | list | None:
|
||||
request = urllib.request.Request(url, data=body, method=method)
|
||||
request.add_header("Authorization", f"token {token}")
|
||||
request.add_header("Accept", "application/json")
|
||||
if content_type:
|
||||
request.add_header("Content-Type", content_type)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
|
||||
payload = response.read()
|
||||
except urllib.error.HTTPError as error:
|
||||
# The response body carries Gitea's actual complaint; without it the
|
||||
# caller only sees "HTTP Error 422" and has to guess.
|
||||
detail = error.read().decode("utf-8", "replace").strip()
|
||||
raise PublishError(f"{method} {url} -> {error.code} {error.reason}\n{detail}") from None
|
||||
except urllib.error.URLError as error:
|
||||
raise PublishError(f"{method} {url} -> {error.reason}") from None
|
||||
|
||||
return json.loads(payload) if payload else None
|
||||
|
||||
|
||||
def find_or_create_release(api: str, token: str, tag: str, *, draft: bool, notes: str) -> dict:
|
||||
try:
|
||||
return _request("GET", f"{api}/releases/tags/{tag}", token)
|
||||
except PublishError as error:
|
||||
if "404" not in str(error):
|
||||
raise
|
||||
|
||||
print(f"Creating release {tag}")
|
||||
body = json.dumps(
|
||||
{"tag_name": tag, "name": tag, "body": notes, "draft": draft, "prerelease": False}
|
||||
).encode()
|
||||
return _request("POST", f"{api}/releases", token, body=body, content_type="application/json")
|
||||
|
||||
|
||||
def upload_asset(api: str, token: str, release: dict, path: Path) -> None:
|
||||
# Uploading over an existing name appends rather than replaces, which would
|
||||
# leave two assets called LM-Gambit-2.0.0-linux.tar.gz after a re-run.
|
||||
for asset in release.get("assets") or []:
|
||||
if asset.get("name") == path.name:
|
||||
print(f" replacing existing {path.name}")
|
||||
_request("DELETE", f"{api}/releases/{release['id']}/assets/{asset['id']}", token)
|
||||
|
||||
boundary = f"----LMGambit{uuid.uuid4().hex}"
|
||||
mime = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
|
||||
body = b"".join(
|
||||
[
|
||||
f"--{boundary}\r\n".encode(),
|
||||
f'Content-Disposition: form-data; name="attachment"; filename="{path.name}"\r\n'.encode(),
|
||||
f"Content-Type: {mime}\r\n\r\n".encode(),
|
||||
path.read_bytes(),
|
||||
f"\r\n--{boundary}--\r\n".encode(),
|
||||
]
|
||||
)
|
||||
|
||||
url = f"{api}/releases/{release['id']}/assets?name={urllib.parse.quote(path.name)}"
|
||||
_request(
|
||||
"POST",
|
||||
url,
|
||||
token,
|
||||
body=body,
|
||||
content_type=f"multipart/form-data; boundary={boundary}",
|
||||
)
|
||||
print(f" uploaded {path.name} ({path.stat().st_size / (1024 * 1024):.1f} MB)")
|
||||
|
||||
|
||||
DEFAULT_NOTES = """\
|
||||
Download the archive for your platform, extract it, and run the launcher
|
||||
inside — `LM-Gambit.bat` on Windows, `./LM-Gambit.sh` on macOS and Linux.
|
||||
|
||||
The first run creates a virtual environment and installs dependencies; that
|
||||
takes a few minutes and happens once. It needs **Python 3.10 or newer** on the
|
||||
machine. See `GPU-ACCELERATION.md` in the archive for CUDA, Metal and ROCm.
|
||||
|
||||
Scores read as *"correct where checkable, well-formed elsewhere"* — 16 of the
|
||||
26 built-in questions have answer keys, and the rest are graded on form.
|
||||
"""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("files", nargs="+", type=Path, help="Archives to attach")
|
||||
parser.add_argument("--tag", required=True, help="Tag to release, e.g. v2.0.0")
|
||||
parser.add_argument(
|
||||
"--publish",
|
||||
action="store_true",
|
||||
help="Publish immediately instead of leaving a draft for review",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
url = os.environ.get("GITEA_URL") or os.environ.get("GITHUB_SERVER_URL")
|
||||
repository = os.environ.get("GITEA_REPOSITORY") or os.environ.get("GITHUB_REPOSITORY")
|
||||
token = os.environ.get("GITEA_TOKEN")
|
||||
|
||||
missing = [
|
||||
name
|
||||
for name, value in (
|
||||
("GITEA_URL", url),
|
||||
("GITEA_REPOSITORY", repository),
|
||||
("GITEA_TOKEN", token),
|
||||
)
|
||||
if not value
|
||||
]
|
||||
if missing:
|
||||
print(f"Missing environment: {', '.join(missing)}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
missing_files = [str(path) for path in args.files if not path.is_file()]
|
||||
if missing_files:
|
||||
print(f"No such file: {', '.join(missing_files)}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
api = f"{url.rstrip('/')}/api/v1/repos/{repository}"
|
||||
|
||||
try:
|
||||
release = find_or_create_release(
|
||||
api, token, args.tag, draft=not args.publish, notes=DEFAULT_NOTES
|
||||
)
|
||||
for path in args.files:
|
||||
upload_asset(api, token, release, path)
|
||||
except PublishError as error:
|
||||
print(f"\n{error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
state = "published" if args.publish else "draft"
|
||||
print(f"\n{args.tag} {state}: {url.rstrip('/')}/{repository}/releases/tag/{args.tag}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user