From 4d09f803038d6a25f665460751d20fe7afd3024e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 18:46:24 +0000 Subject: [PATCH 1/2] feat: publish the models on Hugging Face Mirror flycoder0.3, flycoder0.3fast and flycoder0.3pro on Hugging Face after each publication on ollama.com. scripts/publish-hf.sh downloads the published layers from the Ollama registry (GGUF weights, vision projector, system prompt, sampling settings, license), checks their SHA-256 and uploads them with a model card from docs/huggingface/. The publish-huggingface workflow runs it with the HF_TOKEN secret, under HF_NAMESPACE or the token's owner. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cw7NFZ1kjsaaLELfRZLFv9 --- .github/workflows/publish-huggingface.yml | 66 +++++++++++++++ Documentation.md | 29 ++++++- docs/huggingface/flycoder0.3.md | 46 +++++++++++ docs/huggingface/flycoder0.3fast.md | 46 +++++++++++ docs/huggingface/flycoder0.3pro.md | 46 +++++++++++ readme.md | 6 +- scripts/publish-hf.sh | 99 +++++++++++++++++++++++ tests/modelfile.test.mjs | 19 ++++- 8 files changed, 352 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/publish-huggingface.yml create mode 100644 docs/huggingface/flycoder0.3.md create mode 100644 docs/huggingface/flycoder0.3fast.md create mode 100644 docs/huggingface/flycoder0.3pro.md create mode 100755 scripts/publish-hf.sh diff --git a/.github/workflows/publish-huggingface.yml b/.github/workflows/publish-huggingface.yml new file mode 100644 index 0000000..af7c141 --- /dev/null +++ b/.github/workflows/publish-huggingface.yml @@ -0,0 +1,66 @@ +name: publish-huggingface +# After each successful publication on ollama.com, the same models are mirrored on Hugging Face: +# weights, vision projector, system prompt, sampling settings and the model card from docs/huggingface/. +# Needs one repository secret (Settings > Secrets and variables > Actions): +# HF_TOKEN a Hugging Face token with write access (https://huggingface.co/settings/tokens) +# and optionally one repository variable: +# HF_NAMESPACE the Hugging Face user or organization to publish under (default: the owner of HF_TOKEN) +on: + workflow_run: + workflows: [publish-ollama] + types: [completed] + branches: [main] + push: + branches: [main] + paths: + - 'docs/huggingface/**' + - 'scripts/publish-hf.sh' + - '.github/workflows/publish-huggingface.yml' + workflow_dispatch: + inputs: + models: + description: 'Models to publish (space-separated names, or "all")' + default: all +concurrency: + group: publish-huggingface + cancel-in-progress: false +jobs: + publish: + if: github.repository == 'Tromset/flycoder' && (github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success') + runs-on: ubuntu-latest + timeout-minutes: 180 + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + HF_NAMESPACE: ${{ vars.HF_NAMESPACE }} + steps: + - uses: actions/checkout@v4 + + - name: Check the secret + run: | + [ -n "$HF_TOKEN" ] || { echo "::error::Add the HF_TOKEN secret (a write token from https://huggingface.co/settings/tokens)."; exit 1; } + + - name: Free disk space for the largest model + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force >/dev/null + df -h / + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install huggingface_hub + run: python3 -m pip install --quiet 'huggingface_hub>=0.32,<2' + + - name: Publish on Hugging Face + env: + REQUESTED: ${{ inputs.models }} + run: | + models=${REQUESTED:-all} + # models is a space-separated list of names: split it on purpose. + # shellcheck disable=SC2086 + if [ "$models" = all ]; then sh scripts/publish-hf.sh "$HF_NAMESPACE"; else sh scripts/publish-hf.sh "$HF_NAMESPACE" $models; fi diff --git a/Documentation.md b/Documentation.md index e8fca46..0052cb7 100644 --- a/Documentation.md +++ b/Documentation.md @@ -370,7 +370,30 @@ Updating a page uses the same request as the page's Edit button; Ollama does not By hand, from any machine signed in to the account: `sh scripts/publish.sh` (all models) or `sh scripts/publish.sh Tromset flycoder0.3fast` (one model), then `OLLAMA_API_KEY=... sh scripts/update-ollama-pages.sh`. -For a new version, update the version in `package.json`, `install.sh`, `scripts/publish.sh`, the system prompt of the Modelfiles and the pages. `npm test` checks that they agree. +For a new version, update the version in `package.json`, `install.sh`, `scripts/publish.sh`, `scripts/publish-hf.sh`, the system prompt of the Modelfiles and the pages. `npm test` checks that they agree. + +### 8.1 Publishing on Hugging Face + +After each successful run of publish-ollama, the [publish-huggingface](.github/workflows/publish-huggingface.yml) workflow mirrors `flycoder0.3`, `flycoder0.3fast` and `flycoder0.3pro` on Hugging Face. It also runs when `docs/huggingface/` changes, and by hand from the Actions tab. For each model, `scripts/publish-hf.sh` downloads the published layers of `Tromset/` from the ollama.com registry, checks their SHA-256, and uploads to `/`: + +| File | Content | +|---|---| +| `-Q4_K_M.gguf` | the weights, byte for byte the ollama.com file | +| `mmproj-.gguf` | the vision projector | +| `system`, `params` | the system prompt and the sampling settings, which Ollama applies to `ollama run hf.co//` | +| `LICENSE` | the base's Apache 2.0 license | +| `README.md` | the model card, from `docs/huggingface/.md` (`@HF_NAMESPACE@` is replaced by the namespace) | + +The template layer is not copied: on ollama.com it is a placeholder and Ollama formats Qwen with its built-in renderer. From Hugging Face, Ollama and llama.cpp (`--jinja`) use the chat template inside the GGUF; for tool calling and thinking in Ollama, `Tromset/` stays the reference build. + +One-time setup, in Settings > Secrets and variables > Actions: + +| Name | Kind | Value | +|---|---|---| +| `HF_TOKEN` | secret | a token with write access, created at https://huggingface.co/settings/tokens | +| `HF_NAMESPACE` | variable, optional | the user or organization to publish under; by default, the owner of the token | + +By hand: `pip install huggingface_hub`, then `HF_TOKEN=... sh scripts/publish-hf.sh` (all models, under the token's owner) or `HF_TOKEN=... sh scripts/publish-hf.sh flycoder0.3fast`. The largest model needs 18 GB of free disk. ## 9. Troubleshooting @@ -386,6 +409,7 @@ For a new version, update the version in `package.json`, `install.sh`, `scripts/ | Endless repetition | temperature changed to a value that is too low | go back to the Modelfile's settings | | `npm run bench`: "Reference solution failed" | `python3` or `node` missing, or sandbox refused | `xcode-select --install`, Node 22, or `--no-sandbox` | | The publish workflow fails at "Check the secrets" | `OLLAMA_KEY` is missing | add it (section 8) | +| publish-huggingface fails at "Check the secret" | `HF_TOKEN` is missing | add it (section 8.1) | ## 10. Development @@ -400,11 +424,12 @@ For a new version, update the version in `package.json`, `install.sh`, `scripts/ | `install.sh` | one-command installer | | `scripts/publish.sh`, `scripts/update-ollama-pages.sh` | publishing models and pages on ollama.com | | `docs/ollama/` | the ollama.com pages, one file per model | +| `docs/huggingface/`, `scripts/publish-hf.sh` | the Hugging Face model cards and their publisher | | `bench/bench.mjs`, `bench/problems.mjs` | benchmark and problems | | `bench/baselines/` | 0.1 profile, for comparison | | `docs/bench/` | reference results | | `tests/` | tests for the bench, the Modelfiles, the scripts, the pages and FlyBrain | -| `.github/workflows/test.yml`, `.github/workflows/publish-ollama.yml` | CI on Linux and macOS, automatic publishing | +| `.github/workflows/test.yml`, `.github/workflows/publish-ollama.yml`, `.github/workflows/publish-huggingface.yml` | CI on Linux and macOS, automatic publishing | | `training/`, `scripts/build_language_dataset.js`, `server/fly-language.js` | former FlyBrain voice pipeline, independent of FlyCoder; the voice speaks French by design | ### 10.2 Tests diff --git a/docs/huggingface/flycoder0.3.md b/docs/huggingface/flycoder0.3.md new file mode 100644 index 0000000..6c16d2a --- /dev/null +++ b/docs/huggingface/flycoder0.3.md @@ -0,0 +1,46 @@ +--- +license: apache-2.0 +base_model: Qwen/Qwen3.5-9B +base_model_relation: quantized +pipeline_tag: image-text-to-text +tags: + - gguf + - ollama + - llama.cpp + - code + - flycoder +--- + +# FlyCoder 0.3 + +A local coding model for Macs with 16 GB of memory. Complete, runnable code, a large context, and it answers in your language. + +```sh +ollama run hf.co/@HF_NAMESPACE@/flycoder0.3 +``` + +| Base | Size | Mac | Context | +|---|---|---|---| +| [Qwen3.5 9B](https://huggingface.co/Qwen/Qwen3.5-9B) (Apache 2.0) | 6.6 GB | 16 GB | 64K tokens | + +- **Code that runs**: exact names and signatures, every import, edge cases handled, no "TODO", no invented APIs. +- **Large context**: 64K tokens out of the box, enough for whole files and coding agents. +- **Thinks first**: reasoning is on by default; `/set nothink` in Ollama for instant answers. + +## Files + +- `flycoder0.3-Q4_K_M.gguf`: the weights, the same file as [`Tromset/flycoder0.3`](https://ollama.com/Tromset/flycoder0.3) on ollama.com. +- `mmproj-flycoder0.3.gguf`: the vision projector, for images. +- `system`, `params`: FlyCoder's system prompt and sampling settings, applied by Ollama. + +With llama.cpp, pass the same settings and the contents of `system` as the system message: + +```sh +llama-server -hf @HF_NAMESPACE@/flycoder0.3 --jinja -c 65536 --temp 0.6 --top-k 20 --top-p 0.95 --min-p 0 +``` + +For tool calling and thinking in Ollama, `ollama run Tromset/flycoder0.3` uses Ollama's built-in Qwen renderer and is the reference build. + +Other sizes: [`flycoder0.3fast`](https://huggingface.co/@HF_NAMESPACE@/flycoder0.3fast) for 8 GB Macs, [`flycoder0.3pro`](https://huggingface.co/@HF_NAMESPACE@/flycoder0.3pro) for 32 GB Macs or more. + +Source, benchmark and installer: https://github.com/Tromset/flycoder diff --git a/docs/huggingface/flycoder0.3fast.md b/docs/huggingface/flycoder0.3fast.md new file mode 100644 index 0000000..1db53af --- /dev/null +++ b/docs/huggingface/flycoder0.3fast.md @@ -0,0 +1,46 @@ +--- +license: apache-2.0 +base_model: Qwen/Qwen3.5-4B +base_model_relation: quantized +pipeline_tag: image-text-to-text +tags: + - gguf + - ollama + - llama.cpp + - code + - flycoder +--- + +# FlyCoder 0.3 fast + +The fast FlyCoder, for Macs with 8 GB of memory or when speed matters most. + +```sh +ollama run hf.co/@HF_NAMESPACE@/flycoder0.3fast +``` + +| Base | Size | Mac | Context | +|---|---|---|---| +| [Qwen3.5 4B](https://huggingface.co/Qwen/Qwen3.5-4B) (Apache 2.0) | 3.3 GB | 8 GB | 32K tokens | + +- **Fast**: about twice the speed of `flycoder0.3`, with the same coding rules. +- **Code that runs**: exact names and signatures, every import, edge cases handled, no "TODO", no invented APIs. +- **Light**: fits next to your other apps on an 8 GB Mac. + +## Files + +- `flycoder0.3fast-Q4_K_M.gguf`: the weights, the same file as [`Tromset/flycoder0.3fast`](https://ollama.com/Tromset/flycoder0.3fast) on ollama.com. +- `mmproj-flycoder0.3fast.gguf`: the vision projector, for images. +- `system`, `params`: FlyCoder's system prompt and sampling settings, applied by Ollama. + +With llama.cpp, pass the same settings and the contents of `system` as the system message: + +```sh +llama-server -hf @HF_NAMESPACE@/flycoder0.3fast --jinja -c 32768 --temp 0.6 --top-k 20 --top-p 0.95 --min-p 0 +``` + +For tool calling and thinking in Ollama, `ollama run Tromset/flycoder0.3fast` uses Ollama's built-in Qwen renderer and is the reference build. + +Other sizes: [`flycoder0.3`](https://huggingface.co/@HF_NAMESPACE@/flycoder0.3) for 16 GB Macs, [`flycoder0.3pro`](https://huggingface.co/@HF_NAMESPACE@/flycoder0.3pro) for 32 GB Macs or more. + +Source, benchmark and installer: https://github.com/Tromset/flycoder diff --git a/docs/huggingface/flycoder0.3pro.md b/docs/huggingface/flycoder0.3pro.md new file mode 100644 index 0000000..47d3b2b --- /dev/null +++ b/docs/huggingface/flycoder0.3pro.md @@ -0,0 +1,46 @@ +--- +license: apache-2.0 +base_model: Qwen/Qwen3.8-27B +base_model_relation: quantized +pipeline_tag: image-text-to-text +tags: + - gguf + - ollama + - llama.cpp + - code + - flycoder +--- + +# FlyCoder 0.3 pro + +The strongest FlyCoder, for hard coding work and coding agents, on Macs with 32 GB of memory or more. + +```sh +ollama run hf.co/@HF_NAMESPACE@/flycoder0.3pro +``` + +| Base | Size | Mac | Context | +|---|---|---|---| +| [Qwen3.8 27B](https://huggingface.co/Qwen/Qwen3.8-27B) (Apache 2.0) | 18 GB | 32 GB | 64K tokens | + +- **Code that runs**: exact names and signatures, every import, edge cases handled, a final check before answering, no "TODO", no invented APIs. +- **Built for agents**: reads files before editing, makes minimal changes, reports real test output, never runs a destructive command without asking. +- **Fast for its size**: multi-token prediction is on in Ollama, and 64K tokens of context cost only about 4.4 GB thanks to Qwen3.8's hybrid attention. + +## Files + +- `flycoder0.3pro-Q4_K_M.gguf`: the weights, the same file as [`Tromset/flycoder0.3pro`](https://ollama.com/Tromset/flycoder0.3pro) on ollama.com. +- `mmproj-flycoder0.3pro.gguf`: the vision projector, for images. +- `system`, `params`: FlyCoder's system prompt and sampling settings, applied by Ollama. + +With llama.cpp, pass the same settings and the contents of `system` as the system message: + +```sh +llama-server -hf @HF_NAMESPACE@/flycoder0.3pro --jinja -c 65536 --temp 0.6 --top-k 20 --top-p 0.95 --min-p 0 +``` + +For tool calling and thinking in Ollama, `ollama run Tromset/flycoder0.3pro` uses Ollama's built-in Qwen renderer and is the reference build. + +Other sizes: [`flycoder0.3`](https://huggingface.co/@HF_NAMESPACE@/flycoder0.3) for 16 GB Macs, [`flycoder0.3fast`](https://huggingface.co/@HF_NAMESPACE@/flycoder0.3fast) for 8 GB Macs. + +Source, benchmark and installer: https://github.com/Tromset/flycoder diff --git a/readme.md b/readme.md index 2b1fdf5..00455bd 100644 --- a/readme.md +++ b/readme.md @@ -63,12 +63,14 @@ Details: [Documentation.md, section 5.5](Documentation.md#55-flybrain-the-router Every change to `main` that touches a Modelfile or a page in `docs/ollama/` is published on ollama.com by the [publish-ollama](.github/workflows/publish-ollama.yml) workflow: models first, then their pages. It needs two repository secrets, `OLLAMA_KEY` and `OLLAMA_API_KEY` ([Documentation.md, section 8](Documentation.md#8-publishing-on-ollamacom)). +Each publication is then mirrored on Hugging Face by the [publish-huggingface](.github/workflows/publish-huggingface.yml) workflow: the same GGUF weights, vision projector, system prompt and sampling settings, with a model card from `docs/huggingface/`, runnable with `ollama run hf.co//flycoder0.3` or llama.cpp. It needs the `HF_TOKEN` secret ([Documentation.md, section 8.1](Documentation.md#81-publishing-on-hugging-face)). + ## Repository contents - `Modelfile`, `Modelfile.fast`, `Modelfile.pro`: the three variants. `Modelfile.lite`, `Modelfile.router`: FlyBrain's experts. -- `docs/ollama/`: the ollama.com page of each model. +- `docs/ollama/`: the ollama.com page of each model. `docs/huggingface/`: the Hugging Face model card of each model. - `brain/`: the FlyBrain server and its routing rules. -- `install.sh`: one-command install. `scripts/publish.sh`, `scripts/update-ollama-pages.sh`: publishing. +- `install.sh`: one-command install. `scripts/publish.sh`, `scripts/update-ollama-pages.sh`, `scripts/publish-hf.sh`: publishing. - `bench/`: quality and speed benchmark. `tests/`: `npm test`. - `training/`, `scripts/build_language_dataset.js`, `server/fly-language.js`: the former FlyBrain voice pipeline, independent of FlyCoder. diff --git a/scripts/publish-hf.sh b/scripts/publish-hf.sh new file mode 100755 index 0000000..4a73034 --- /dev/null +++ b/scripts/publish-hf.sh @@ -0,0 +1,99 @@ +#!/bin/sh +# Publishes FlyCoder on Hugging Face: /flycoder0.3, /flycoder0.3fast, /flycoder0.3pro. +# HF_TOKEN=... sh scripts/publish-hf.sh [] [ ...] +# Default namespace: the owner of HF_TOKEN. Default names: every model below. +# Each repository mirrors the model published on ollama.com (Tromset/), byte for byte: +# the GGUF weights, the vision projector, the system prompt, the sampling settings and the base's license, +# with docs/huggingface/.md as the model card. Ollama, llama.cpp and LM Studio can run it from there. +# Prerequisites: curl, node, python3 with huggingface_hub (pip install huggingface_hub), a token with write access +# (https://huggingface.co/settings/tokens). Downloads one model at a time: the largest needs 18 GB of free disk. +# GitHub Actions runs this script after each publication on ollama.com (.github/workflows/publish-huggingface.yml). +set -eu + +VERSION=0.3 +OLLAMA_USER=Tromset +# Published name, quantization. Keep in sync with scripts/publish.sh and docs/huggingface/. +MODELS='flycoder0.3 Q4_K_M +flycoder0.3fast Q4_K_M +flycoder0.3pro Q4_K_M' +REGISTRY=https://registry.ollama.ai/v2 + +die() { printf 'FlyCoder: %s\n' "$*" >&2; exit 1; } + +[ -n "${HF_TOKEN:-}" ] || die "HF_TOKEN is not set (create a write token at https://huggingface.co/settings/tokens)." +for tool in curl node python3; do command -v "$tool" >/dev/null 2>&1 || die "$tool is not installed."; done +python3 -c 'import huggingface_hub' 2>/dev/null || die "huggingface_hub is missing: pip install huggingface_hub" + +here=$(cd "$(dirname "$0")/.." && pwd) +namespace=${1:-} +[ $# -gt 0 ] && shift +[ "${namespace#-}" = "$namespace" ] || die "usage: sh scripts/publish-hf.sh [] [ ...]" +if [ -z "$namespace" ]; then + namespace=$(curl -fsS -H "Authorization: Bearer $HF_TOKEN" https://huggingface.co/api/whoami-v2 \ + | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).name))') \ + || die "Hugging Face refused HF_TOKEN." +fi + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +wanted() { # wanted [ ...]: true when nothing was requested or was + n=$1; shift + [ $# -eq 0 ] && return 0 + for w in "$@"; do [ "$w" = "$n" ] && return 0; done + return 1 +} + +# layer : the digest of the layer with that type, or nothing. +layer() { + node -e 'const m=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")); + const l=m.layers.find(l=>l.mediaType==="application/vnd.ollama.image."+process.argv[2]); if(l) console.log(l.digest)' "$1" "$2" +} + +# fetch : downloads a blob of Tromset/ and checks its digest. +fetch() { + curl -fsSL --retry 4 --retry-all-errors -o "$3" "$REGISTRY/$OLLAMA_USER/$1/blobs/$2" + [ "sha256:$(sha256sum "$3" | cut -d' ' -f1)" = "$2" ] || die "$3 does not match $2." +} + +echo "$MODELS" | while read -r name quant; do + wanted "$name" "$@" || continue + card="$here/docs/huggingface/$name.md" + [ -f "$card" ] || die "$card is missing." + repo="$namespace/$name" + dir="$work/$name" + mkdir -p "$dir" + printf '\n==> https://huggingface.co/%s (from ollama.com/%s/%s)\n' "$repo" "$OLLAMA_USER" "$name" + + curl -fsS -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \ + -o "$work/manifest.json" "$REGISTRY/$OLLAMA_USER/$name/manifests/latest" || die "Tromset/$name is not on ollama.com." + model=$(layer "$work/manifest.json" model) + [ -n "$model" ] || die "Tromset/$name has no weights." + fetch "$name" "$model" "$dir/$name-$quant.gguf" + [ "$(head -c 4 "$dir/$name-$quant.gguf")" = GGUF ] || die "the weights of Tromset/$name are not a GGUF file." + projector=$(layer "$work/manifest.json" projector) + [ -z "$projector" ] || fetch "$name" "$projector" "$dir/mmproj-$name.gguf" + # Ollama reads `system` and `params` from the repository when it runs hf.co/. The template layer is not + # copied: it is a placeholder, Ollama formats Qwen3.5 with its built-in renderer and llama.cpp with the GGUF's template. + for kind in system params license; do + digest=$(layer "$work/manifest.json" "$kind") + [ -n "$digest" ] || continue + file=$kind; [ "$kind" = license ] && file=LICENSE + fetch "$name" "$digest" "$dir/$file" + done + sed "s|@HF_NAMESPACE@|$namespace|g" "$card" > "$dir/README.md" + + python3 - "$repo" "$dir" "FlyCoder $VERSION: $name from ollama.com/$OLLAMA_USER/$name" <<'EOF' +import sys +from huggingface_hub import HfApi +repo, folder, message = sys.argv[1:] +api = HfApi() +api.create_repo(repo, repo_type="model", exist_ok=True) +api.upload_folder(repo_id=repo, folder_path=folder, commit_message=message) +EOF + rm -rf "$dir" +done + +printf '\nPublished FlyCoder %s on Hugging Face. Anyone can now run:\n' "$VERSION" +printf ' ollama run hf.co/%s/flycoder0.3\n' "$namespace" +printf ' llama-server -hf %s/flycoder0.3 --jinja\n' "$namespace" diff --git a/tests/modelfile.test.mjs b/tests/modelfile.test.mjs index 07fd664..2aed028 100644 --- a/tests/modelfile.test.mjs +++ b/tests/modelfile.test.mjs @@ -99,6 +99,23 @@ test('the publish workflow runs the publisher and updates every page', () => { for (const secret of ['OLLAMA_KEY', 'OLLAMA_API_KEY']) assert.ok(workflow.includes(`secrets.${secret}`), `workflow uses ${secret}`); }); +test('every published model has a Hugging Face model card and is mirrored by the Hugging Face publisher', () => { + const publish = read('scripts/publish-hf.sh'), workflow = read('.github/workflows/publish-huggingface.yml'); + assert.match(publish, /^VERSION=0\.3$/m); + assert.match(publish, /^OLLAMA_USER=Tromset$/m); + for (const v of VARIANTS.filter(v => v.page)) { + const card = read(`docs/huggingface/${v.name}.md`), base = v.base.split(' by ')[0]; + assert.match(publish, new RegExp(`^(MODELS=')?${v.name.replace('.', '\\.')} Q4_K_M'?$`, 'm'), `publish-hf.sh mirrors ${v.name}`); + assert.match(card, /^---\nlicense: apache-2\.0\n/, `${v.name} card declares the base's license`); + assert.ok(card.includes(`base_model: Qwen/${base.replace(' ', '-')}\n`), `${v.name} card links ${base}`); + assert.ok(card.includes(`ollama run hf.co/@HF_NAMESPACE@/${v.name}\n`), `${v.name} card shows how to run it`); + for (const fact of [v.size, v.mac, v.ctx, base, `-c ${v.params.num_ctx} `]) assert.ok(card.includes(fact), `${v.name} card mentions ${fact}`); + } + assert.match(workflow, /workflows: \[publish-ollama\]/); + assert.ok(workflow.includes('secrets.HF_TOKEN'), 'workflow uses HF_TOKEN'); + assert.match(workflow, /sh scripts\/publish-hf\.sh/); +}); + test('shell scripts parse with POSIX sh', () => { - for (const file of ['install.sh', 'scripts/publish.sh', 'scripts/update-ollama-pages.sh']) execFileSync('sh', ['-n', new URL(`../${file}`, import.meta.url).pathname]); + for (const file of ['install.sh', 'scripts/publish.sh', 'scripts/publish-hf.sh', 'scripts/update-ollama-pages.sh']) execFileSync('sh', ['-n', new URL(`../${file}`, import.meta.url).pathname]); }); From 5f35cd4719cf5a9023734d5f8454bb51da37d242 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 18:50:03 +0000 Subject: [PATCH 2/2] ci: skip the Hugging Face mirror while HF_TOKEN is missing Without the secret the workflow now warns and publishes nothing instead of failing, so main stays green until a token is added. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cw7NFZ1kjsaaLELfRZLFv9 --- .github/workflows/publish-huggingface.yml | 12 +++++++++++- Documentation.md | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-huggingface.yml b/.github/workflows/publish-huggingface.yml index af7c141..96fb2e3 100644 --- a/.github/workflows/publish-huggingface.yml +++ b/.github/workflows/publish-huggingface.yml @@ -36,27 +36,37 @@ jobs: - uses: actions/checkout@v4 - name: Check the secret + id: secret run: | - [ -n "$HF_TOKEN" ] || { echo "::error::Add the HF_TOKEN secret (a write token from https://huggingface.co/settings/tokens)."; exit 1; } + # Without a token the mirror is skipped, not failed: ollama.com stays the reference. + if [ -n "$HF_TOKEN" ]; then echo "ready=true" >> "$GITHUB_OUTPUT"; else + echo "::warning::HF_TOKEN is not set: nothing was published on Hugging Face. Add a write token from https://huggingface.co/settings/tokens." + echo "Skipped: add the HF_TOKEN secret to publish on Hugging Face." >> "$GITHUB_STEP_SUMMARY" + fi - name: Free disk space for the largest model + if: steps.secret.outputs.ready == 'true' run: | sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL sudo docker image prune --all --force >/dev/null df -h / - uses: actions/setup-node@v4 + if: steps.secret.outputs.ready == 'true' with: node-version: 22 - uses: actions/setup-python@v5 + if: steps.secret.outputs.ready == 'true' with: python-version: '3.12' - name: Install huggingface_hub + if: steps.secret.outputs.ready == 'true' run: python3 -m pip install --quiet 'huggingface_hub>=0.32,<2' - name: Publish on Hugging Face + if: steps.secret.outputs.ready == 'true' env: REQUESTED: ${{ inputs.models }} run: | diff --git a/Documentation.md b/Documentation.md index 0052cb7..58c58c7 100644 --- a/Documentation.md +++ b/Documentation.md @@ -409,7 +409,7 @@ By hand: `pip install huggingface_hub`, then `HF_TOKEN=... sh scripts/publish-hf | Endless repetition | temperature changed to a value that is too low | go back to the Modelfile's settings | | `npm run bench`: "Reference solution failed" | `python3` or `node` missing, or sandbox refused | `xcode-select --install`, Node 22, or `--no-sandbox` | | The publish workflow fails at "Check the secrets" | `OLLAMA_KEY` is missing | add it (section 8) | -| publish-huggingface fails at "Check the secret" | `HF_TOKEN` is missing | add it (section 8.1) | +| publish-huggingface warns "HF_TOKEN is not set" and publishes nothing | `HF_TOKEN` is missing | add it (section 8.1) | ## 10. Development