Merge pull request 'feat(qwen): let Qwen (and Kimi) join the swarm' (#30)
This commit was merged in pull request #30.
This commit is contained in:
@@ -46,6 +46,16 @@ jobs:
|
||||
secrets:
|
||||
OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# Forwarded so a "qwen/<model>" or "kimi/<model>" entry can join the
|
||||
# swarm by editing the GADFLY_DEFAULT_MODELS var alone — no workflow
|
||||
# edit, no re-release. Both are forwarded together on purpose: the
|
||||
# reusable workflow declares both, and forwarding only one is a config
|
||||
# that looks complete and 401s on the model you didn't wire. Empty until
|
||||
# the repo secret exists, which is a 401 on that one model, not a broken
|
||||
# review. NB kimi/<model> is Moonshot's own API — a different route than
|
||||
# the kimi-k2.6:cloud swarm entry, which rides OLLAMA_CLOUD_API_KEY.
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
GADFLY_FINDINGS_URL: ${{ secrets.GADFLY_FINDINGS_URL }}
|
||||
GADFLY_FINDINGS_TOKEN: ${{ secrets.GADFLY_FINDINGS_TOKEN }}
|
||||
with:
|
||||
|
||||
@@ -45,6 +45,108 @@ env:
|
||||
IMAGE_NAME: gitea.stevedudenhoeffer.com/steve/gadfly
|
||||
|
||||
jobs:
|
||||
# Runs alongside the image build rather than gating it: a red test should be
|
||||
# loud on the PR without standing between Steve and a rebuild. Added because
|
||||
# this repo had NO test job at all — `go test` and scripts/preflight_test.sh
|
||||
# both existed and neither was ever executed by CI, which is worse than
|
||||
# having no tests, since it reads as coverage.
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
# This job executes repository code (`go test`) on pull_request, so it gets
|
||||
# the narrowest token the platform will give it. Nothing here writes.
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Scrubbing the registry credential while leaving the checkout token
|
||||
# in .git/config would just move the prize: `go test` below runs
|
||||
# repository code with the workspace readable. Nothing in this job
|
||||
# talks to git after checkout, so the token has no reason to persist.
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
# Fetch dependencies, then DESTROY the credential before any step that
|
||||
# executes repository code. REGISTRY_PASSWORD is push-capable, this repo
|
||||
# is public so pull_request runs can carry attacker-authored code, and
|
||||
# `go test` runs that code — a plaintext ~/.gitconfig left in place is a
|
||||
# credential any test could print. The image build faces the same
|
||||
# question and answers it the same way: its creds are BuildKit secrets
|
||||
# scoped to the module-download RUN, never present while code runs.
|
||||
- name: Fetch private modules
|
||||
env:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Own the config path outright. `git config --global` writes to
|
||||
# GIT_CONFIG_GLOBAL, else $XDG_CONFIG_HOME/git/config when that
|
||||
# directory exists, else ~/.gitconfig — so "delete ~/.gitconfig"
|
||||
# scrubs a file the credential may never have been in. Naming the
|
||||
# path leaves exactly one file to remove.
|
||||
export GIT_CONFIG_GLOBAL="$(mktemp)"
|
||||
|
||||
# Scrub on ANY exit, not just success. `set -e` means a failed
|
||||
# `go mod download` aborts this step, and a cleanup written as the
|
||||
# next line would never run — leaving a push-capable credential on a
|
||||
# long-lived self-hosted runner for whatever job lands there next.
|
||||
trap 'rm -f "$GIT_CONFIG_GLOBAL"' EXIT
|
||||
|
||||
go env -w GOPRIVATE=gitea.stevedudenhoeffer.com/*
|
||||
# Basic-auth header rather than credentials inside the URL: a
|
||||
# password containing @ : / or # breaks URL parsing, and the failure
|
||||
# would look like a bad password rather than a quoting bug.
|
||||
git config --global \
|
||||
"http.https://gitea.stevedudenhoeffer.com/.extraheader" \
|
||||
"Authorization: Basic $(printf '%s:%s' "$REGISTRY_USER" "$REGISTRY_PASSWORD" | base64 | tr -d '\n')"
|
||||
go mod download
|
||||
|
||||
rm -f "$GIT_CONFIG_GLOBAL"
|
||||
test ! -e "$GIT_CONFIG_GLOBAL"
|
||||
|
||||
# Prove the scrub across the whole home dir, not just the file we
|
||||
# deleted — that check would pass no matter what, and git/go can also
|
||||
# write ~/.netrc or ~/.config/go/env. Guarded on a non-empty secret:
|
||||
# `grep -F ""` matches every file, so a secretless run (fork PR) would
|
||||
# fail here with a message accusing it of leaking nothing.
|
||||
# -e, so a password beginning with "-" is a pattern and not options.
|
||||
# And distinguish grep's three exits: 0 found, 1 clean, >=2 ERROR. As
|
||||
# a bare condition an error reads as "not found" and the guard is
|
||||
# skipped — a check that fails OPEN in exactly the case where it can no
|
||||
# longer see the filesystem it is supposed to be searching.
|
||||
if [ -n "${REGISTRY_PASSWORD:-}" ]; then
|
||||
set +e
|
||||
grep -rqF -e "$REGISTRY_PASSWORD" "$HOME" 2>/dev/null
|
||||
rc=$?
|
||||
set -e
|
||||
case "$rc" in
|
||||
0) echo "::error::registry credential still present under \$HOME after scrub"; exit 1 ;;
|
||||
1) : ;; # clean
|
||||
*) echo "::error::credential scrub check could not run (grep exit $rc); refusing to continue"; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# GOPROXY=off from here on: the module cache is already warm, so any
|
||||
# attempt to reach the network is a bug — and it fails loudly instead of
|
||||
# quietly looking for the credential that is now gone.
|
||||
- name: go build
|
||||
env: { GOPROXY: "off" }
|
||||
run: go build ./...
|
||||
- name: go vet
|
||||
env: { GOPROXY: "off" }
|
||||
run: go vet ./...
|
||||
- name: gofmt
|
||||
env: { GOPROXY: "off" }
|
||||
run: test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }
|
||||
- name: go test
|
||||
env: { GOPROXY: "off" }
|
||||
run: go test -count=1 ./...
|
||||
- name: pre-flight credential table
|
||||
run: bash scripts/preflight_test.sh
|
||||
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
@@ -87,6 +87,15 @@ on:
|
||||
OPENAI_API_KEY: { required: false }
|
||||
ANTHROPIC_API_KEY: { required: false }
|
||||
GOOGLE_API_KEY: { required: false }
|
||||
# Alibaba Model Studio (Qwen), for GADFLY_MODELS entries like
|
||||
# "qwen/qwen3.8-max". NOT interchangeable with OPENAI_API_KEY: majordomo's
|
||||
# qwen built-in reads QWEN_API_KEY only and deliberately refuses to fall
|
||||
# back to the OpenAI key, so an unforwarded secret is a 401, not a
|
||||
# mis-billed OpenAI call.
|
||||
QWEN_API_KEY: { required: false }
|
||||
# Moonshot (Kimi) over its own API — distinct from the ollama-cloud
|
||||
# "kimi-k2.6:cloud" entry, which is keyed by OLLAMA_CLOUD_API_KEY.
|
||||
KIMI_API_KEY: { required: false }
|
||||
GADFLY_API_KEY: { required: false }
|
||||
CLAUDE_CODE_OAUTH_TOKEN: { required: false }
|
||||
GADFLY_FINDINGS_URL: { required: false }
|
||||
@@ -146,6 +155,12 @@ jobs:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
# Qwen (Alibaba Model Studio) and Kimi (Moonshot) over their own APIs,
|
||||
# for GADFLY_MODELS entries like "qwen/qwen3.8-max". Each built-in
|
||||
# reads ONLY its own variable — no cross-provider fallback — so a
|
||||
# missing line here is a clean 401, never a silently mis-keyed call.
|
||||
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
|
||||
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
|
||||
GADFLY_API_KEY: ${{ secrets.GADFLY_API_KEY }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# Named LAN endpoints, defined in user/org vars (format
|
||||
|
||||
Reference in New Issue
Block a user