Skip to content

ci(deps): bump actions/checkout from 7.0.0 to 7.0.1 in the github-actions group #83

ci(deps): bump actions/checkout from 7.0.0 to 7.0.1 in the github-actions group

ci(deps): bump actions/checkout from 7.0.0 to 7.0.1 in the github-actions group #83

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
ACTIONLINT_VERSION: "1.7.12"
ACTIONLINT_SHA256: "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8"
SHELLCHECK_VERSION: "v0.11.0"
SHELLCHECK_SHA256: "b7af85e41cc99489dcc21d66c6d5f3685138f06d34651e6d34b42ec6d54fe6f6"
CROWDIN_CLI_VERSION: "4.14.2"
jobs:
# ─── workflow lint (runs first — catches YAML errors in other jobs) ──────────
actionlint:
name: Workflow lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install actionlint (verified checksum)
run: |
TARBALL="/tmp/actionlint.tar.gz"
curl -fsSL \
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \
-o "$TARBALL"
echo "${ACTIONLINT_SHA256} ${TARBALL}" | sha256sum --check --strict
tar -xzf "$TARBALL" -C /tmp actionlint
- name: Run actionlint
run: /tmp/actionlint -color
# ─── metadata ────────────────────────────────────────────────────────────────
metadata:
name: Metadata
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Validate plugin.json (syntax + semver)
run: |
jq --exit-status . plugin.json >/dev/null
VERSION="$(jq -r '.version' plugin.json)"
echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || {
echo "::error::plugin.json version '${VERSION}' is not semver (expected X.Y.Z)" >&2
exit 1
}
echo "version=${VERSION}" >> "$GITHUB_STEP_SUMMARY"
- name: Validate i18n JSON syntax
run: |
for f in i18n/*.json; do
jq --exit-status . "$f" >/dev/null || {
echo "::error::${f} is not valid JSON" >&2
exit 1
}
done
- name: Validate i18n key parity (all locales must match en)
run: |
python3 - <<'PY'
import json, sys
from pathlib import Path
base = set(json.loads(Path('i18n/en.json').read_text()).keys())
errors = []
for locale in ('pt_BR', 'zh_CN', 'es_ES', 'de_DE'):
keys = set(json.loads(Path(f'i18n/{locale}.json').read_text()).keys())
missing = sorted(base - keys)
extra = sorted(keys - base)
if missing:
errors.append(f'::error::{locale}: missing {len(missing)} key(s): {missing}')
if extra:
errors.append(f'::error::{locale}: {len(extra)} extra key(s) not in en.json: {extra}')
if errors:
sys.exit('\n'.join(errors))
print(f'i18n OK — {len(base)} keys in en.json, all locales match.')
PY
- name: Validate CHANGELOG has entry for plugin.json version
run: |
VERSION="$(jq -r '.version' plugin.json)"
grep -qF "## ${VERSION}" CHANGELOG.md || grep -qF "## [${VERSION}]" CHANGELOG.md || {
echo "::error::CHANGELOG.md has no entry for version ${VERSION}" >&2
exit 1
}
# ─── shell ───────────────────────────────────────────────────────────────────
shell:
name: Shell scripts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Syntax check — all get-* scripts
run: find providers -name 'get-*' -type f -print0 | xargs -0 bash -n
- name: Confirm all provider scripts are executable
run: |
FAIL=0
while IFS= read -r -d '' f; do
if [ ! -x "$f" ]; then
echo "::error::Not executable: ${f}"
FAIL=1
fi
done < <(find providers -name 'get-*' -type f -print0)
exit "$FAIL"
- name: Install shellcheck (verified checksum)
run: |
TARBALL="/tmp/shellcheck.tar.gz"
curl -fsSL \
"https://github.com/koalaman/shellcheck/releases/download/${SHELLCHECK_VERSION}/shellcheck-${SHELLCHECK_VERSION}.linux.x86_64.tar.gz" \
-o "$TARBALL"
echo "${SHELLCHECK_SHA256} ${TARBALL}" | sha256sum --check --strict
tar -xzf "$TARBALL" --strip-components=1 -C /tmp "shellcheck-${SHELLCHECK_VERSION}/shellcheck"
/tmp/shellcheck --version
- name: shellcheck — main backends (error level)
run: |
/tmp/shellcheck -S error \
providers/get-provider-usage \
providers/get-copilot-usage \
providers/get-claude-usage \
providers/get-provider-wrapper
- name: shellcheck — main backends (warning level)
run: |
/tmp/shellcheck -S warning \
providers/get-provider-usage \
providers/get-copilot-usage \
providers/get-claude-usage \
providers/get-provider-wrapper
- name: shellcheck — all provider stubs (warning level)
run: |
find providers -name 'get-*' \
! -name 'get-provider-usage' \
! -name 'get-copilot-usage' \
! -name 'get-claude-usage' \
! -name 'get-provider-wrapper' \
-type f -print0 \
| xargs -0 /tmp/shellcheck -S warning
- name: Provider dispatch coverage
run: |
python3 - <<'PY'
import re, sys
from pathlib import Path
# collect stub IDs: get-<id>-usage (strip known non-provider scripts)
skip = {'provider', 'provider-wrapper', 'copilot', 'claude', 'codex', 'provider-health'}
stubs = set()
for f in Path('providers').glob('get-*-usage'):
name = f.name.removeprefix('get-').removesuffix('-usage')
if name not in skip:
stubs.add(name)
# collect dispatch cases. Each case is `canonical|alias1|alias2)`:
# the first token is the canonical provider (expected to own a stub),
# the rest are documented aliases (no stub of their own, by design).
# The char class includes '.' so dotted aliases like `z.ai` parse.
src = Path('providers/get-provider-usage').read_text()
dispatched = set() # every routable token (canonical + aliases)
canonical = set() # first token of each case only
in_func = False
for line in src.splitlines():
if re.match(r'^fetch_provider\(\)', line):
in_func = True
if not in_func:
continue
m = re.match(r'^\s+([\w.|/-]+)\)', line)
if m:
tokens = [a.strip() for a in m.group(1).split('|')]
if tokens == ['*']:
continue
canonical.add(tokens[0])
dispatched.update(tokens)
# a provider is covered if ANY of its tokens routes; aliases are fine.
missing_dispatch = sorted(stubs - dispatched)
# only canonical names are expected to back a script; ignore the
# dedicated non-stub providers (claude/copilot/codex live in `skip`).
existing = {f.name.removeprefix('get-').removesuffix('-usage')
for f in Path('providers').glob('get-*-usage')}
missing_stub = sorted(canonical - existing - skip)
errors = []
if missing_dispatch:
errors.append(f'Stubs with no dispatch case: {missing_dispatch}')
if missing_stub:
errors.append(f'Canonical dispatch cases with no script: {missing_stub}')
print(f'Coverage: {len(stubs)} stubs, {len(dispatched)} dispatch entries '
f'({len(canonical)} canonical).')
if errors:
sys.exit('\n'.join(f'::error::{error}' for error in errors))
PY
# ─── integration tests ───────────────────────────────────────────────────────
integration:
name: Integration tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install dependencies (jq, curl, sqlite3)
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends jq curl sqlite3
- name: Smoke test — get-claude-usage (no credentials)
run: |
# get-claude-usage emits KEY=VALUE pairs (sourced by QML widget, not JSON)
OUT="$(HOME=/tmp providers/get-claude-usage 2>/dev/null || true)"
# Must contain at least SUBSCRIPTION_TYPE and WEEK_TOKENS keys
echo "$OUT" | grep -qE '^SUBSCRIPTION_TYPE=' || {
echo "::error::get-claude-usage did not emit SUBSCRIPTION_TYPE key"
echo "Output was: ${OUT}"
exit 1
}
echo "$OUT" | grep -qE '^WEEK_TOKENS=' || {
echo "::error::get-claude-usage did not emit WEEK_TOKENS key"
exit 1
}
# All lines must be KEY=VALUE (no bare text, no JSON, no stack traces)
INVALID="$(echo "$OUT" | grep -vE '^[A-Z_]+=.*$' || true)"
if [ -n "$INVALID" ]; then
echo "::error::get-claude-usage emitted unexpected non-KEY=VALUE lines:"
echo "$INVALID"
exit 1
fi
echo "get-claude-usage output (no creds): OK — $(echo "$OUT" | wc -l) KEY=VALUE lines"
- name: Smoke test — get-provider-usage single provider (error path)
run: |
OUT="$(providers/get-provider-usage "openrouter" "" 2>/dev/null || true)"
echo "$OUT" | jq --exit-status '. | type == "array" and length > 0' >/dev/null || {
echo "::error::get-provider-usage did not return a non-empty JSON array"
echo "Output was: ${OUT}"
exit 1
}
PROVIDER="$(echo "$OUT" | jq -r '.[0].provider')"
[ "$PROVIDER" = "openrouter" ] || {
echo "::error::Expected provider=openrouter, got: ${PROVIDER}"
exit 1
}
echo "get-provider-usage smoke: OK — provider=${PROVIDER}"
- name: Integration test — Codex app-server fixture
run: |
FAKE_BIN="$(mktemp -d)"
cat > "$FAKE_BIN/codex" <<'SH'
#!/usr/bin/env bash
cat >/dev/null
cat "$GITHUB_WORKSPACE/tests/fixtures/${CODEX_FIXTURE:-codex-app-server.jsonl}"
SH
chmod +x "$FAKE_BIN/codex"
OUT="$(PATH="$FAKE_BIN:$PATH" providers/get-provider-usage "codex" "")"
echo "$OUT" | jq --exit-status '
length == 1 and .[0].provider == "codex" and
.[0].usage.primary.usedPercent == 25 and
.[0].usage.primary.resetDescription == "Session" and
.[0].usage.secondary.usedPercent == 50 and
.[0].usage.secondary.resetDescription == "Weekly" and
.[0].credits.remaining == "12"
' >/dev/null
WEEKLY_OUT="$(CODEX_FIXTURE=codex-app-server-weekly-only.jsonl \
PATH="$FAKE_BIN:$PATH" providers/get-provider-usage "codex" "")"
echo "$WEEKLY_OUT" | jq --exit-status '
length == 1 and .[0].provider == "codex" and
.[0].usage.primary.usedPercent == 79 and
.[0].usage.primary.windowMinutes == 10080 and
.[0].usage.primary.resetDescription == "Weekly" and
.[0].usage.secondary == null and
.[0].credits.remaining == "1"
' >/dev/null
- name: Integration test — 9Router cached tokens
run: |
HOME_DIR="$(mktemp -d)"
mkdir -p "$HOME_DIR/.9router"
TODAY="$(date +%F)"
printf '{"dailySummary":{"%s":{"requests":2,"promptTokens":100,"cachedTokens":40,"completionTokens":20,"cost":0.5}}}\n' "$TODAY" > "$HOME_DIR/.9router/usage.json"
OUT="$(HOME="$HOME_DIR" providers/get-provider-usage "9router" "")"
echo "$OUT" | jq --exit-status '.[0].usage.tertiary.displayValue | contains("40 cached")' >/dev/null
- name: Integration test — Antigravity quota fixture
run: |
OUT="$(ANTIGRAVITY_RESPONSE_FILE=tests/fixtures/antigravity-valid.json \
ANTIGRAVITY_ACCOUNT_EMAIL=test@example.invalid \
providers/get-provider-usage "antigravity" "")"
echo "$OUT" | jq -e '
length == 1
and .[0].provider == "antigravity"
and .[0].usage.primary.resetDescription == "Gemini 3.1 Pro"
and .[0].usage.primary.usedPercent == 80
and .[0].usage.secondary.resetDescription == "Gemini 3.5 Flash"
and .[0].usage.secondary.usedPercent == 50
and .[0].usage.tertiary.resetDescription == "GPT-OSS 120B"
and .[0].usage.tertiary.usedPercent == 10
' >/dev/null
AUTH_ERROR="$(ANTIGRAVITY_RESPONSE_FILE=tests/fixtures/antigravity-valid.json \
ANTIGRAVITY_HTTP_STATUS=401 providers/get-provider-usage "antigravity" "")"
echo "$AUTH_ERROR" | jq -e '.[0].error.code == 401' >/dev/null
RATE_ERROR="$(ANTIGRAVITY_RESPONSE_FILE=tests/fixtures/antigravity-valid.json \
ANTIGRAVITY_HTTP_STATUS=429 providers/get-provider-usage "antigravity" "")"
echo "$RATE_ERROR" | jq -e '.[0].error.code == 429' >/dev/null
MISSING_SESSION="$(ANTIGRAVITY_STATE_DB=/does/not/exist \
providers/get-provider-usage "antigravity" "")"
echo "$MISSING_SESSION" | jq -e '.[0].error.code == 2' >/dev/null
SCHEMA_ERROR="$(ANTIGRAVITY_RESPONSE_FILE=tests/fixtures/antigravity-schema-changed.json \
providers/get-provider-usage "antigravity" "")"
echo "$SCHEMA_ERROR" | jq -e '.[0].error.message | contains("schema changed")' >/dev/null
- name: Integration test — Antigravity available-models fixture
run: |
# fetchAvailableModels is the live endpoint. Its models member is an
# object keyed by model ID, unlike the older groups/buckets response.
OUT="$(ANTIGRAVITY_RESPONSE_FILE=tests/fixtures/antigravity-models-valid.json \
providers/get-provider-usage "antigravity" "")"
echo "$OUT" | jq -e '
.[0].usage.primary.resetDescription == "Gemini Models"
and .[0].usage.primary.usedPercent == 100
and ([.[0].usage.primary, .[0].usage.secondary, .[0].usage.tertiary]
| map(select(.resetDescription == "Claude & OpenAI Models" and .usedPercent == 0))
| length) == 1
and ([.[0].usage.primary, .[0].usage.secondary, .[0].usage.tertiary]
| map(select(.resetDescription == "Other Models" and .usedPercent == 25))
| length) == 1
and .[0].accounts[0].modelWindows[0].name == "Gemini Flash"
and .[0].accounts[0].modelWindows[0].usedPercent == 65
and .[0].accounts[0].modelWindows[1].name == "Gemini Pro"
and .[0].accounts[0].modelWindows[1].usedPercent == 100
and ([.[0].accounts[0].modelWindows[].name] | index("Placeholder")) == null
' >/dev/null
- name: Integration test — Antigravity live request safeguards
run: tests/test-antigravity-live.sh
- name: Smoke test — all stubs delegate correctly (structure check)
run: |
FAIL=0
for stub in providers/get-*-usage; do
name="$(basename "$stub")"
case "$name" in
get-provider-usage|get-provider-health|get-copilot-usage|get-claude-usage|get-codex-usage) continue ;;
esac
# Each stub must exec get-provider-wrapper — verify it contains the pattern
grep -q 'get-provider-wrapper' "$stub" || {
echo "::error::${name} does not delegate to get-provider-wrapper"
FAIL=1
}
done
exit "$FAIL"
- name: Smoke test — prerequisite health schema
run: |
OUT="$(providers/get-provider-health "codex,perplexity")"
echo "$OUT" | jq --exit-status '
type == "array" and length == 2 and
all(.[]; has("provider") and has("status") and has("detail"))
' >/dev/null
- name: Integration test — usage history round-trip
run: |
XDG_CACHE_HOME="$(mktemp -d)"
export XDG_CACHE_HOME
HIST_DIR="$XDG_CACHE_HOME/AiOverviewControl"
mkdir -p "$HIST_DIR"
# Empty cache → exactly "{}"
[ "$(providers/get-usage-history)" = "{}" ] || {
echo "::error::get-usage-history must emit {} when no history exists"
exit 1
}
# Seed unordered snapshots; aggregation must sort by ts and group by provider
printf '%s\n' \
'{"ts":200,"provider":"codex","pct":20}' \
'{"ts":100,"provider":"codex","pct":10}' \
'{"ts":150,"provider":"claude","pct":5.5}' \
> "$HIST_DIR/usage-history.jsonl"
OUT="$(providers/get-usage-history)"
echo "$OUT" | jq --exit-status '
(.codex | length == 2) and
.codex[0] == {t:100, p:10} and
.codex[1] == {t:200, p:20} and
.claude[0] == {t:150, p:5.5}
' >/dev/null || {
echo "::error::get-usage-history aggregation mismatch. Output: ${OUT}"
exit 1
}
# Dispatcher must not append snapshots for informational/local cards
# that intentionally report usedPercent:0. History is reserved for
# real non-zero quota/spend pressure so sparklines stay meaningful.
BEFORE="$(wc -l < "$HIST_DIR/usage-history.jsonl")"
providers/get-provider-usage "perplexity,ollama" "" >/dev/null
AFTER="$(wc -l < "$HIST_DIR/usage-history.jsonl")"
if [ "$AFTER" != "$BEFORE" ]; then
echo "::error::get-provider-usage recorded a 0% informational/local snapshot"
echo "Before lines: ${BEFORE}; after lines: ${AFTER}"
cat "$HIST_DIR/usage-history.jsonl"
exit 1
fi
echo "usage history round-trip: OK"
# ─── qml ─────────────────────────────────────────────────────────────────────
qml:
name: QML lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Qt5's qmllint is a syntax verifier: it validates QML/JS structure
# (braces, bindings, property declarations) and exits non-zero on a
# malformed file, without trying to resolve the DankMaterialShell `qs.*`
# modules — so there is no import-resolution noise to filter. Qt6's
# qmllint resolves imports and would drown the run in unavoidable
# `qs.*`/unqualified warnings, so we deliberately pin the Qt5 tool to keep
# this a clean HARD gate rather than an advisory one.
- name: Install Qt5 qmllint
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends qtdeclarative5-dev-tools
- name: Locate qmllint
id: qmllint
run: |
bin="$(command -v qmllint || true)"
if [ -z "$bin" ]; then
for cand in \
/usr/lib/qt5/bin/qmllint \
/usr/lib/x86_64-linux-gnu/qt5/bin/qmllint; do
if [ -x "$cand" ]; then bin="$cand"; break; fi
done
fi
if [ -z "$bin" ]; then
echo "::error::qmllint not found after installing qtdeclarative5-dev-tools"
exit 1
fi
echo "bin=$bin" >> "$GITHUB_OUTPUT"
"$bin" --version
- name: QML lint (hard gate)
run: |
"${{ steps.qmllint.outputs.bin }}" \
AiOverviewControlWidget.qml \
AiOverviewControlSettings.qml \
AiOverviewControlI18n.qml \
ProviderLogo.qml
# ─── crowdin ─────────────────────────────────────────────────────────────────
crowdin:
name: Crowdin config
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Validate Crowdin config structure
run: |
python3 - <<'PY'
from pathlib import Path
config = Path('.github/crowdin.yml').read_text()
required = [
'project_id: "899362"',
'base_path: ".."',
'source: "/i18n/en.json"',
'translation: "/i18n/%locale_with_underscore%.json"',
]
missing = [item for item in required if item not in config]
if missing:
raise SystemExit(f'::error::Crowdin config missing entries: {missing}')
if not Path('i18n/en.json').is_file():
raise SystemExit('::error::Crowdin source file i18n/en.json is missing')
PY
- name: Validate with Crowdin CLI (requires secret)
env:
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
run: |
if [ -z "${CROWDIN_API_TOKEN:-}" ]; then
echo "Crowdin token not available — skipping remote CLI validation."
exit 0
fi
curl -fsSL \
"https://github.com/crowdin/crowdin-cli/releases/download/${CROWDIN_CLI_VERSION}/crowdin-cli.zip" \
-o /tmp/crowdin-cli.zip
unzip -q /tmp/crowdin-cli.zip -d /tmp/crowdin-cli
python3 - <<'PY'
import os
from pathlib import Path
Path('/tmp/crowdin-identity.yml').write_text(
'api_token: ' + os.environ['CROWDIN_API_TOKEN'] + '\n'
)
PY
JAR="/tmp/crowdin-cli/${CROWDIN_CLI_VERSION}/crowdin-cli.jar"
java -jar "$JAR" config lint --config .github/crowdin.yml --identity /tmp/crowdin-identity.yml
# These commands call Crowdin's remote API and can fail or hang for
# reasons outside this repository (service issues, token scope, rate
# limiting). Keep the structural lint above as the hard gate, and
# surface remote validation problems as warnings instead of breaking CI.
for check in sources translations; do
if ! timeout 60 java -jar "$JAR" config "$check" --config .github/crowdin.yml --identity /tmp/crowdin-identity.yml; then
echo "::warning::Crowdin remote '${check}' validation failed or timed out; skipping as non-blocking."
fi
done