Skip to content

Commit 21ffa31

Browse files
jeffdijdicorpo
andauthored
feat: cf precheck --list-checks, client-side version check, User-Agent header (2.4.0) (#16)
* feat: cf precheck --list-checks, client-side version check, User-Agent header (2.4.0) New user-facing features ------------------------ * cf precheck --list-checks (and an expanded --help) prints the canonical list of precheck references, sourced from the new chipfoundry_cli.check_refs module so CLI help, validation, and future tooling share one definition. * Client-side version check: every invocation polls GET /api/v1/cli/version (6h on-disk cache at ~/.chipfoundry-cli/version_check.json) and prints a dim yellow tip when a newer cf is available. A second, louder tier prints a red warning when the installed version is below the server's advertised minimum_supported. All failures are swallowed -- a flaky network must never block a real command. Gate-offable via CF_SKIP_VERSION_CHECK=1. * User-Agent on every outbound API call is now "chipfoundry-cli/<version> python/<py-ver> <platform>" so the backend can attribute traffic and (post-May 13) apply hard-floor rejection to pre-2.4.0 installs without affecting browser/portal/curl clients. Bumps cf-cli to 2.4.0. Notes ----- The backend (chipignite-backend-services 1.18.1) already serves the version endpoint and has the nudge middleware live. The hard-floor middleware is parked there until after the May 13 shuttle deadline, at which point flipping MINIMUM_SUPPORTED_CLI_VERSION to 2.4.0 will start rejecting older installs. This release is the first one customers can upgrade to in order to get ahead of that flip. Not yet published to PyPI; ship when ready. Made-with: Cursor * test: update stale help/error assertions to match current CLI output Four tests were asserting on help/error strings that have drifted over time (unrelated to this branch). Refresh them to match the current copy so CI can verify the version-check / list-checks work in this PR without riding on pre-existing red. - test_config_help: SSH private key path wording - test_init_help: "Initialize or refresh..." wording - test_status_help: "Show project status" wording - test_push_missing_required_files: accept the newer unlinked-project abort path alongside the legacy missing-file keywords Made-with: Cursor * test(remote-precheck-git): inject author env so commits succeed on clean CI CI runners have no global git identity, so `git commit -m init` was exiting 128 ("please tell me who you are") and failing four remote-precheck-git tests on every matrix run. Pass GIT_AUTHOR_* / GIT_COMMITTER_* via env rather than mutating `git config --global` so local runs on developer machines remain untouched. Made-with: Cursor --------- Co-authored-by: jdicorpo <jdicorpo@gmail.com>
1 parent 59de8d0 commit 21ffa31

12 files changed

Lines changed: 660 additions & 35 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ __pycache__/
99
# Distribution / packaging
1010
.Python
1111
env/
12+
venv/
13+
.venv/
1214
build/
1315
develop-eggs/
1416
dist/

chipfoundry_cli/check_refs.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Known cf-precheck check names with display metadata.
2+
3+
Must stay in sync with cf-precheck's ``ALL_CHECKS`` ordering
4+
(see ``cf-precheck/src/cf_precheck/check_manager.py``). The backend mirrors
5+
the ref keys in ``chipignite-backend-services/src/precheck_service/check_refs.py``.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from typing import NamedTuple
11+
12+
13+
class PrecheckCheck(NamedTuple):
14+
ref: str
15+
surname: str
16+
optional: bool
17+
18+
19+
PRECHECK_CHECKS: tuple[PrecheckCheck, ...] = (
20+
PrecheckCheck("topcell_check", "Top Cell", False),
21+
PrecheckCheck("gpio_defines", "GPIO Defines", False),
22+
PrecheckCheck("pdnmulti", "PDN Multi", False),
23+
PrecheckCheck("metalcheck", "Metal Check", False),
24+
PrecheckCheck("xor", "XOR", False),
25+
PrecheckCheck("magic_drc", "Magic DRC", True),
26+
PrecheckCheck("klayout_feol", "Klayout FEOL", False),
27+
PrecheckCheck("klayout_beol", "Klayout BEOL", False),
28+
PrecheckCheck("klayout_offgrid", "Klayout Offgrid", False),
29+
PrecheckCheck("klayout_met_min_ca_density", "Klayout Metal Density", False),
30+
PrecheckCheck(
31+
"klayout_pin_label_purposes_overlapping_drawing",
32+
"Klayout Pin Label",
33+
False,
34+
),
35+
PrecheckCheck("klayout_zeroarea", "Klayout ZeroArea", False),
36+
PrecheckCheck("spike_check", "Spike Check", False),
37+
PrecheckCheck("illegal_cellname_check", "Illegal Cellname", False),
38+
PrecheckCheck("lvs", "LVS", False),
39+
PrecheckCheck("oeb", "OEB", False),
40+
)
41+
42+
PRECHECK_CHECK_REFS: frozenset[str] = frozenset(c.ref for c in PRECHECK_CHECKS)

chipfoundry_cli/main.py

Lines changed: 110 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import click
22
import getpass
33
from typing import Optional, List
4+
from chipfoundry_cli.check_refs import PRECHECK_CHECKS
45
from chipfoundry_cli.remote_precheck_git import RemotePrecheckGitError, verify_remote_precheck_repo
6+
from chipfoundry_cli.version_check import maybe_warn_outdated
57
from chipfoundry_cli.utils import (
68
collect_project_files, ensure_cf_directory, update_or_create_project_json,
79
sftp_connect, upload_with_progress, sftp_ensure_dirs, sftp_download_recursive,
@@ -174,7 +176,16 @@ def check_project_initialized(project_root_path: Path, command_name: str, dry_ru
174176
@click.group(help="ChipFoundry CLI: Automate project submission and management.")
175177
@click.version_option(importlib.metadata.version("chipfoundry-cli"), "-v", "--version", message="%(version)s")
176178
def main():
177-
pass
179+
# Best-effort upgrade check. Cached on disk for CACHE_TTL_SECONDS and
180+
# guarded by a short timeout so it never slows down a command. Runs
181+
# only when a subcommand was dispatched — `cf --version` / `cf --help`
182+
# exit before this callback fires.
183+
try:
184+
current = importlib.metadata.version("chipfoundry-cli")
185+
maybe_warn_outdated(current, _get_api_url(), console, user_agent=_cf_user_agent())
186+
except Exception:
187+
# Never let a version-check issue break the actual command.
188+
pass
178189

179190
@main.command('config')
180191
def config_cmd():
@@ -3553,11 +3564,62 @@ def _upload_precheck_results(project_json_path: Path):
35533564
console.print("[yellow]⚠ Precheck results could not be synced to platform[/yellow]")
35543565

35553566

3556-
@main.command('precheck')
3567+
def _print_precheck_checks() -> None:
3568+
"""Print the list of available precheck checks as a table."""
3569+
table = Table(title="Available cf-precheck checks", show_lines=False)
3570+
table.add_column("Ref", style="cyan", no_wrap=True)
3571+
table.add_column("Name", style="white")
3572+
table.add_column("Default", style="green")
3573+
for c in PRECHECK_CHECKS:
3574+
default = "opt-in" if c.optional else "on"
3575+
table.add_row(c.ref, c.surname, default)
3576+
console.print(table)
3577+
console.print(
3578+
"\n[dim]Use [bold]--checks REF[/bold] to run only specific checks, "
3579+
"[bold]--skip-checks REF[/bold] to skip, or [bold]--magic-drc[/bold] "
3580+
"to include the optional Magic DRC check.[/dim]"
3581+
)
3582+
3583+
3584+
def _build_precheck_help() -> str:
3585+
"""Build the --help text, including the list of available checks."""
3586+
lines = [
3587+
"Run precheck validation on the project.",
3588+
"",
3589+
"This runs the cf-precheck tool to validate your design before submission.",
3590+
"",
3591+
"\b",
3592+
"Examples:",
3593+
" cf precheck # Run all checks",
3594+
" cf precheck --list-checks # List available checks and exit",
3595+
" cf precheck --skip-checks lvs # Skip LVS check",
3596+
" cf precheck --magic-drc # Include optional Magic DRC",
3597+
" cf precheck --checks topcell_check # Run specific checks only",
3598+
" cf precheck --remote # Queue on platform; exit when accepted",
3599+
" cf precheck --remote --poll # Wait and stream progress",
3600+
" cf precheck --remote --poll --wait-timeout 0 # Poll until done (no time limit)",
3601+
"",
3602+
"\b",
3603+
"Available checks (pass to --checks / --skip-checks):",
3604+
]
3605+
for c in PRECHECK_CHECKS:
3606+
suffix = " (optional; opt in via --magic-drc)" if c.optional else ""
3607+
lines.append(f" {c.ref}{suffix}")
3608+
lines += [
3609+
"",
3610+
"Remote precheck requires your local HEAD to match origin for --git-ref, and",
3611+
"precheck inputs (wrapper GDS, verilog/rtl/user_defines.v when the GPIO check",
3612+
"runs, and tracked .cf/project.json) to match that commit.",
3613+
]
3614+
return "\n".join(lines)
3615+
3616+
3617+
@main.command('precheck', help=_build_precheck_help())
35573618
@click.option('--project-root', type=click.Path(exists=True, file_okay=False), help='Path to the project directory (defaults to current directory)')
3558-
@click.option('--skip-checks', multiple=True, help='Checks to skip (can be specified multiple times)')
3619+
@click.option('--skip-checks', multiple=True, help='Checks to skip (repeatable). See --list-checks for valid refs.')
35593620
@click.option('--magic-drc', is_flag=True, help='Include Magic DRC check (optional, off by default)')
3560-
@click.option('--checks', multiple=True, help='Specific checks to run (can be specified multiple times)')
3621+
@click.option('--checks', multiple=True, help='Specific checks to run (repeatable). See --list-checks for valid refs.')
3622+
@click.option('--list-checks', 'list_checks', is_flag=True, help='List the available precheck checks and exit.')
35613623
@click.option('--dry-run', is_flag=True, help='Show the command without running')
35623624
@click.option('--remote', is_flag=True, help='Queue precheck on the ChipFoundry platform (requires cf login + linked project)')
35633625
@click.option(
@@ -3573,25 +3635,10 @@ def _upload_precheck_results(project_json_path: Path):
35733635
show_default=True,
35743636
help='With --remote --poll: max seconds to wait (0 = no limit). Ignored without --poll.',
35753637
)
3576-
def precheck(project_root, skip_checks, magic_drc, checks, dry_run, remote, poll, git_ref, wait_timeout):
3577-
"""Run precheck validation on the project.
3578-
3579-
This runs the cf-precheck tool to validate your design before
3580-
submission.
3581-
3582-
Examples:
3583-
cf precheck # Run all checks
3584-
cf precheck --skip-checks lvs # Skip LVS check
3585-
cf precheck --magic-drc # Include optional Magic DRC
3586-
cf precheck --checks topcell_check # Run specific checks only
3587-
cf precheck --remote # Queue on platform; exit when accepted
3588-
cf precheck --remote --poll # Wait and stream progress
3589-
cf precheck --remote --poll --wait-timeout 0 # Poll until done (no time limit)
3590-
3591-
Remote precheck requires your local HEAD to match origin for --git-ref, and precheck
3592-
inputs (wrapper GDS, verilog/rtl/user_defines.v when the GPIO check runs, and tracked
3593-
.cf/project.json) to match that commit.
3594-
"""
3638+
def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run, remote, poll, git_ref, wait_timeout):
3639+
if list_checks:
3640+
_print_precheck_checks()
3641+
return
35953642
cwd_root, _ = get_project_json_from_cwd()
35963643
if not project_root and cwd_root:
35973644
project_root = cwd_root
@@ -3659,7 +3706,10 @@ def precheck(project_root, skip_checks, magic_drc, checks, dry_run, remote, poll
36593706
api_url = _get_api_url()
36603707
client = httpx_remote.Client(
36613708
base_url=f"{api_url}/api/v1",
3662-
headers={'Authorization': f'Bearer {api_key}'},
3709+
headers={
3710+
'Authorization': f'Bearer {api_key}',
3711+
'User-Agent': _cf_user_agent(),
3712+
},
36633713
timeout=120.0,
36643714
)
36653715
try:
@@ -4174,6 +4224,24 @@ def verify(test, project_root, sim, list_tests, run_all, tag, dry_run):
41744224
PORTAL_BASE_URL = 'https://platform.chipfoundry.io'
41754225

41764226

4227+
def _cf_user_agent() -> str:
4228+
"""User-Agent string for platform requests.
4229+
4230+
Format: ``chipfoundry-cli/<cli-version> python/<py-version> <platform>``.
4231+
Lets the backend track which CLI versions are in the wild without a
4232+
dedicated telemetry endpoint.
4233+
"""
4234+
import platform as _platform
4235+
4236+
try:
4237+
cli_version = importlib.metadata.version("chipfoundry-cli")
4238+
except importlib.metadata.PackageNotFoundError:
4239+
cli_version = "unknown"
4240+
py = _platform.python_version()
4241+
system = f"{_platform.system().lower()}-{_platform.machine().lower()}"
4242+
return f"chipfoundry-cli/{cli_version} python/{py} {system}"
4243+
4244+
41774245
def _get_api_url() -> str:
41784246
config = load_user_config()
41794247
return config.get('api_url', DEFAULT_API_URL)
@@ -4199,7 +4267,10 @@ def _api_client():
41994267
api_url = _get_api_url()
42004268
client = httpx.Client(
42014269
base_url=f"{api_url}/api/v1",
4202-
headers={'Authorization': f'Bearer {api_key}'},
4270+
headers={
4271+
'Authorization': f'Bearer {api_key}',
4272+
'User-Agent': _cf_user_agent(),
4273+
},
42034274
timeout=15,
42044275
)
42054276
return client, api_url
@@ -4435,7 +4506,11 @@ def login_cmd(test):
44354506
console.print(f"Opening browser to authenticate with [bold]{api_url}[/bold]...\n")
44364507

44374508
try:
4438-
resp = httpx.post(f"{api_url}/api/v1/auth/cli/sessions", timeout=10)
4509+
resp = httpx.post(
4510+
f"{api_url}/api/v1/auth/cli/sessions",
4511+
headers={'User-Agent': _cf_user_agent()},
4512+
timeout=10,
4513+
)
44394514
resp.raise_for_status()
44404515
data = resp.json()
44414516
except httpx.HTTPError as e:
@@ -4457,7 +4532,11 @@ def login_cmd(test):
44574532
for _ in range(max_polls):
44584533
time.sleep(poll_interval)
44594534
try:
4460-
poll_resp = httpx.get(poll_url, timeout=10)
4535+
poll_resp = httpx.get(
4536+
poll_url,
4537+
headers={'User-Agent': _cf_user_agent()},
4538+
timeout=10,
4539+
)
44614540
poll_resp.raise_for_status()
44624541
poll_data = poll_resp.json()
44634542
except httpx.HTTPError:
@@ -4528,7 +4607,10 @@ def whoami_cmd():
45284607
try:
45294608
resp = httpx.get(
45304609
f"{api_url}/api/v1/auth/cli/whoami",
4531-
headers={'Authorization': f'Bearer {api_key}'},
4610+
headers={
4611+
'Authorization': f'Bearer {api_key}',
4612+
'User-Agent': _cf_user_agent(),
4613+
},
45324614
timeout=10,
45334615
)
45344616
if resp.status_code == 401:

0 commit comments

Comments
 (0)