browsertap-mcp 0.4.12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- browsertap_mcp-0.4.12/.github/dependabot.yml +28 -0
- browsertap_mcp-0.4.12/.github/workflows/live.yml +115 -0
- browsertap_mcp-0.4.12/.github/workflows/release.yml +175 -0
- browsertap_mcp-0.4.12/.github/workflows/supply-chain.yml +132 -0
- browsertap_mcp-0.4.12/.github/workflows/test.yml +226 -0
- browsertap_mcp-0.4.12/.gitignore +120 -0
- browsertap_mcp-0.4.12/AGENTS.md +393 -0
- browsertap_mcp-0.4.12/CHANGELOG.md +937 -0
- browsertap_mcp-0.4.12/CONTRIBUTING.md +402 -0
- browsertap_mcp-0.4.12/CONTRIBUTING.zh-CN.md +295 -0
- browsertap_mcp-0.4.12/LICENSE +21 -0
- browsertap_mcp-0.4.12/MANIFEST.in +30 -0
- browsertap_mcp-0.4.12/PKG-INFO +652 -0
- browsertap_mcp-0.4.12/README.md +596 -0
- browsertap_mcp-0.4.12/README.zh-CN.md +629 -0
- browsertap_mcp-0.4.12/SECURITY.md +139 -0
- browsertap_mcp-0.4.12/THIRD-PARTY-NOTICES.md +64 -0
- browsertap_mcp-0.4.12/docs/TROUBLESHOOTING.md +283 -0
- browsertap_mcp-0.4.12/docs/TROUBLESHOOTING.zh-CN.md +231 -0
- browsertap_mcp-0.4.12/docs/USAGE.md +178 -0
- browsertap_mcp-0.4.12/docs/USAGE.zh-CN.md +156 -0
- browsertap_mcp-0.4.12/examples/claude-desktop-config.json +9 -0
- browsertap_mcp-0.4.12/examples/cursor-mcp.json +9 -0
- browsertap_mcp-0.4.12/examples/hermes-config.yaml +5 -0
- browsertap_mcp-0.4.12/pyproject.toml +175 -0
- browsertap_mcp-0.4.12/scripts/__init__.py +1 -0
- browsertap_mcp-0.4.12/scripts/acceptance_report.py +458 -0
- browsertap_mcp-0.4.12/scripts/check_distribution.py +317 -0
- browsertap_mcp-0.4.12/scripts/check_install.py +379 -0
- browsertap_mcp-0.4.12/scripts/check_release_tag.py +198 -0
- browsertap_mcp-0.4.12/scripts/check_tool_docs.py +399 -0
- browsertap_mcp-0.4.12/scripts/evidence_manifest.py +240 -0
- browsertap_mcp-0.4.12/scripts/finalize_change.py +189 -0
- browsertap_mcp-0.4.12/scripts/tool_coverage_report.py +414 -0
- browsertap_mcp-0.4.12/scripts/versioning.py +535 -0
- browsertap_mcp-0.4.12/server.json +45 -0
- browsertap_mcp-0.4.12/setup.cfg +4 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/__init__.py +9 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/_version.py +1 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/bridge.py +468 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/browser_bridge.py +1635 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/_locales/en/messages.json +17 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/_locales/zh_CN/messages.json +17 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/background.js +4600 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/content.js +81 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/disable_dialogs.js +94 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/manifest.json +53 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/popup.html +27 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/chrome_extension/popup.js +82 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/cli.py +192 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/page_input.py +740 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/paths.py +109 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/physical_input.py +730 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/server.py +5971 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/simphtml.py +1277 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/skills/browsertap-bridge-recovery/SKILL.md +330 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp/skills/browsertap-default/SKILL.md +253 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp.egg-info/PKG-INFO +652 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp.egg-info/SOURCES.txt +104 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp.egg-info/dependency_links.txt +1 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp.egg-info/entry_points.txt +2 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp.egg-info/requires.txt +23 -0
- browsertap_mcp-0.4.12/src/browsertap_mcp.egg-info/top_level.txt +1 -0
- browsertap_mcp-0.4.12/tests/conftest.py +379 -0
- browsertap_mcp-0.4.12/tests/live_preflight.py +351 -0
- browsertap_mcp-0.4.12/tests/test_acceptance_report.py +347 -0
- browsertap_mcp-0.4.12/tests/test_all_tools_behavior.py +372 -0
- browsertap_mcp-0.4.12/tests/test_bridge_coverage.py +461 -0
- browsertap_mcp-0.4.12/tests/test_browser_bridge_coverage.py +1611 -0
- browsertap_mcp-0.4.12/tests/test_cli_coverage.py +284 -0
- browsertap_mcp-0.4.12/tests/test_dialog_policy.py +1647 -0
- browsertap_mcp-0.4.12/tests/test_distribution_contract.py +774 -0
- browsertap_mcp-0.4.12/tests/test_documentation_contract.py +601 -0
- browsertap_mcp-0.4.12/tests/test_downloads.py +670 -0
- browsertap_mcp-0.4.12/tests/test_evidence_manifest.py +307 -0
- browsertap_mcp-0.4.12/tests/test_finalize_change.py +62 -0
- browsertap_mcp-0.4.12/tests/test_hit_verification.py +333 -0
- browsertap_mcp-0.4.12/tests/test_image_dimensions.py +260 -0
- browsertap_mcp-0.4.12/tests/test_install_check.py +372 -0
- browsertap_mcp-0.4.12/tests/test_link_auth.py +390 -0
- browsertap_mcp-0.4.12/tests/test_link_refs.py +124 -0
- browsertap_mcp-0.4.12/tests/test_live_browser.py +1172 -0
- browsertap_mcp-0.4.12/tests/test_live_preflight.py +544 -0
- browsertap_mcp-0.4.12/tests/test_locator_contract.py +136 -0
- browsertap_mcp-0.4.12/tests/test_log_redaction.py +157 -0
- browsertap_mcp-0.4.12/tests/test_offline.py +2044 -0
- browsertap_mcp-0.4.12/tests/test_offscreen_note.py +56 -0
- browsertap_mcp-0.4.12/tests/test_page_input.py +202 -0
- browsertap_mcp-0.4.12/tests/test_paths.py +141 -0
- browsertap_mcp-0.4.12/tests/test_phase0_recovery.py +4818 -0
- browsertap_mcp-0.4.12/tests/test_phase1_tools.py +407 -0
- browsertap_mcp-0.4.12/tests/test_physical_input.py +1617 -0
- browsertap_mcp-0.4.12/tests/test_release_tag.py +177 -0
- browsertap_mcp-0.4.12/tests/test_response_classification.py +108 -0
- browsertap_mcp-0.4.12/tests/test_screen_bounds.py +531 -0
- browsertap_mcp-0.4.12/tests/test_screenshot_content.py +251 -0
- browsertap_mcp-0.4.12/tests/test_server_coverage.py +943 -0
- browsertap_mcp-0.4.12/tests/test_setup_diagnostics.py +369 -0
- browsertap_mcp-0.4.12/tests/test_simphtml_coverage.py +432 -0
- browsertap_mcp-0.4.12/tests/test_site_permissions.py +542 -0
- browsertap_mcp-0.4.12/tests/test_state_paths_report.py +173 -0
- browsertap_mcp-0.4.12/tests/test_stdio_contract.py +25 -0
- browsertap_mcp-0.4.12/tests/test_supply_chain.py +259 -0
- browsertap_mcp-0.4.12/tests/test_tool_registration.py +292 -0
- browsertap_mcp-0.4.12/tests/test_versioning.py +431 -0
- browsertap_mcp-0.4.12/tests/tool_coverage_manifest.py +355 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Only the GitHub Actions ecosystem is watched here, and that is a decision
|
|
2
|
+
# rather than an omission.
|
|
3
|
+
#
|
|
4
|
+
# Every action in this repository is pinned to a commit SHA (see release.yml),
|
|
5
|
+
# which is the right thing for a workflow holding `id-token: write` -- but a SHA
|
|
6
|
+
# has no update channel, so without this file the pins simply age. Dependabot is
|
|
7
|
+
# that channel.
|
|
8
|
+
#
|
|
9
|
+
# The Python side is deliberately absent. `supply-chain.yml` already audits the
|
|
10
|
+
# real dependency closure with pip-audit and publishes an SBOM, and the upper
|
|
11
|
+
# bounds in pyproject.toml's `dev` extra are load-bearing: they exist so that a
|
|
12
|
+
# new ruff or pytest minor cannot move a gate number with no commit behind it.
|
|
13
|
+
# A bot raising those bounds would undo exactly what they are for. Runtime
|
|
14
|
+
# dependencies carry lower bounds on purpose too -- a user's resolver picks the
|
|
15
|
+
# version, not this repository.
|
|
16
|
+
version: 2
|
|
17
|
+
updates:
|
|
18
|
+
- package-ecosystem: github-actions
|
|
19
|
+
directory: "/"
|
|
20
|
+
schedule:
|
|
21
|
+
interval: monthly
|
|
22
|
+
open-pull-requests-limit: 3
|
|
23
|
+
commit-message:
|
|
24
|
+
prefix: chore
|
|
25
|
+
groups:
|
|
26
|
+
actions:
|
|
27
|
+
patterns:
|
|
28
|
+
- "*"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
name: BTAP live Chrome gates
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: btap-live-browser
|
|
11
|
+
cancel-in-progress: false
|
|
12
|
+
|
|
13
|
+
env:
|
|
14
|
+
BTAP_PYTHON: ${{ vars.BTAP_LIVE_PYTHON || 'python' }}
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
preflight:
|
|
18
|
+
# This job exists because the `live` job below cannot report its own missing
|
|
19
|
+
# prerequisite. A job whose `runs-on` labels match no online runner does not
|
|
20
|
+
# fail -- it queues, for up to 24 hours, and `timeout-minutes` governs
|
|
21
|
+
# execution time only, not queue time. So a "check the runner first" step
|
|
22
|
+
# placed inside `live` would never execute on the one path where it matters.
|
|
23
|
+
# Only a job that is guaranteed to start can say "there is no runner", which
|
|
24
|
+
# means a GitHub-hosted one.
|
|
25
|
+
#
|
|
26
|
+
# It gates on a repository variable rather than querying the runner API on
|
|
27
|
+
# purpose: listing self-hosted runners needs the Administration permission,
|
|
28
|
+
# which `permissions:` cannot grant to GITHUB_TOKEN, so an API check would
|
|
29
|
+
# need a long-lived PAT secret just to produce a better error message.
|
|
30
|
+
name: Check live prerequisites
|
|
31
|
+
if: github.repository == 'LinVireo/browsertap-mcp'
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
timeout-minutes: 5
|
|
34
|
+
steps:
|
|
35
|
+
- name: Require a registered self-hosted browser runner
|
|
36
|
+
if: vars.BTAP_LIVE_RUNNER != 'ready'
|
|
37
|
+
run: |
|
|
38
|
+
echo "::error title=No live runner registered::The live browser suite has no machine to run on; failing now instead of queueing."
|
|
39
|
+
cat <<'EOF'
|
|
40
|
+
The live suite drives a real Chrome window through the bridge and the
|
|
41
|
+
unpacked extension, so it only runs on a self-hosted Windows runner
|
|
42
|
+
that a maintainer owns. None is declared, and without one the `live`
|
|
43
|
+
job would sit in Queued for up to 24 hours and then expire -- which
|
|
44
|
+
reads as an infrastructure hiccup rather than as "this was never
|
|
45
|
+
configured".
|
|
46
|
+
|
|
47
|
+
To enable it:
|
|
48
|
+
|
|
49
|
+
1. Register a self-hosted runner on a Windows machine with Chrome,
|
|
50
|
+
the bridge and the unpacked extension already working. Give it the
|
|
51
|
+
labels: self-hosted Windows X64 btap
|
|
52
|
+
2. Optionally set the repository variable BTAP_LIVE_PYTHON to the
|
|
53
|
+
interpreter to use; it defaults to `python` on PATH.
|
|
54
|
+
3. Set the repository variable BTAP_LIVE_RUNNER to: ready
|
|
55
|
+
(Settings -> Secrets and variables -> Actions -> Variables)
|
|
56
|
+
|
|
57
|
+
The `btap-live` environment referenced by the live job is created by
|
|
58
|
+
GitHub on first use; add required reviewers there if a run should need
|
|
59
|
+
approval before it touches that desktop.
|
|
60
|
+
|
|
61
|
+
Until then, run the live gates locally:
|
|
62
|
+
|
|
63
|
+
python -m pytest tests/ -q -m live
|
|
64
|
+
|
|
65
|
+
and leave the browser idle while they run. The suite checks that for
|
|
66
|
+
itself now: it samples the tab list before the first test and skips the
|
|
67
|
+
whole live layer if anything moved, then compares the inventory again
|
|
68
|
+
at the end. CONTRIBUTING.md describes both checks and the override.
|
|
69
|
+
EOF
|
|
70
|
+
exit 1
|
|
71
|
+
- name: Report prerequisites satisfied
|
|
72
|
+
run: |
|
|
73
|
+
echo "BTAP_LIVE_RUNNER=ready; handing off to the self-hosted browser runner."
|
|
74
|
+
echo "Interpreter: ${BTAP_PYTHON}"
|
|
75
|
+
|
|
76
|
+
live:
|
|
77
|
+
name: Live Chrome verification
|
|
78
|
+
needs: preflight
|
|
79
|
+
environment: btap-live
|
|
80
|
+
runs-on: [self-hosted, Windows, X64, btap]
|
|
81
|
+
timeout-minutes: 10
|
|
82
|
+
steps:
|
|
83
|
+
- name: Check out source
|
|
84
|
+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
85
|
+
- name: Resolve configured Python
|
|
86
|
+
shell: powershell
|
|
87
|
+
run: |
|
|
88
|
+
$python = Get-Command $env:BTAP_PYTHON -ErrorAction Stop
|
|
89
|
+
"BTAP_PYTHON=$($python.Source)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
|
90
|
+
- name: Install editable package
|
|
91
|
+
shell: powershell
|
|
92
|
+
run: |
|
|
93
|
+
& $env:BTAP_PYTHON -m pip install -e ".[desktop,dev]"
|
|
94
|
+
- name: Verify bridge and extension diagnostics
|
|
95
|
+
shell: powershell
|
|
96
|
+
run: |
|
|
97
|
+
& $env:BTAP_PYTHON -c 'import json; from browsertap_mcp import server as S; s = S.get_setup_status(); print(json.dumps(s, ensure_ascii=False, indent=2, default=str)); raise SystemExit(2 if s.get("reload_extension_required") or s.get("restart_bridge_required") else 0)'
|
|
98
|
+
- name: Run live browser suite
|
|
99
|
+
shell: powershell
|
|
100
|
+
run: |
|
|
101
|
+
New-Item -ItemType Directory -Force artifacts | Out-Null
|
|
102
|
+
& $env:BTAP_PYTHON -m pytest tests -q -m live --junitxml=artifacts/live-junit.xml
|
|
103
|
+
- name: Recheck diagnostics after cleanup
|
|
104
|
+
if: always()
|
|
105
|
+
shell: powershell
|
|
106
|
+
run: |
|
|
107
|
+
New-Item -ItemType Directory -Force artifacts | Out-Null
|
|
108
|
+
& $env:BTAP_PYTHON -c "import json; from browsertap_mcp import server as S; print(json.dumps(S.get_setup_status(), ensure_ascii=False, indent=2, default=str))" | Out-File -Encoding utf8 artifacts/setup-status.json
|
|
109
|
+
- name: Upload live evidence
|
|
110
|
+
if: always()
|
|
111
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
112
|
+
with:
|
|
113
|
+
name: btap-live-evidence
|
|
114
|
+
path: artifacts/
|
|
115
|
+
if-no-files-found: ignore
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
name: BTAP publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Nothing here runs on a push. Publishing is a deliberate act with an
|
|
4
|
+
# irreversible result -- a PyPI filename can never be reused, so a wrong upload
|
|
5
|
+
# burns that version number for good -- so the only triggers are a manual run
|
|
6
|
+
# and a published GitHub Release.
|
|
7
|
+
on:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
inputs:
|
|
10
|
+
index:
|
|
11
|
+
description: Which index to upload to
|
|
12
|
+
type: choice
|
|
13
|
+
default: testpypi
|
|
14
|
+
options:
|
|
15
|
+
- testpypi
|
|
16
|
+
- pypi
|
|
17
|
+
release:
|
|
18
|
+
types: [published]
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
concurrency:
|
|
24
|
+
group: btap-publish
|
|
25
|
+
cancel-in-progress: false
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
build:
|
|
29
|
+
name: Build and check distributions
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
if: github.repository == 'LinVireo/browsertap-mcp'
|
|
32
|
+
timeout-minutes: 15
|
|
33
|
+
steps:
|
|
34
|
+
- name: Check out source
|
|
35
|
+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
36
|
+
with:
|
|
37
|
+
fetch-depth: 0
|
|
38
|
+
- name: Set up Python
|
|
39
|
+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
40
|
+
with:
|
|
41
|
+
python-version: '3.13'
|
|
42
|
+
cache: pip
|
|
43
|
+
# The version being published comes from the source tree, not from the ref
|
|
44
|
+
# that started this run, so a tag left behind on an older commit would ship
|
|
45
|
+
# a tree nobody validated under that name. This is the cheapest step that
|
|
46
|
+
# can fail, so it runs before anything is installed or built.
|
|
47
|
+
# A TestPyPI rehearsal may legitimately run from a branch that is not tagged
|
|
48
|
+
# yet, so there a missing tag is a note -- but a tag that points somewhere
|
|
49
|
+
# else is still a failure.
|
|
50
|
+
- name: Check the release tag names this commit (rehearsal)
|
|
51
|
+
if: github.event_name == 'workflow_dispatch' && inputs.index == 'testpypi'
|
|
52
|
+
run: python -m scripts.check_release_tag --allow-missing-tag
|
|
53
|
+
- name: Check the release tag names this commit
|
|
54
|
+
if: github.event_name == 'release' || inputs.index == 'pypi'
|
|
55
|
+
run: python -m scripts.check_release_tag
|
|
56
|
+
- name: Install package and development dependencies
|
|
57
|
+
run: python -m pip install --upgrade pip && python -m pip install -e ".[dev,desktop]"
|
|
58
|
+
# A release must not ship a tree that fails its own gates. This is the
|
|
59
|
+
# offline suite only: the live-Chrome gates need a real browser and are a
|
|
60
|
+
# separate, self-hosted workflow.
|
|
61
|
+
- name: Run offline tests with coverage
|
|
62
|
+
run: >-
|
|
63
|
+
python -m pytest tests -q
|
|
64
|
+
--cov=browsertap_mcp
|
|
65
|
+
--cov-fail-under=85
|
|
66
|
+
- name: Check documentation and versions
|
|
67
|
+
run: python -m scripts.check_tool_docs --format markdown
|
|
68
|
+
- name: Build distributions
|
|
69
|
+
run: python -m build --wheel --sdist --outdir dist
|
|
70
|
+
# `check_distribution` reads what is inside the archives -- required files,
|
|
71
|
+
# no machine-local data, and the publishing metadata the index needs.
|
|
72
|
+
- name: Check distribution contents and metadata
|
|
73
|
+
run: python -m scripts.check_distribution dist
|
|
74
|
+
# And this is the other question a reader actually cares about: does a
|
|
75
|
+
# stranger's `pip install` leave them with something that runs. The wheel
|
|
76
|
+
# goes into a throwaway virtual environment with no repository on the path,
|
|
77
|
+
# and the console script, the packaged skills, and the extension files are
|
|
78
|
+
# exercised there rather than inferred from the archive listing.
|
|
79
|
+
- name: Check a clean install of the built wheel
|
|
80
|
+
run: python -m scripts.check_install dist
|
|
81
|
+
# `twine check` is the other half: it renders the long description the way
|
|
82
|
+
# the index will. Only a built archive can be checked this way, and only
|
|
83
|
+
# before the upload -- afterwards the version is spent.
|
|
84
|
+
- name: Check the rendered long description
|
|
85
|
+
run: python -m twine check --strict dist/*
|
|
86
|
+
# Supply-chain evidence for exactly what is about to be published: the
|
|
87
|
+
# dependency closure this wheel pulls onto a user's machine, audited against
|
|
88
|
+
# the advisory database, plus a CycloneDX SBOM of that same closure. Blocking
|
|
89
|
+
# on purpose, unlike the informational run in supply-chain.yml -- a
|
|
90
|
+
# known-vulnerable dependency is a reason not to publish, and this is the
|
|
91
|
+
# last point at which not publishing is still free.
|
|
92
|
+
- name: Audit and describe what the wheel installs
|
|
93
|
+
run: |
|
|
94
|
+
set -euo pipefail
|
|
95
|
+
python -m venv sbom-venv
|
|
96
|
+
./sbom-venv/bin/python -m pip install --quiet --no-input dist/*.whl
|
|
97
|
+
./sbom-venv/bin/python -m pip freeze --exclude-editable > runtime-requirements.txt
|
|
98
|
+
cat runtime-requirements.txt
|
|
99
|
+
# `pip freeze` lists the package under audit alongside its dependencies,
|
|
100
|
+
# and because it was installed from a local wheel it comes out as a PEP
|
|
101
|
+
# 610 direct reference -- `browsertap-mcp @ file:///.../*.whl#sha256=...`.
|
|
102
|
+
# `pip-audit --strict` then fails the whole run with "Dependency not found
|
|
103
|
+
# on PyPI and could not be audited", naming the exact version this
|
|
104
|
+
# workflow is about to upload, which by definition is not on the index
|
|
105
|
+
# yet. That made the first release of any name impossible to publish and
|
|
106
|
+
# went unnoticed because the same step does not block in
|
|
107
|
+
# supply-chain.yml. The subject of an audit is not one of its own
|
|
108
|
+
# dependencies, so it is dropped from the auditor's input -- after the
|
|
109
|
+
# full freeze has been printed above, so the wheel and its hash stay in
|
|
110
|
+
# the log as evidence. The SBOM below still describes the whole
|
|
111
|
+
# environment, package included, which is what an SBOM is for.
|
|
112
|
+
sed -i -E '/^browsertap[-_]mcp([[:space:]=@]|$)/d' runtime-requirements.txt
|
|
113
|
+
python -m pip install --quiet pip-audit==2.10.1 cyclonedx-bom==7.3.1
|
|
114
|
+
pip-audit --requirement runtime-requirements.txt --no-deps --strict --desc
|
|
115
|
+
mkdir -p sbom
|
|
116
|
+
python -m cyclonedx_py environment ./sbom-venv --spec-version 1.6 \
|
|
117
|
+
--output-format JSON --output-file sbom/browsertap-mcp.cdx.json
|
|
118
|
+
# A separate artifact, never dist/: the publish job uploads everything under
|
|
119
|
+
# dist/ to the index, and a stray .json there would either be rejected or
|
|
120
|
+
# published as part of the release.
|
|
121
|
+
- name: Upload the SBOM
|
|
122
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
123
|
+
with:
|
|
124
|
+
name: btap-sbom
|
|
125
|
+
path: sbom/
|
|
126
|
+
if-no-files-found: error
|
|
127
|
+
# The version the archives carry is what the index will hold forever, so it
|
|
128
|
+
# is printed here rather than inferred from the tag by a reader later.
|
|
129
|
+
- name: Report the version being published
|
|
130
|
+
run: python -c "import pathlib; print('\n'.join(p.name for p in sorted(pathlib.Path('dist').iterdir())))"
|
|
131
|
+
- name: Upload built distributions
|
|
132
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
133
|
+
with:
|
|
134
|
+
name: btap-dist
|
|
135
|
+
path: dist/
|
|
136
|
+
if-no-files-found: error
|
|
137
|
+
|
|
138
|
+
publish:
|
|
139
|
+
name: Upload to ${{ github.event_name == 'release' && 'pypi' || inputs.index }}
|
|
140
|
+
needs: build
|
|
141
|
+
runs-on: ubuntu-latest
|
|
142
|
+
# The environment is the approval gate: configure required reviewers on it so
|
|
143
|
+
# a human confirms before an upload that cannot be undone. It is also where
|
|
144
|
+
# the PyPI Trusted Publisher is scoped -- see CONTRIBUTING.md, "Publishing to
|
|
145
|
+
# PyPI", for the one-time setup this workflow assumes.
|
|
146
|
+
environment: ${{ github.event_name == 'release' && 'pypi' || inputs.index }}
|
|
147
|
+
permissions:
|
|
148
|
+
# Trusted Publishing exchanges this short-lived OIDC token for an upload
|
|
149
|
+
# token at request time. No API token is stored in the repository, so there
|
|
150
|
+
# is none to leak or rotate.
|
|
151
|
+
id-token: write
|
|
152
|
+
timeout-minutes: 15
|
|
153
|
+
steps:
|
|
154
|
+
- name: Download built distributions
|
|
155
|
+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
|
156
|
+
with:
|
|
157
|
+
name: btap-dist
|
|
158
|
+
path: dist/
|
|
159
|
+
# The only action here not pinned to a commit. PyPA publishes security and
|
|
160
|
+
# protocol fixes for Trusted Publishing onto this branch and asks callers
|
|
161
|
+
# to track it, and a stale pin on the one step holding `id-token: write`
|
|
162
|
+
# fails closed in the worst place -- a release that cannot upload. The
|
|
163
|
+
# trade-off is deliberate and asserted in tests/test_supply_chain.py, so
|
|
164
|
+
# adding a second floating action is a test failure rather than a habit.
|
|
165
|
+
- name: Publish to TestPyPI
|
|
166
|
+
if: github.event_name == 'workflow_dispatch' && inputs.index == 'testpypi'
|
|
167
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
168
|
+
with:
|
|
169
|
+
repository-url: https://test.pypi.org/legacy/
|
|
170
|
+
print-hash: true
|
|
171
|
+
- name: Publish to PyPI
|
|
172
|
+
if: github.event_name == 'release' || inputs.index == 'pypi'
|
|
173
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
174
|
+
with:
|
|
175
|
+
print-hash: true
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
name: BTAP supply chain
|
|
2
|
+
|
|
3
|
+
# Two questions this repository could not answer before: did a credential ever
|
|
4
|
+
# reach a commit, and what exactly does `pip install browsertap-mcp` pull onto a
|
|
5
|
+
# stranger's machine. Both are answered here on every push and once a week --
|
|
6
|
+
# weekly because an advisory can appear without a single line of this repository
|
|
7
|
+
# changing.
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
pull_request:
|
|
11
|
+
schedule:
|
|
12
|
+
# Monday morning, off the hour so it does not queue behind everyone's cron.
|
|
13
|
+
- cron: '17 6 * * 1'
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: supply-chain-${{ github.workflow }}-${{ github.ref }}
|
|
21
|
+
cancel-in-progress: true
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
secrets:
|
|
25
|
+
name: Secret scan
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
timeout-minutes: 10
|
|
28
|
+
steps:
|
|
29
|
+
- name: Check out source
|
|
30
|
+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
31
|
+
with:
|
|
32
|
+
# The whole point is the history: a secret that was committed and then
|
|
33
|
+
# deleted is still published. A shallow clone would scan one commit and
|
|
34
|
+
# report a clean repository.
|
|
35
|
+
fetch-depth: 0
|
|
36
|
+
- name: Install the pinned gitleaks build
|
|
37
|
+
env:
|
|
38
|
+
GITLEAKS_VERSION: 8.30.1
|
|
39
|
+
# sha256 of gitleaks_8.30.1_linux_x64.tar.gz, read out of that release's
|
|
40
|
+
# own checksums file. Version and digest are bumped together and never
|
|
41
|
+
# separately: a scanner resolved at run time is a third party that can
|
|
42
|
+
# change what it does to this repository between two green runs.
|
|
43
|
+
GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
|
|
44
|
+
run: |
|
|
45
|
+
set -euo pipefail
|
|
46
|
+
base=https://github.com/gitleaks/gitleaks/releases/download
|
|
47
|
+
archive="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
|
|
48
|
+
curl --fail --silent --show-error --location --output gitleaks.tar.gz \
|
|
49
|
+
"${base}/v${GITLEAKS_VERSION}/${archive}"
|
|
50
|
+
echo "${GITLEAKS_SHA256} gitleaks.tar.gz" | sha256sum --check --strict -
|
|
51
|
+
tar -xzf gitleaks.tar.gz gitleaks
|
|
52
|
+
./gitleaks version
|
|
53
|
+
# --redact is not cosmetic: a scan report is world-readable build output, so
|
|
54
|
+
# printing the secret it found would publish the very thing it detected.
|
|
55
|
+
- name: Scan the full history for secrets
|
|
56
|
+
run: >-
|
|
57
|
+
./gitleaks git . --no-banner --redact --exit-code 1
|
|
58
|
+
--report-format json --report-path gitleaks-history.json
|
|
59
|
+
- name: Scan the working tree for secrets
|
|
60
|
+
run: >-
|
|
61
|
+
./gitleaks dir . --no-banner --redact --exit-code 1
|
|
62
|
+
--report-format json --report-path gitleaks-worktree.json
|
|
63
|
+
- name: Upload scan reports
|
|
64
|
+
if: always()
|
|
65
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
66
|
+
with:
|
|
67
|
+
name: btap-secret-scan
|
|
68
|
+
path: |
|
|
69
|
+
gitleaks-history.json
|
|
70
|
+
gitleaks-worktree.json
|
|
71
|
+
if-no-files-found: ignore
|
|
72
|
+
|
|
73
|
+
dependencies:
|
|
74
|
+
name: Dependency audit and SBOM
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
timeout-minutes: 15
|
|
77
|
+
steps:
|
|
78
|
+
- name: Check out source
|
|
79
|
+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
80
|
+
- name: Set up Python
|
|
81
|
+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
82
|
+
with:
|
|
83
|
+
python-version: '3.13'
|
|
84
|
+
cache: pip
|
|
85
|
+
# The runtime closure, not this job's environment: development extras are
|
|
86
|
+
# not what a user installs, and auditing them would report risk nobody is
|
|
87
|
+
# exposed to while hiding nothing.
|
|
88
|
+
- name: Resolve the closure a plain install produces
|
|
89
|
+
run: |
|
|
90
|
+
set -euo pipefail
|
|
91
|
+
python -m venv runtime-venv
|
|
92
|
+
./runtime-venv/bin/python -m pip install --quiet --upgrade pip
|
|
93
|
+
./runtime-venv/bin/python -m pip install --quiet .
|
|
94
|
+
./runtime-venv/bin/python -m pip freeze --exclude-editable > runtime-requirements.txt
|
|
95
|
+
cat runtime-requirements.txt
|
|
96
|
+
# The package itself is in that list, as a PEP 610 direct reference to
|
|
97
|
+
# the local checkout, and `pip-audit --strict` treats a requirement it
|
|
98
|
+
# cannot resolve on PyPI as a failure. Here the step is
|
|
99
|
+
# `continue-on-error`, so the job stayed green while the audit had in
|
|
100
|
+
# fact stopped -- the whole closure was going unaudited and the report
|
|
101
|
+
# said so to nobody. Same removal as release.yml, where it is fatal.
|
|
102
|
+
sed -i -E '/^browsertap[-_]mcp([[:space:]=@]|$)/d' runtime-requirements.txt
|
|
103
|
+
# Informational here, blocking in release.yml. An advisory published
|
|
104
|
+
# overnight would otherwise turn every branch red for a reason unrelated to
|
|
105
|
+
# the commit that is being pushed -- but it is a perfectly good reason not
|
|
106
|
+
# to publish a new version, which is where the same check is required.
|
|
107
|
+
- name: Audit the closure against the advisory database (informational)
|
|
108
|
+
continue-on-error: true
|
|
109
|
+
run: |
|
|
110
|
+
set -euo pipefail
|
|
111
|
+
python -m pip install --quiet pip-audit==2.10.1
|
|
112
|
+
pip-audit --requirement runtime-requirements.txt --no-deps --strict --desc \
|
|
113
|
+
--format markdown --output pip-audit.md
|
|
114
|
+
cat pip-audit.md
|
|
115
|
+
- name: Describe the closure as a CycloneDX SBOM
|
|
116
|
+
run: |
|
|
117
|
+
set -euo pipefail
|
|
118
|
+
python -m pip install --quiet cyclonedx-bom==7.3.1
|
|
119
|
+
mkdir -p sbom
|
|
120
|
+
python -m cyclonedx_py environment ./runtime-venv --spec-version 1.6 \
|
|
121
|
+
--output-format JSON --output-file sbom/browsertap-mcp.cdx.json
|
|
122
|
+
python -c "import json; print(len(json.load(open('sbom/browsertap-mcp.cdx.json')).get('components', [])), 'components described')"
|
|
123
|
+
- name: Upload the audit and the SBOM
|
|
124
|
+
if: always()
|
|
125
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
126
|
+
with:
|
|
127
|
+
name: btap-supply-chain
|
|
128
|
+
path: |
|
|
129
|
+
runtime-requirements.txt
|
|
130
|
+
pip-audit.md
|
|
131
|
+
sbom/
|
|
132
|
+
if-no-files-found: ignore
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
name: BTAP offline gates
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
# One in-flight run per ref. Re-pushing a branch cancels the previous run
|
|
11
|
+
# instead of racing it, so the newest commit owns the gate result.
|
|
12
|
+
concurrency:
|
|
13
|
+
group: offline-${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
offline:
|
|
18
|
+
name: Python ${{ matrix.python-version }}
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
timeout-minutes: 15
|
|
21
|
+
strategy:
|
|
22
|
+
fail-fast: false
|
|
23
|
+
matrix:
|
|
24
|
+
python-version: ['3.10', '3.11', '3.12', '3.13']
|
|
25
|
+
steps:
|
|
26
|
+
- name: Check out source
|
|
27
|
+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
28
|
+
with:
|
|
29
|
+
fetch-depth: 0
|
|
30
|
+
- name: Set up Python
|
|
31
|
+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
32
|
+
with:
|
|
33
|
+
python-version: ${{ matrix.python-version }}
|
|
34
|
+
cache: pip
|
|
35
|
+
- name: Install package and development dependencies
|
|
36
|
+
run: python -m pip install --upgrade pip && python -m pip install -e ".[dev,desktop]"
|
|
37
|
+
- name: Check Python lint
|
|
38
|
+
if: matrix.python-version == '3.13'
|
|
39
|
+
run: python -m ruff check src tests scripts
|
|
40
|
+
- name: Compile Python sources
|
|
41
|
+
run: python -m compileall -q src
|
|
42
|
+
- name: Prepare evidence directory
|
|
43
|
+
run: mkdir -p artifacts
|
|
44
|
+
- name: Run offline tests with coverage
|
|
45
|
+
run: >-
|
|
46
|
+
python -m pytest tests -q
|
|
47
|
+
--cov=browsertap_mcp
|
|
48
|
+
--cov-fail-under=85
|
|
49
|
+
--cov-report=term-missing
|
|
50
|
+
--cov-report=xml
|
|
51
|
+
--cov-report=json:artifacts/coverage.json
|
|
52
|
+
--junitxml=artifacts/offline-junit.xml
|
|
53
|
+
- name: Check tool behavior coverage
|
|
54
|
+
run: >-
|
|
55
|
+
python -m scripts.tool_coverage_report --format markdown
|
|
56
|
+
--output artifacts/tool-coverage-offline.json
|
|
57
|
+
- name: Check documentation and versions
|
|
58
|
+
run: python -m scripts.check_tool_docs --format markdown
|
|
59
|
+
- name: Check SemVer bump on release branches
|
|
60
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release/') && github.event.before != '0000000000000000000000000000000000000000'
|
|
61
|
+
run: python -m scripts.versioning check-bump --base "${{ github.event.before }}"
|
|
62
|
+
- name: Build distributions
|
|
63
|
+
run: python -m build --wheel --sdist --outdir artifacts/dist
|
|
64
|
+
- name: Check distribution contents
|
|
65
|
+
run: python -m scripts.check_distribution artifacts/dist
|
|
66
|
+
# The other half of the same question: not what is inside the archive,
|
|
67
|
+
# but what a stranger has after `pip install`. Runs on every version in
|
|
68
|
+
# the matrix, because package data and console scripts are exactly what
|
|
69
|
+
# breaks on one interpreter and not another.
|
|
70
|
+
- name: Check a clean install of the built wheel
|
|
71
|
+
run: python -m scripts.check_install artifacts/dist
|
|
72
|
+
- name: Check installed dependency consistency
|
|
73
|
+
run: python -m pip check
|
|
74
|
+
# The evidence chain is only meaningful if something other than the release
|
|
75
|
+
# operator's own machine exercises it. CI seals the artifacts it just
|
|
76
|
+
# generated and immediately re-validates the seal, so a broken manifest
|
|
77
|
+
# writer or validator fails here rather than at release time. Committed
|
|
78
|
+
# artifacts are not used: `artifacts/` is gitignored, so this always runs
|
|
79
|
+
# against a clean checkout plus this run's own output.
|
|
80
|
+
- name: Seal and verify this run's evidence
|
|
81
|
+
if: matrix.python-version == '3.13'
|
|
82
|
+
run: >-
|
|
83
|
+
python -m scripts.evidence_manifest &&
|
|
84
|
+
python -m scripts.evidence_manifest --check
|
|
85
|
+
# Scored for visibility only. The live-Chrome gates cannot pass on a
|
|
86
|
+
# headless runner, so a non-zero exit is expected and must not fail the job.
|
|
87
|
+
- name: Score offline acceptance gates (informational)
|
|
88
|
+
if: matrix.python-version == '3.13'
|
|
89
|
+
continue-on-error: true
|
|
90
|
+
run: python -m scripts.acceptance_report --output artifacts/acceptance-report.md
|
|
91
|
+
- name: Upload gate artifacts
|
|
92
|
+
if: always()
|
|
93
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
94
|
+
with:
|
|
95
|
+
name: btap-offline-${{ matrix.python-version }}
|
|
96
|
+
path: artifacts/
|
|
97
|
+
if-no-files-found: ignore
|
|
98
|
+
|
|
99
|
+
# Every OS-specific failure this product has is a Windows one: wsgiref resets a
|
|
100
|
+
# rejected request that still has an unread body (so a 401 reaches the caller as
|
|
101
|
+
# a dropped connection), the host lock is `SO_EXCLUSIVEADDRUSE`, `pythonw.exe`
|
|
102
|
+
# is a forwarding stub whose child holds the port, a minimised window accepts
|
|
103
|
+
# focus calls that put nothing on screen, and a log file cannot be renamed while
|
|
104
|
+
# the daemon holds it open. The job above runs on Linux only, so none of that was
|
|
105
|
+
# ever exercised by CI -- and `bridge.py`, where most of it lives, is the least
|
|
106
|
+
# covered module in the package. Oldest and newest interpreter only: this is
|
|
107
|
+
# about the platform, and the version matrix is already covered above.
|
|
108
|
+
windows-offline:
|
|
109
|
+
name: Windows offline (Python ${{ matrix.python-version }})
|
|
110
|
+
runs-on: windows-latest
|
|
111
|
+
timeout-minutes: 20
|
|
112
|
+
defaults:
|
|
113
|
+
run:
|
|
114
|
+
# Deliberately the same shell as the Linux job. `mkdir -p` and the folded
|
|
115
|
+
# multi-line commands below are POSIX, and a step whose text differs
|
|
116
|
+
# between the two jobs stops being a comparison of the platform.
|
|
117
|
+
shell: bash
|
|
118
|
+
strategy:
|
|
119
|
+
fail-fast: false
|
|
120
|
+
matrix:
|
|
121
|
+
python-version: ['3.10', '3.13']
|
|
122
|
+
steps:
|
|
123
|
+
- name: Check out source
|
|
124
|
+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
125
|
+
- name: Set up Python
|
|
126
|
+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
127
|
+
with:
|
|
128
|
+
python-version: ${{ matrix.python-version }}
|
|
129
|
+
cache: pip
|
|
130
|
+
- name: Install package and development dependencies
|
|
131
|
+
run: python -m pip install --upgrade pip && python -m pip install -e ".[dev,desktop]"
|
|
132
|
+
- name: Compile Python sources
|
|
133
|
+
run: python -m compileall -q src
|
|
134
|
+
- name: Prepare evidence directory
|
|
135
|
+
run: mkdir -p artifacts
|
|
136
|
+
# No coverage gate here: the threshold is enforced once, on Linux, and a
|
|
137
|
+
# second instrumented run would only add wall clock. The junit file is kept
|
|
138
|
+
# because which test failed on Windows is the whole point of this job.
|
|
139
|
+
- name: Run offline tests
|
|
140
|
+
run: >-
|
|
141
|
+
python -m pytest tests -q
|
|
142
|
+
--junitxml=artifacts/windows-offline-junit.xml
|
|
143
|
+
# What a stranger has after `pip install` on the platform most of this
|
|
144
|
+
# product's users run, where the console script is a generated .exe rather
|
|
145
|
+
# than a shebang. Newest interpreter only, to keep the job short.
|
|
146
|
+
- name: Build distributions
|
|
147
|
+
if: matrix.python-version == '3.13'
|
|
148
|
+
run: python -m build --wheel --sdist --outdir artifacts/dist
|
|
149
|
+
- name: Check distribution contents
|
|
150
|
+
if: matrix.python-version == '3.13'
|
|
151
|
+
run: python -m scripts.check_distribution artifacts/dist
|
|
152
|
+
- name: Check a clean install of the built wheel
|
|
153
|
+
if: matrix.python-version == '3.13'
|
|
154
|
+
run: python -m scripts.check_install artifacts/dist
|
|
155
|
+
- name: Check installed dependency consistency
|
|
156
|
+
run: python -m pip check
|
|
157
|
+
- name: Upload gate artifacts
|
|
158
|
+
if: always()
|
|
159
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
160
|
+
with:
|
|
161
|
+
name: btap-windows-offline-${{ matrix.python-version }}
|
|
162
|
+
path: artifacts/
|
|
163
|
+
if-no-files-found: ignore
|
|
164
|
+
|
|
165
|
+
# The third platform, and the only one with no coverage at all until now. Two
|
|
166
|
+
# code paths in this package exist solely for it -- `_macos_pointer_position`
|
|
167
|
+
# reads the cursor through Quartz, and `_darwin_process_identity` is how the
|
|
168
|
+
# bridge decides whether the PID in its lock file is still the daemon it wrote
|
|
169
|
+
# -- and neither had ever been executed by anything but a reviewer's eyes. The
|
|
170
|
+
# quiet-input gate also degrades here rather than on Windows: macOS answers the
|
|
171
|
+
# pointer read only once someone grants the accessibility permission, which a
|
|
172
|
+
# hosted runner never does, so this job is where `input_quiet.enforced: false`
|
|
173
|
+
# is actually produced instead of merely documented. Oldest and newest
|
|
174
|
+
# interpreter only, same as Windows: this is about the platform.
|
|
175
|
+
macos-offline:
|
|
176
|
+
name: macOS offline (Python ${{ matrix.python-version }})
|
|
177
|
+
runs-on: macos-latest
|
|
178
|
+
timeout-minutes: 20
|
|
179
|
+
defaults:
|
|
180
|
+
run:
|
|
181
|
+
# Same shell as the other two jobs on purpose; a step whose text differs
|
|
182
|
+
# between them stops being a comparison of the platform.
|
|
183
|
+
shell: bash
|
|
184
|
+
strategy:
|
|
185
|
+
fail-fast: false
|
|
186
|
+
matrix:
|
|
187
|
+
python-version: ['3.10', '3.13']
|
|
188
|
+
steps:
|
|
189
|
+
- name: Check out source
|
|
190
|
+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
191
|
+
- name: Set up Python
|
|
192
|
+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
193
|
+
with:
|
|
194
|
+
python-version: ${{ matrix.python-version }}
|
|
195
|
+
cache: pip
|
|
196
|
+
- name: Install package and development dependencies
|
|
197
|
+
run: python -m pip install --upgrade pip && python -m pip install -e ".[dev,desktop]"
|
|
198
|
+
- name: Compile Python sources
|
|
199
|
+
run: python -m compileall -q src
|
|
200
|
+
- name: Prepare evidence directory
|
|
201
|
+
run: mkdir -p artifacts
|
|
202
|
+
# No coverage gate here either: the threshold is enforced once, on Linux.
|
|
203
|
+
- name: Run offline tests
|
|
204
|
+
run: >-
|
|
205
|
+
python -m pytest tests -q
|
|
206
|
+
--junitxml=artifacts/macos-offline-junit.xml
|
|
207
|
+
# Console scripts and package data on the third platform. Newest
|
|
208
|
+
# interpreter only, to keep the job short.
|
|
209
|
+
- name: Build distributions
|
|
210
|
+
if: matrix.python-version == '3.13'
|
|
211
|
+
run: python -m build --wheel --sdist --outdir artifacts/dist
|
|
212
|
+
- name: Check distribution contents
|
|
213
|
+
if: matrix.python-version == '3.13'
|
|
214
|
+
run: python -m scripts.check_distribution artifacts/dist
|
|
215
|
+
- name: Check a clean install of the built wheel
|
|
216
|
+
if: matrix.python-version == '3.13'
|
|
217
|
+
run: python -m scripts.check_install artifacts/dist
|
|
218
|
+
- name: Check installed dependency consistency
|
|
219
|
+
run: python -m pip check
|
|
220
|
+
- name: Upload gate artifacts
|
|
221
|
+
if: always()
|
|
222
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
223
|
+
with:
|
|
224
|
+
name: btap-macos-offline-${{ matrix.python-version }}
|
|
225
|
+
path: artifacts/
|
|
226
|
+
if-no-files-found: ignore
|