watcherobot 0.1.1__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.
- watcherobot-0.1.1/.github/scripts/luxiao_review.py +151 -0
- watcherobot-0.1.1/.github/workflows/pr-review.yml +52 -0
- watcherobot-0.1.1/.github/workflows/prepare-release.yml +169 -0
- watcherobot-0.1.1/.github/workflows/release.yml +288 -0
- watcherobot-0.1.1/.github/workflows/sdk-ci.yml +183 -0
- watcherobot-0.1.1/.gitignore +15 -0
- watcherobot-0.1.1/CHANGELOG.md +33 -0
- watcherobot-0.1.1/LICENSE +177 -0
- watcherobot-0.1.1/PKG-INFO +273 -0
- watcherobot-0.1.1/README.md +233 -0
- watcherobot-0.1.1/README.zh-CN.md +210 -0
- watcherobot-0.1.1/docs/application-marketplace/README.md +14 -0
- watcherobot-0.1.1/docs/application-marketplace/application-cli-reference.md +218 -0
- watcherobot-0.1.1/docs/application-marketplace/distribution-contract.md +124 -0
- watcherobot-0.1.1/docs/application-marketplace/hugging-face-oauth.md +49 -0
- watcherobot-0.1.1/docs/application-marketplace/implementation-progress.md +189 -0
- watcherobot-0.1.1/docs/application-marketplace/publishing-quality-gates.zh-CN.md +93 -0
- watcherobot-0.1.1/docs/application-marketplace/sdk-application-usage.md +422 -0
- watcherobot-0.1.1/docs/application-marketplace/sdk-application-usage.zh-CN.md +453 -0
- watcherobot-0.1.1/docs/bluetooth-provisioning.md +158 -0
- watcherobot-0.1.1/docs/cli-reference.md +378 -0
- watcherobot-0.1.1/docs/cli-reference.zh-CN.md +307 -0
- watcherobot-0.1.1/docs/contracts/daemon-control-protocol.md +48 -0
- watcherobot-0.1.1/docs/contracts/runtime-profile-index.md +202 -0
- watcherobot-0.1.1/docs/device-states/README.md +145 -0
- watcherobot-0.1.1/docs/face-tracking-preview.md +92 -0
- watcherobot-0.1.1/docs/hardware-testing.md +73 -0
- watcherobot-0.1.1/docs/installation.md +172 -0
- watcherobot-0.1.1/docs/installation.zh-CN.md +163 -0
- watcherobot-0.1.1/docs/microphone-audio.md +54 -0
- watcherobot-0.1.1/docs/migration/server-daemon-dependencies.md +76 -0
- watcherobot-0.1.1/docs/releasing.md +124 -0
- watcherobot-0.1.1/docs/resources.md +130 -0
- watcherobot-0.1.1/docs/troubleshooting.md +27 -0
- watcherobot-0.1.1/examples/README.md +27 -0
- watcherobot-0.1.1/examples/assets/sample_speech.wav +0 -0
- watcherobot-0.1.1/examples/capture_photo/app.json +8 -0
- watcherobot-0.1.1/examples/capture_photo/app.py +19 -0
- watcherobot-0.1.1/examples/face_tracking_preview/app.json +8 -0
- watcherobot-0.1.1/examples/face_tracking_preview/app.py +44 -0
- watcherobot-0.1.1/examples/hello_robot/app.json +8 -0
- watcherobot-0.1.1/examples/hello_robot/app.py +17 -0
- watcherobot-0.1.1/examples/play_audio_file/app.json +8 -0
- watcherobot-0.1.1/examples/play_audio_file/app.py +19 -0
- watcherobot-0.1.1/examples/quickstart/app.json +8 -0
- watcherobot-0.1.1/examples/quickstart/app.py +32 -0
- watcherobot-0.1.1/examples/record_microphone/app.json +8 -0
- watcherobot-0.1.1/examples/record_microphone/app.py +29 -0
- watcherobot-0.1.1/examples/sdk_media_lab/.watcherignore +1 -0
- watcherobot-0.1.1/examples/sdk_media_lab/README.md +101 -0
- watcherobot-0.1.1/examples/sdk_media_lab/app.json +11 -0
- watcherobot-0.1.1/examples/sdk_media_lab/app.py +78 -0
- watcherobot-0.1.1/examples/sdk_media_lab/assets/sample_speech.wav +0 -0
- watcherobot-0.1.1/examples/sdk_media_lab/icon.svg +9 -0
- watcherobot-0.1.1/examples/sdk_media_lab/service.py +1112 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/animation-confirmation.mjs +20 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/animation-random.mjs +50 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/app.js +1980 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/i18n.mjs +438 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/index.html +375 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/media-resource-policy.mjs +61 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/mjpeg-chunk-reassembly.mjs +124 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/resource-health.mjs +107 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/rtc-audio-capture.mjs +11 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/rtc-audio-health.mjs +43 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/rtc-audio-latency.mjs +86 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/styles.css +515 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/video-feedback.mjs +68 -0
- watcherobot-0.1.1/examples/sdk_media_lab/web/video-frame-queue.mjs +19 -0
- watcherobot-0.1.1/pyproject.toml +73 -0
- watcherobot-0.1.1/src/watcherobot/__init__.py +83 -0
- watcherobot-0.1.1/src/watcherobot/_internal/__init__.py +1 -0
- watcherobot-0.1.1/src/watcherobot/_internal/audio_status.py +40 -0
- watcherobot-0.1.1/src/watcherobot/application/__init__.py +30 -0
- watcherobot-0.1.1/src/watcherobot/application/channels.py +35 -0
- watcherobot-0.1.1/src/watcherobot/application/context.py +99 -0
- watcherobot-0.1.1/src/watcherobot/application/desktop.py +26 -0
- watcherobot-0.1.1/src/watcherobot/application/ignore.py +54 -0
- watcherobot-0.1.1/src/watcherobot/application/project.py +336 -0
- watcherobot-0.1.1/src/watcherobot/application/rtc.py +402 -0
- watcherobot-0.1.1/src/watcherobot/application/transport.py +533 -0
- watcherobot-0.1.1/src/watcherobot/audio.py +81 -0
- watcherobot-0.1.1/src/watcherobot/cli.py +1186 -0
- watcherobot-0.1.1/src/watcherobot/distribution/__init__.py +154 -0
- watcherobot-0.1.1/src/watcherobot/distribution/catalog_submission.py +176 -0
- watcherobot-0.1.1/src/watcherobot/distribution/check.py +66 -0
- watcherobot-0.1.1/src/watcherobot/distribution/cli.py +1077 -0
- watcherobot-0.1.1/src/watcherobot/distribution/credentials.py +85 -0
- watcherobot-0.1.1/src/watcherobot/distribution/download.py +242 -0
- watcherobot-0.1.1/src/watcherobot/distribution/events.py +172 -0
- watcherobot-0.1.1/src/watcherobot/distribution/hf_marketplace.py +286 -0
- watcherobot-0.1.1/src/watcherobot/distribution/hf_publish.py +351 -0
- watcherobot-0.1.1/src/watcherobot/distribution/hub_http.py +128 -0
- watcherobot-0.1.1/src/watcherobot/distribution/install.py +1010 -0
- watcherobot-0.1.1/src/watcherobot/distribution/login.py +318 -0
- watcherobot-0.1.1/src/watcherobot/distribution/marketplace.py +228 -0
- watcherobot-0.1.1/src/watcherobot/distribution/oauth_http.py +229 -0
- watcherobot-0.1.1/src/watcherobot/distribution/ports.py +327 -0
- watcherobot-0.1.1/src/watcherobot/distribution/publish.py +205 -0
- watcherobot-0.1.1/src/watcherobot/distribution/publish_files.py +140 -0
- watcherobot-0.1.1/src/watcherobot/distribution/source_files.py +107 -0
- watcherobot-0.1.1/src/watcherobot/distribution/submit.py +398 -0
- watcherobot-0.1.1/src/watcherobot/errors.py +50 -0
- watcherobot-0.1.1/src/watcherobot/inputs.py +180 -0
- watcherobot-0.1.1/src/watcherobot/job.py +115 -0
- watcherobot-0.1.1/src/watcherobot/media.py +243 -0
- watcherobot-0.1.1/src/watcherobot/opus.py +79 -0
- watcherobot-0.1.1/src/watcherobot/protocol.py +95 -0
- watcherobot-0.1.1/src/watcherobot/provisioning/__init__.py +41 -0
- watcherobot-0.1.1/src/watcherobot/provisioning/backend.py +50 -0
- watcherobot-0.1.1/src/watcherobot/provisioning/bleak_backend.py +271 -0
- watcherobot-0.1.1/src/watcherobot/provisioning/errors.py +82 -0
- watcherobot-0.1.1/src/watcherobot/provisioning/models.py +102 -0
- watcherobot-0.1.1/src/watcherobot/provisioning/protocol.py +190 -0
- watcherobot-0.1.1/src/watcherobot/provisioning/service.py +526 -0
- watcherobot-0.1.1/src/watcherobot/py.typed +0 -0
- watcherobot-0.1.1/src/watcherobot/robot.py +1093 -0
- watcherobot-0.1.1/src/watcherobot/runtime/__init__.py +1 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/__init__.py +16 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/__main__.py +159 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/application/__init__.py +44 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/application/bridge.py +237 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/application/client.py +140 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/application/launcher.py +396 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/application/logging.py +89 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/application/manifest.py +352 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/application/runtime.py +551 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/application/session.py +146 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/connections/__init__.py +39 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/connections/device_state.py +41 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/connections/identity.py +95 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/connections/registry.py +134 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/connections/websocket_server.py +482 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/control/__init__.py +5 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/control/rest.py +641 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/instance.py +146 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/logging.py +90 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/maintenance/__init__.py +5 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/maintenance/card_reader.py +906 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/maintenance/releases.py +426 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/maintenance/service.py +1475 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/maintenance/work_assets.py +241 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/maintenance/works.py +722 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/pairing/__init__.py +14 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/pairing/protocol.py +434 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/pairing/session.py +302 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/pairing/udp.py +659 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/preview/__init__.py +5 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/preview/face_tracking.py +113 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/preview/udp_feedback.py +67 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/preview/udp_protocol.py +276 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/preview/udp_service.py +214 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/routing/__init__.py +5 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/routing/raw.py +93 -0
- watcherobot-0.1.1/src/watcherobot/runtime/daemon/runtime.py +543 -0
- watcherobot-0.1.1/src/watcherobot/runtime/frozen_entry.py +32 -0
- watcherobot-0.1.1/src/watcherobot/vision.py +523 -0
- watcherobot-0.1.1/tests/__init__.py +1 -0
- watcherobot-0.1.1/tests/application/__init__.py +1 -0
- watcherobot-0.1.1/tests/application/test_channels.py +25 -0
- watcherobot-0.1.1/tests/application/test_context.py +124 -0
- watcherobot-0.1.1/tests/application/test_project_init.py +171 -0
- watcherobot-0.1.1/tests/application/test_rtc.py +452 -0
- watcherobot-0.1.1/tests/application/test_transport.py +405 -0
- watcherobot-0.1.1/tests/distribution/_publishing_fakes.py +218 -0
- watcherobot-0.1.1/tests/distribution/test_auth_cli.py +247 -0
- watcherobot-0.1.1/tests/distribution/test_catalog_submission.py +185 -0
- watcherobot-0.1.1/tests/distribution/test_check.py +196 -0
- watcherobot-0.1.1/tests/distribution/test_credentials.py +87 -0
- watcherobot-0.1.1/tests/distribution/test_daemon_boundary.py +26 -0
- watcherobot-0.1.1/tests/distribution/test_distribution_docs.py +144 -0
- watcherobot-0.1.1/tests/distribution/test_distribution_entrypoint.py +48 -0
- watcherobot-0.1.1/tests/distribution/test_download.py +337 -0
- watcherobot-0.1.1/tests/distribution/test_download_cli.py +244 -0
- watcherobot-0.1.1/tests/distribution/test_events.py +110 -0
- watcherobot-0.1.1/tests/distribution/test_hf_marketplace.py +377 -0
- watcherobot-0.1.1/tests/distribution/test_hf_publish.py +389 -0
- watcherobot-0.1.1/tests/distribution/test_hub_http.py +117 -0
- watcherobot-0.1.1/tests/distribution/test_install.py +457 -0
- watcherobot-0.1.1/tests/distribution/test_install_cli.py +213 -0
- watcherobot-0.1.1/tests/distribution/test_login.py +499 -0
- watcherobot-0.1.1/tests/distribution/test_marketplace.py +262 -0
- watcherobot-0.1.1/tests/distribution/test_marketplace_cli.py +244 -0
- watcherobot-0.1.1/tests/distribution/test_oauth_http.py +224 -0
- watcherobot-0.1.1/tests/distribution/test_ports.py +117 -0
- watcherobot-0.1.1/tests/distribution/test_publish.py +148 -0
- watcherobot-0.1.1/tests/distribution/test_publish_cli.py +241 -0
- watcherobot-0.1.1/tests/distribution/test_publish_files.py +111 -0
- watcherobot-0.1.1/tests/distribution/test_publish_ports.py +189 -0
- watcherobot-0.1.1/tests/distribution/test_source_files.py +89 -0
- watcherobot-0.1.1/tests/distribution/test_submit.py +266 -0
- watcherobot-0.1.1/tests/distribution/test_submit_cli.py +154 -0
- watcherobot-0.1.1/tests/fixtures/contracts/watcher_lan_pairing_v1.json +121 -0
- watcherobot-0.1.1/tests/fixtures/contracts/wspk_v1.json +30 -0
- watcherobot-0.1.1/tests/js/test_animation_confirmation.mjs +40 -0
- watcherobot-0.1.1/tests/js/test_animation_random.mjs +37 -0
- watcherobot-0.1.1/tests/js/test_media_resource_policy.mjs +202 -0
- watcherobot-0.1.1/tests/js/test_mjpeg_chunk_reassembly.mjs +89 -0
- watcherobot-0.1.1/tests/js/test_resource_health.mjs +218 -0
- watcherobot-0.1.1/tests/js/test_rtc_audio_capture.mjs +23 -0
- watcherobot-0.1.1/tests/js/test_rtc_audio_health.mjs +101 -0
- watcherobot-0.1.1/tests/js/test_rtc_audio_latency.mjs +78 -0
- watcherobot-0.1.1/tests/js/test_sdk_media_lab_i18n.mjs +35 -0
- watcherobot-0.1.1/tests/js/test_video_feedback.mjs +82 -0
- watcherobot-0.1.1/tests/js/test_video_frame_queue.mjs +39 -0
- watcherobot-0.1.1/tests/provisioning/__init__.py +1 -0
- watcherobot-0.1.1/tests/provisioning/test_bleak_backend.py +316 -0
- watcherobot-0.1.1/tests/provisioning/test_cli.py +242 -0
- watcherobot-0.1.1/tests/provisioning/test_hardware_tool.py +83 -0
- watcherobot-0.1.1/tests/provisioning/test_protocol.py +179 -0
- watcherobot-0.1.1/tests/provisioning/test_service.py +571 -0
- watcherobot-0.1.1/tests/runtime/__init__.py +1 -0
- watcherobot-0.1.1/tests/runtime/pairing_helpers.py +71 -0
- watcherobot-0.1.1/tests/runtime/test_application_bridge.py +123 -0
- watcherobot-0.1.1/tests/runtime/test_application_client.py +173 -0
- watcherobot-0.1.1/tests/runtime/test_application_launcher.py +727 -0
- watcherobot-0.1.1/tests/runtime/test_application_logging.py +216 -0
- watcherobot-0.1.1/tests/runtime/test_application_manifest.py +205 -0
- watcherobot-0.1.1/tests/runtime/test_application_manifest_metadata.py +72 -0
- watcherobot-0.1.1/tests/runtime/test_application_runtime.py +656 -0
- watcherobot-0.1.1/tests/runtime/test_application_session.py +88 -0
- watcherobot-0.1.1/tests/runtime/test_connection_identity.py +75 -0
- watcherobot-0.1.1/tests/runtime/test_control_rest.py +655 -0
- watcherobot-0.1.1/tests/runtime/test_daemon_entrypoint.py +106 -0
- watcherobot-0.1.1/tests/runtime/test_daemon_logging.py +38 -0
- watcherobot-0.1.1/tests/runtime/test_daemon_runtime_routing.py +633 -0
- watcherobot-0.1.1/tests/runtime/test_external_websocket_routing.py +273 -0
- watcherobot-0.1.1/tests/runtime/test_face_tracking_preview_broker.py +181 -0
- watcherobot-0.1.1/tests/runtime/test_face_tracking_udp_feedback.py +44 -0
- watcherobot-0.1.1/tests/runtime/test_face_tracking_udp_protocol.py +143 -0
- watcherobot-0.1.1/tests/runtime/test_face_tracking_udp_service.py +164 -0
- watcherobot-0.1.1/tests/runtime/test_frozen_entry.py +27 -0
- watcherobot-0.1.1/tests/runtime/test_full_architecture_flow.py +235 -0
- watcherobot-0.1.1/tests/runtime/test_lan_pairing_protocol.py +200 -0
- watcherobot-0.1.1/tests/runtime/test_maintenance_extensions.py +906 -0
- watcherobot-0.1.1/tests/runtime/test_maintenance_service.py +567 -0
- watcherobot-0.1.1/tests/runtime/test_pairing_session.py +311 -0
- watcherobot-0.1.1/tests/runtime/test_pairing_udp.py +328 -0
- watcherobot-0.1.1/tests/runtime/test_pairing_udp_interfaces.py +468 -0
- watcherobot-0.1.1/tests/runtime/test_pairing_websocket_gate.py +193 -0
- watcherobot-0.1.1/tests/runtime/test_runtime_instance.py +45 -0
- watcherobot-0.1.1/tests/runtime/test_sd_resource_contract_end_to_end.py +168 -0
- watcherobot-0.1.1/tests/runtime/test_standalone_application_routing_contract.py +97 -0
- watcherobot-0.1.1/tests/test_api.py +827 -0
- watcherobot-0.1.1/tests/test_app_cli_usability.py +296 -0
- watcherobot-0.1.1/tests/test_audio.py +41 -0
- watcherobot-0.1.1/tests/test_cli_runtime.py +390 -0
- watcherobot-0.1.1/tests/test_contract_vectors.py +53 -0
- watcherobot-0.1.1/tests/test_documentation.py +149 -0
- watcherobot-0.1.1/tests/test_examples.py +147 -0
- watcherobot-0.1.1/tests/test_face_tracking.py +232 -0
- watcherobot-0.1.1/tests/test_inputs.py +165 -0
- watcherobot-0.1.1/tests/test_job.py +61 -0
- watcherobot-0.1.1/tests/test_media.py +219 -0
- watcherobot-0.1.1/tests/test_opus.py +31 -0
- watcherobot-0.1.1/tests/test_protocol.py +71 -0
- watcherobot-0.1.1/tests/test_release.py +210 -0
- watcherobot-0.1.1/tests/test_removed_legacy_api.py +12 -0
- watcherobot-0.1.1/tests/test_robot_onboarding.py +413 -0
- watcherobot-0.1.1/tests/test_sdk_media_lab.py +1584 -0
- watcherobot-0.1.1/tests/tools/test_check_release_availability.py +88 -0
- watcherobot-0.1.1/tests/tools/test_check_release_gate.py +126 -0
- watcherobot-0.1.1/tests/tools/test_check_release_version.py +78 -0
- watcherobot-0.1.1/tests/tools/test_prepare_release.py +86 -0
- watcherobot-0.1.1/tools/ble_provisioning_hardware_test.py +147 -0
- watcherobot-0.1.1/tools/check_release_availability.py +85 -0
- watcherobot-0.1.1/tools/check_release_gate.py +195 -0
- watcherobot-0.1.1/tools/check_release_version.py +101 -0
- watcherobot-0.1.1/tools/prepare_release.py +91 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Bridge one GitHub review job to the Luxiao Hermes profile safely."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import uuid
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
MAX_DIFF_CHARS = 100_000
|
|
16
|
+
REMOTE_REVIEW_TIMEOUT_SECONDS = 600
|
|
17
|
+
SSH_OPTIONS = ("-o", "StrictHostKeyChecking=yes", "-o", "BatchMode=yes")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _write_result(path: Path, text: str) -> None:
|
|
21
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
path.write_text(text, encoding="utf-8")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> int:
|
|
26
|
+
if len(sys.argv) != 3:
|
|
27
|
+
print("Usage: luxiao_review.py <diff_path> <output_path>", file=sys.stderr)
|
|
28
|
+
return 2
|
|
29
|
+
|
|
30
|
+
diff_path = Path(sys.argv[1])
|
|
31
|
+
output_path = Path(sys.argv[2])
|
|
32
|
+
if not diff_path.is_file():
|
|
33
|
+
_write_result(output_path, "## 🤖 Luxiao PR 审查报告\n\n⚠️ Diff 文件不存在。")
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
hermes_host = os.environ.get("LUXIAO_HERMES_HOST", "").strip()
|
|
37
|
+
remote_script = os.environ.get(
|
|
38
|
+
"LUXIAO_REMOTE_SCRIPT", "/home/hermesadmin/scripts/luxiao-run.sh"
|
|
39
|
+
).strip()
|
|
40
|
+
remote_dir = os.environ.get(
|
|
41
|
+
"LUXIAO_REMOTE_DIR", "/home/hermesadmin/.cache/luxiao-review"
|
|
42
|
+
).strip()
|
|
43
|
+
if not hermes_host:
|
|
44
|
+
_write_result(
|
|
45
|
+
output_path,
|
|
46
|
+
"## 🤖 Luxiao PR 审查报告\n\n⚠️ Runner 未配置 LUXIAO_HERMES_HOST。",
|
|
47
|
+
)
|
|
48
|
+
return 1
|
|
49
|
+
|
|
50
|
+
full_diff = diff_path.read_text(encoding="utf-8", errors="replace")
|
|
51
|
+
diff_text = full_diff[:MAX_DIFF_CHARS]
|
|
52
|
+
truncation_notice = ""
|
|
53
|
+
if len(full_diff) > MAX_DIFF_CHARS:
|
|
54
|
+
omitted_chars = len(full_diff) - MAX_DIFF_CHARS
|
|
55
|
+
omitted_lines = full_diff[MAX_DIFF_CHARS:].count("\n")
|
|
56
|
+
truncation_notice = (
|
|
57
|
+
"\n\n> ⚠️ Diff 过大,本次输入已明确截断:"
|
|
58
|
+
f"省略 {omitted_chars} 个字符、约 {omitted_lines} 行。"
|
|
59
|
+
"审查结论必须注明未覆盖范围,不能宣称完成全量审查。"
|
|
60
|
+
)
|
|
61
|
+
if not diff_text.strip() or diff_text.strip() == "empty":
|
|
62
|
+
_write_result(output_path, "## 🤖 Luxiao PR 审查报告\n\n✅ 无代码变更。")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
prompt = f"""请审查以下 Pull Request。按照你的审查框架(架构、产品、规范、损伤 + 意图分析 + Merge 建议)给出完整审查报告。
|
|
66
|
+
|
|
67
|
+
## PR 信息
|
|
68
|
+
|
|
69
|
+
- 标题: {os.environ.get('PR_TITLE', '')}
|
|
70
|
+
- 描述: {os.environ.get('PR_BODY', '')}
|
|
71
|
+
- 变更: {os.environ.get('PR_FILES', '')} 个文件, +{os.environ.get('PR_ADDITIONS', '')} / -{os.environ.get('PR_DELETIONS', '')}
|
|
72
|
+
|
|
73
|
+
## 代码 Diff
|
|
74
|
+
|
|
75
|
+
{diff_text}{truncation_notice}
|
|
76
|
+
|
|
77
|
+
请直接输出审查报告,不要多余的前缀。"""
|
|
78
|
+
|
|
79
|
+
runner_temp = Path(os.environ.get("RUNNER_TEMP", tempfile.gettempdir()))
|
|
80
|
+
runner_temp.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
remote_file = f"{remote_dir}/{uuid.uuid4().hex}.txt"
|
|
82
|
+
with tempfile.NamedTemporaryFile(
|
|
83
|
+
mode="w", encoding="utf-8", prefix="luxiao-prompt-", suffix=".txt", dir=runner_temp, delete=False
|
|
84
|
+
) as prompt_file:
|
|
85
|
+
prompt_file.write(prompt)
|
|
86
|
+
local_file = Path(prompt_file.name)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
subprocess.run(
|
|
90
|
+
["ssh", *SSH_OPTIONS, hermes_host, "mkdir", "-p", remote_dir],
|
|
91
|
+
check=True,
|
|
92
|
+
capture_output=True,
|
|
93
|
+
text=True,
|
|
94
|
+
timeout=30,
|
|
95
|
+
)
|
|
96
|
+
subprocess.run(
|
|
97
|
+
["scp", *SSH_OPTIONS, str(local_file), f"{hermes_host}:{remote_file}"],
|
|
98
|
+
check=True,
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
timeout=30,
|
|
102
|
+
)
|
|
103
|
+
result = subprocess.run(
|
|
104
|
+
[
|
|
105
|
+
"ssh",
|
|
106
|
+
*SSH_OPTIONS,
|
|
107
|
+
hermes_host,
|
|
108
|
+
"timeout",
|
|
109
|
+
"--signal=TERM",
|
|
110
|
+
"--kill-after=30s",
|
|
111
|
+
f"{REMOTE_REVIEW_TIMEOUT_SECONDS}s",
|
|
112
|
+
shlex.quote(remote_script),
|
|
113
|
+
shlex.quote(remote_file),
|
|
114
|
+
],
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True,
|
|
117
|
+
timeout=REMOTE_REVIEW_TIMEOUT_SECONDS + 60,
|
|
118
|
+
)
|
|
119
|
+
finally:
|
|
120
|
+
local_file.unlink(missing_ok=True)
|
|
121
|
+
subprocess.run(
|
|
122
|
+
["ssh", *SSH_OPTIONS, hermes_host, "rm", "-f", "--", remote_file],
|
|
123
|
+
capture_output=True,
|
|
124
|
+
text=True,
|
|
125
|
+
timeout=30,
|
|
126
|
+
check=False,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
130
|
+
_write_result(
|
|
131
|
+
output_path,
|
|
132
|
+
"## 🤖 Luxiao PR 审查报告\n\n"
|
|
133
|
+
f"⚠️ Luxiao Agent 调用失败(退出码 {result.returncode})。",
|
|
134
|
+
)
|
|
135
|
+
return 1
|
|
136
|
+
|
|
137
|
+
text = result.stdout
|
|
138
|
+
markers = ("🤖 PR 审查报告", "PR 审查报告", "## PR 审查", "## 审查报告")
|
|
139
|
+
for marker in markers:
|
|
140
|
+
if marker in text:
|
|
141
|
+
text = text[text.index(marker) :]
|
|
142
|
+
break
|
|
143
|
+
else:
|
|
144
|
+
text = text[-6000:]
|
|
145
|
+
_write_result(output_path, "## 🤖 Luxiao PR 审查报告\n\n" + text)
|
|
146
|
+
print(f"Review saved to {output_path}")
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: PR Review by Luxiao
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize, reopened, ready_for_review]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
pull-requests: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
review:
|
|
13
|
+
runs-on: [self-hosted, Linux, X64, sdk-ci, pr-review]
|
|
14
|
+
if: github.event.pull_request.draft == false
|
|
15
|
+
steps:
|
|
16
|
+
- name: Get PR Diff via API
|
|
17
|
+
env:
|
|
18
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
19
|
+
run: |
|
|
20
|
+
diff_file="${RUNNER_TEMP}/pr-${{ github.event.pull_request.number }}.diff"
|
|
21
|
+
gh pr diff ${{ github.event.pull_request.number }} -R ${{ github.repository }} > "${diff_file}" 2>/dev/null
|
|
22
|
+
if [ ! -s "${diff_file}" ]; then echo "empty" > "${diff_file}"; fi
|
|
23
|
+
echo "DIFF_FILE=${diff_file}" >> "${GITHUB_ENV}"
|
|
24
|
+
echo "Diff lines: $(wc -l < "${diff_file}")"
|
|
25
|
+
|
|
26
|
+
- name: Run Luxiao Review
|
|
27
|
+
env:
|
|
28
|
+
LUXIAO_HERMES_HOST: ${{ vars.LUXIAO_HERMES_HOST }}
|
|
29
|
+
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
30
|
+
PR_BODY: ${{ github.event.pull_request.body }}
|
|
31
|
+
PR_ADDITIONS: ${{ github.event.pull_request.additions }}
|
|
32
|
+
PR_DELETIONS: ${{ github.event.pull_request.deletions }}
|
|
33
|
+
PR_FILES: ${{ github.event.pull_request.changed_files }}
|
|
34
|
+
run: |
|
|
35
|
+
python3 /home/runner-ci/scripts/luxiao-review.py \
|
|
36
|
+
"${DIFF_FILE}" \
|
|
37
|
+
"${RUNNER_TEMP}/review_result.md"
|
|
38
|
+
|
|
39
|
+
- name: Post Review Comment
|
|
40
|
+
if: always()
|
|
41
|
+
env:
|
|
42
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
43
|
+
run: |
|
|
44
|
+
review_file="${RUNNER_TEMP}/review_result.md"
|
|
45
|
+
if [ ! -s "${review_file}" ]; then
|
|
46
|
+
echo "## 🤖 Luxiao PR 审查报告" > "${review_file}"
|
|
47
|
+
echo "" >> "${review_file}"
|
|
48
|
+
echo "⚠️ Review agent did not produce output." >> "${review_file}"
|
|
49
|
+
fi
|
|
50
|
+
gh pr comment ${{ github.event.pull_request.number }} \
|
|
51
|
+
-R ${{ github.repository }} \
|
|
52
|
+
--body "$(cat "${review_file}")"
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
name: Prepare SDK release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [closed]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
target_version:
|
|
9
|
+
description: "显式 PEP 440 版本;飞书指令使用此字段"
|
|
10
|
+
required: false
|
|
11
|
+
type: string
|
|
12
|
+
release_type:
|
|
13
|
+
description: "未指定显式版本时使用的版本增量"
|
|
14
|
+
required: false
|
|
15
|
+
type: choice
|
|
16
|
+
options: [none, prerelease, stable, minor, major]
|
|
17
|
+
default: none
|
|
18
|
+
source:
|
|
19
|
+
description: "发布请求来源"
|
|
20
|
+
required: true
|
|
21
|
+
type: string
|
|
22
|
+
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
prepare:
|
|
28
|
+
if: >-
|
|
29
|
+
github.event_name == 'workflow_dispatch' ||
|
|
30
|
+
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main')
|
|
31
|
+
# Trusted orchestration never shares an execution identity with pull-request code.
|
|
32
|
+
runs-on: [self-hosted, Linux, X64, sdk-orchestrator]
|
|
33
|
+
steps:
|
|
34
|
+
- name: Resolve one explicit release request
|
|
35
|
+
id: release
|
|
36
|
+
env:
|
|
37
|
+
EVENT_NAME: ${{ github.event_name }}
|
|
38
|
+
LABELS_JSON: ${{ toJSON(github.event.pull_request.labels.*.name) }}
|
|
39
|
+
INPUT_TARGET: ${{ inputs.target_version }}
|
|
40
|
+
INPUT_TYPE: ${{ inputs.release_type }}
|
|
41
|
+
INPUT_SOURCE: ${{ inputs.source }}
|
|
42
|
+
SOURCE_PR: ${{ github.event.pull_request.number }}
|
|
43
|
+
SOURCE_TITLE: ${{ github.event.pull_request.title }}
|
|
44
|
+
shell: bash
|
|
45
|
+
run: |
|
|
46
|
+
python - <<'PY' >> "${GITHUB_OUTPUT}"
|
|
47
|
+
import json
|
|
48
|
+
import os
|
|
49
|
+
|
|
50
|
+
if os.environ["EVENT_NAME"] == "workflow_dispatch":
|
|
51
|
+
target = os.environ.get("INPUT_TARGET", "").strip()
|
|
52
|
+
release_type = os.environ.get("INPUT_TYPE", "").strip()
|
|
53
|
+
if release_type == "none":
|
|
54
|
+
release_type = ""
|
|
55
|
+
if bool(target) == bool(release_type):
|
|
56
|
+
raise SystemExit("Exactly one of target_version or release_type is required")
|
|
57
|
+
print(f"target={target}")
|
|
58
|
+
print(f"release_type={release_type}")
|
|
59
|
+
print(f"source={os.environ['INPUT_SOURCE'].strip()}")
|
|
60
|
+
print("skip=false")
|
|
61
|
+
else:
|
|
62
|
+
mapping = {
|
|
63
|
+
"release:prerelease": "prerelease",
|
|
64
|
+
"release:stable": "stable",
|
|
65
|
+
"release:minor": "minor",
|
|
66
|
+
"release:major": "major",
|
|
67
|
+
}
|
|
68
|
+
labels = json.loads(os.environ.get("LABELS_JSON") or "[]")
|
|
69
|
+
selected = [mapping[label] for label in labels if label in mapping]
|
|
70
|
+
if len(selected) > 1:
|
|
71
|
+
raise SystemExit("A merged PR may have only one release:* label")
|
|
72
|
+
if not selected:
|
|
73
|
+
print("skip=true")
|
|
74
|
+
else:
|
|
75
|
+
print("target=")
|
|
76
|
+
print(f"release_type={selected[0]}")
|
|
77
|
+
print(
|
|
78
|
+
"source=PR #{}:{}".format(
|
|
79
|
+
os.environ["SOURCE_PR"], os.environ["SOURCE_TITLE"].strip()
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
print("skip=false")
|
|
83
|
+
PY
|
|
84
|
+
|
|
85
|
+
- name: Mint repository-scoped GitHub App token
|
|
86
|
+
if: steps.release.outputs.skip == 'false'
|
|
87
|
+
id: app-token
|
|
88
|
+
uses: actions/create-github-app-token@v2
|
|
89
|
+
with:
|
|
90
|
+
app-id: ${{ secrets.ORULINK_RELEASE_APP_ID }}
|
|
91
|
+
private-key: ${{ secrets.ORULINK_RELEASE_APP_PRIVATE_KEY }}
|
|
92
|
+
owner: ${{ github.repository_owner }}
|
|
93
|
+
repositories: WatcheRobot_python_sdk
|
|
94
|
+
|
|
95
|
+
- name: Check out main
|
|
96
|
+
if: steps.release.outputs.skip == 'false'
|
|
97
|
+
uses: actions/checkout@v6
|
|
98
|
+
with:
|
|
99
|
+
ref: main
|
|
100
|
+
fetch-depth: 0
|
|
101
|
+
persist-credentials: true
|
|
102
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
103
|
+
|
|
104
|
+
- uses: actions/setup-python@v6
|
|
105
|
+
if: steps.release.outputs.skip == 'false'
|
|
106
|
+
with:
|
|
107
|
+
python-version: "3.12"
|
|
108
|
+
|
|
109
|
+
- name: Prepare version branch and pull request
|
|
110
|
+
if: steps.release.outputs.skip == 'false'
|
|
111
|
+
env:
|
|
112
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
113
|
+
RELEASE_TYPE: ${{ steps.release.outputs.release_type }}
|
|
114
|
+
TARGET_VERSION: ${{ steps.release.outputs.target }}
|
|
115
|
+
RELEASE_SOURCE: ${{ steps.release.outputs.source }}
|
|
116
|
+
shell: bash
|
|
117
|
+
run: |
|
|
118
|
+
# `packaging` is part of the trusted Python tool cache provisioned on the
|
|
119
|
+
# orchestrator runner. Release preparation must not depend on a mutable
|
|
120
|
+
# package-index download before it can even validate the requested version.
|
|
121
|
+
python -c "from packaging.version import Version; Version('0.1.1a1')"
|
|
122
|
+
arguments=(--source "${RELEASE_SOURCE}")
|
|
123
|
+
if [[ -n "${TARGET_VERSION}" ]]; then
|
|
124
|
+
arguments+=(--target "${TARGET_VERSION}")
|
|
125
|
+
else
|
|
126
|
+
arguments+=(--release-type "${RELEASE_TYPE}")
|
|
127
|
+
fi
|
|
128
|
+
version=$(python tools/prepare_release.py "${arguments[@]}")
|
|
129
|
+
python tools/check_release_availability.py "${version}" --repository "${GITHUB_REPOSITORY}"
|
|
130
|
+
branch="release/watcherobot-${version}"
|
|
131
|
+
existing=$(gh pr list --repo "${GITHUB_REPOSITORY}" --head "${branch}" --state open --json number --jq 'length')
|
|
132
|
+
if [[ "${existing}" != "0" ]]; then
|
|
133
|
+
echo "Release PR already exists for ${version}; nothing to do."
|
|
134
|
+
exit 0
|
|
135
|
+
fi
|
|
136
|
+
git switch -c "${branch}"
|
|
137
|
+
git config user.name "orulink-release-bot"
|
|
138
|
+
git config user.email "release-bot@users.noreply.github.com"
|
|
139
|
+
git add src/watcherobot/__init__.py CHANGELOG.md
|
|
140
|
+
git commit -m "chore(release): 准备发布 watcherobot ${version}" \
|
|
141
|
+
-m "由 ${RELEASE_SOURCE} 触发。此提交仅更新 SDK 唯一版本源和中文更新日志,正式发布仍需版本 PR 审查、标签门禁、TestPyPI 验证与 GitHub Environment 人工批准。"
|
|
142
|
+
git push origin "${branch}"
|
|
143
|
+
body=$(cat <<EOF
|
|
144
|
+
## 发布来源
|
|
145
|
+
|
|
146
|
+
- 来源:${RELEASE_SOURCE}
|
|
147
|
+
- 发布类型:${RELEASE_TYPE:-显式版本}
|
|
148
|
+
- 目标版本:${version}
|
|
149
|
+
|
|
150
|
+
## 门禁
|
|
151
|
+
|
|
152
|
+
- 本 PR 必须人工审查和合并
|
|
153
|
+
- 合并后由陆骁创建 annotated tag
|
|
154
|
+
- Tag 构建先发布并验证 TestPyPI
|
|
155
|
+
- 正式 PyPI 发布需要 GitHub Environment 人工批准
|
|
156
|
+
|
|
157
|
+
## 已知风险与实机状态
|
|
158
|
+
|
|
159
|
+
- 本 PR 只准备版本元数据,不代表已完成实机验收
|
|
160
|
+
- 稳定版发布前必须补充 RTC 实机验证结果
|
|
161
|
+
EOF
|
|
162
|
+
)
|
|
163
|
+
gh pr create \
|
|
164
|
+
--repo "${GITHUB_REPOSITORY}" \
|
|
165
|
+
--base main \
|
|
166
|
+
--head "${branch}" \
|
|
167
|
+
--title "chore(release): 发布 watcherobot ${version}" \
|
|
168
|
+
--label "release:version" \
|
|
169
|
+
--body "${body}"
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
name: Release Python package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: sdk-release-${{ github.ref }}
|
|
9
|
+
cancel-in-progress: false
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
start-clean:
|
|
16
|
+
name: Clean release workspace before use
|
|
17
|
+
runs-on: [self-hosted, Linux, X64, sdk-release]
|
|
18
|
+
steps:
|
|
19
|
+
- name: Remove files left by a previous release job
|
|
20
|
+
shell: bash
|
|
21
|
+
run: |
|
|
22
|
+
workspace=$(realpath -m "${GITHUB_WORKSPACE}")
|
|
23
|
+
[[ "${workspace}" == /opt/actions-runner-release/_work/* ]] || {
|
|
24
|
+
echo "Refusing to clean unexpected workspace: ${workspace}" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
}
|
|
27
|
+
shopt -s dotglob nullglob
|
|
28
|
+
rm -rf -- "${workspace}"/*
|
|
29
|
+
|
|
30
|
+
gate:
|
|
31
|
+
name: Validate release request
|
|
32
|
+
needs: start-clean
|
|
33
|
+
runs-on: [self-hosted, Linux, X64, sdk-release]
|
|
34
|
+
outputs:
|
|
35
|
+
version: ${{ steps.gate.outputs.version }}
|
|
36
|
+
prerelease: ${{ steps.gate.outputs.prerelease }}
|
|
37
|
+
commit: ${{ steps.gate.outputs.commit }}
|
|
38
|
+
reuse-artifact: ${{ steps.gate.outputs.reuse-artifact }}
|
|
39
|
+
permissions:
|
|
40
|
+
contents: read
|
|
41
|
+
pull-requests: read
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v6
|
|
44
|
+
with:
|
|
45
|
+
fetch-depth: 0
|
|
46
|
+
persist-credentials: false
|
|
47
|
+
- uses: actions/setup-python@v6
|
|
48
|
+
with:
|
|
49
|
+
python-version: "3.12"
|
|
50
|
+
cache: pip
|
|
51
|
+
- run: python -m pip install --upgrade packaging
|
|
52
|
+
- id: gate
|
|
53
|
+
name: Validate tag, main ancestry, version PR and registries
|
|
54
|
+
env:
|
|
55
|
+
GH_TOKEN: ${{ github.token }}
|
|
56
|
+
run: |
|
|
57
|
+
tag_commit=$(git rev-list -n 1 "${GITHUB_REF_NAME}")
|
|
58
|
+
gate_json=$(python tools/check_release_gate.py \
|
|
59
|
+
--repository "${GITHUB_REPOSITORY}" \
|
|
60
|
+
--tag "${GITHUB_REF_NAME}" \
|
|
61
|
+
--sha "${tag_commit}")
|
|
62
|
+
version=$(python -c 'import json,sys; print(json.load(sys.stdin)["version"])' <<< "${gate_json}")
|
|
63
|
+
reuse_artifact=$(python -c 'import json,sys; print(str(json.load(sys.stdin)["reuse_artifact"]).lower())' <<< "${gate_json}")
|
|
64
|
+
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
|
65
|
+
echo "commit=${tag_commit}" >> "${GITHUB_OUTPUT}"
|
|
66
|
+
echo "reuse-artifact=${reuse_artifact}" >> "${GITHUB_OUTPUT}"
|
|
67
|
+
python tools/check_release_version.py "${GITHUB_REF_NAME}"
|
|
68
|
+
python -c 'from packaging.version import Version; import sys; print(f"prerelease={str(Version(sys.argv[1]).is_prerelease).lower()}")' "${version}" >> "${GITHUB_OUTPUT}"
|
|
69
|
+
|
|
70
|
+
build:
|
|
71
|
+
name: Build immutable distributions
|
|
72
|
+
needs: gate
|
|
73
|
+
runs-on: [self-hosted, Linux, X64, sdk-release]
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/checkout@v6
|
|
76
|
+
if: needs.gate.outputs.reuse-artifact != 'true'
|
|
77
|
+
with:
|
|
78
|
+
fetch-depth: 0
|
|
79
|
+
persist-credentials: false
|
|
80
|
+
- uses: actions/setup-python@v6
|
|
81
|
+
if: needs.gate.outputs.reuse-artifact != 'true'
|
|
82
|
+
with:
|
|
83
|
+
python-version: "3.12"
|
|
84
|
+
cache: pip
|
|
85
|
+
- if: needs.gate.outputs.reuse-artifact != 'true'
|
|
86
|
+
run: python -m pip install --upgrade pip build twine
|
|
87
|
+
- if: needs.gate.outputs.reuse-artifact != 'true'
|
|
88
|
+
run: python -m pip install -e ".[test]"
|
|
89
|
+
- if: needs.gate.outputs.reuse-artifact != 'true'
|
|
90
|
+
run: python -m pytest
|
|
91
|
+
- if: needs.gate.outputs.reuse-artifact != 'true'
|
|
92
|
+
run: python -m mypy src/watcherobot
|
|
93
|
+
- if: needs.gate.outputs.reuse-artifact != 'true'
|
|
94
|
+
run: python -m build
|
|
95
|
+
- if: needs.gate.outputs.reuse-artifact != 'true'
|
|
96
|
+
run: python -m twine check dist/*
|
|
97
|
+
- name: Verify wheel installation
|
|
98
|
+
if: needs.gate.outputs.reuse-artifact != 'true'
|
|
99
|
+
run: |
|
|
100
|
+
python -m venv .venv-wheel-check
|
|
101
|
+
.venv-wheel-check/bin/python -m pip install --force-reinstall dist/*.whl
|
|
102
|
+
.venv-wheel-check/bin/python -m pip check
|
|
103
|
+
.venv-wheel-check/bin/python -c "import watcherobot; assert watcherobot.__version__ == '${{ needs.gate.outputs.version }}'"
|
|
104
|
+
.venv-wheel-check/bin/watcherobot --help
|
|
105
|
+
- name: Record distribution hashes
|
|
106
|
+
if: needs.gate.outputs.reuse-artifact != 'true'
|
|
107
|
+
run: sha256sum dist/* > SHA256SUMS
|
|
108
|
+
- name: Recover immutable distributions from the matching draft Release
|
|
109
|
+
if: needs.gate.outputs.reuse-artifact == 'true'
|
|
110
|
+
env:
|
|
111
|
+
GH_TOKEN: ${{ github.token }}
|
|
112
|
+
run: |
|
|
113
|
+
mkdir -p dist
|
|
114
|
+
gh release download "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" --pattern 'watcherobot-*' --dir dist
|
|
115
|
+
gh release download "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" --pattern SHA256SUMS --dir .
|
|
116
|
+
sha256sum --check SHA256SUMS
|
|
117
|
+
- uses: actions/upload-artifact@v7
|
|
118
|
+
with:
|
|
119
|
+
name: watcherobot-${{ needs.gate.outputs.version }}-${{ github.run_attempt }}
|
|
120
|
+
path: |
|
|
121
|
+
dist/
|
|
122
|
+
SHA256SUMS
|
|
123
|
+
if-no-files-found: error
|
|
124
|
+
retention-days: 30
|
|
125
|
+
|
|
126
|
+
draft-release:
|
|
127
|
+
name: Create draft GitHub Release
|
|
128
|
+
needs: [gate, build]
|
|
129
|
+
runs-on: [self-hosted, Linux, X64, sdk-release]
|
|
130
|
+
permissions:
|
|
131
|
+
contents: write
|
|
132
|
+
steps:
|
|
133
|
+
- uses: actions/download-artifact@v8
|
|
134
|
+
with:
|
|
135
|
+
name: watcherobot-${{ needs.gate.outputs.version }}-${{ github.run_attempt }}
|
|
136
|
+
path: artifact/
|
|
137
|
+
- name: Create immutable draft Release
|
|
138
|
+
env:
|
|
139
|
+
GH_TOKEN: ${{ github.token }}
|
|
140
|
+
run: |
|
|
141
|
+
prerelease_flag=()
|
|
142
|
+
if [[ "${{ needs.gate.outputs.prerelease }}" == "true" ]]; then prerelease_flag=(--prerelease); fi
|
|
143
|
+
if gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
|
|
144
|
+
echo "Matching draft Release already exists; reusing it."
|
|
145
|
+
else
|
|
146
|
+
gh release create "${GITHUB_REF_NAME}" artifact/dist/* artifact/SHA256SUMS \
|
|
147
|
+
--repo "${GITHUB_REPOSITORY}" \
|
|
148
|
+
--target "${{ needs.gate.outputs.commit }}" \
|
|
149
|
+
--title "watcherobot ${{ needs.gate.outputs.version }}" \
|
|
150
|
+
--generate-notes \
|
|
151
|
+
--draft \
|
|
152
|
+
"${prerelease_flag[@]}"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
publish-testpypi:
|
|
156
|
+
name: Publish to TestPyPI
|
|
157
|
+
needs: [gate, build, draft-release]
|
|
158
|
+
runs-on: [self-hosted, Linux, X64, sdk-release]
|
|
159
|
+
environment: testpypi
|
|
160
|
+
permissions:
|
|
161
|
+
id-token: write
|
|
162
|
+
steps:
|
|
163
|
+
- uses: actions/download-artifact@v8
|
|
164
|
+
with:
|
|
165
|
+
name: watcherobot-${{ needs.gate.outputs.version }}-${{ github.run_attempt }}
|
|
166
|
+
path: artifact/
|
|
167
|
+
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
168
|
+
- name: Publish immutable distributions to TestPyPI with OIDC
|
|
169
|
+
env:
|
|
170
|
+
UV_PUBLISH_CHECK_URL: https://test.pypi.org/simple/
|
|
171
|
+
run: >-
|
|
172
|
+
uv publish
|
|
173
|
+
--trusted-publishing always
|
|
174
|
+
--publish-url https://test.pypi.org/legacy/
|
|
175
|
+
artifact/dist/*
|
|
176
|
+
|
|
177
|
+
verify-testpypi:
|
|
178
|
+
name: Verify TestPyPI installation
|
|
179
|
+
needs: [gate, build, publish-testpypi]
|
|
180
|
+
runs-on: [self-hosted, Linux, X64, sdk-release]
|
|
181
|
+
steps:
|
|
182
|
+
- uses: actions/setup-python@v6
|
|
183
|
+
with:
|
|
184
|
+
python-version: "3.12"
|
|
185
|
+
- uses: actions/download-artifact@v8
|
|
186
|
+
with:
|
|
187
|
+
name: watcherobot-${{ needs.gate.outputs.version }}-${{ github.run_attempt }}
|
|
188
|
+
path: artifact/
|
|
189
|
+
- name: Install exact release from TestPyPI
|
|
190
|
+
run: |
|
|
191
|
+
python -m venv .venv-testpypi
|
|
192
|
+
.venv-testpypi/bin/python -m pip install --index-url https://pypi.org/simple/ artifact/dist/*.whl
|
|
193
|
+
.venv-testpypi/bin/python -m pip uninstall -y watcherobot
|
|
194
|
+
for attempt in {1..12}; do
|
|
195
|
+
if .venv-testpypi/bin/python -m pip download \
|
|
196
|
+
--index-url https://test.pypi.org/simple/ \
|
|
197
|
+
--no-deps --only-binary=:all: --dest testpypi-download \
|
|
198
|
+
"watcherobot==${{ needs.gate.outputs.version }}"; then
|
|
199
|
+
break
|
|
200
|
+
fi
|
|
201
|
+
if [[ "${attempt}" == "12" ]]; then exit 1; fi
|
|
202
|
+
sleep 10
|
|
203
|
+
done
|
|
204
|
+
expected_wheel=$(find artifact/dist -maxdepth 1 -name '*.whl' -printf '%f\n')
|
|
205
|
+
test "$(sha256sum "testpypi-download/${expected_wheel}" | cut -d' ' -f1)" = \
|
|
206
|
+
"$(sha256sum "artifact/dist/${expected_wheel}" | cut -d' ' -f1)"
|
|
207
|
+
.venv-testpypi/bin/python -m pip install --no-deps "testpypi-download/${expected_wheel}"
|
|
208
|
+
.venv-testpypi/bin/python -m pip check
|
|
209
|
+
.venv-testpypi/bin/python -c "import watcherobot; assert watcherobot.__version__ == '${{ needs.gate.outputs.version }}'"
|
|
210
|
+
.venv-testpypi/bin/watcherobot --help
|
|
211
|
+
|
|
212
|
+
publish-pypi:
|
|
213
|
+
name: Publish to PyPI
|
|
214
|
+
needs: [gate, build, verify-testpypi, draft-release]
|
|
215
|
+
runs-on: [self-hosted, Linux, X64, sdk-release]
|
|
216
|
+
environment:
|
|
217
|
+
name: pypi
|
|
218
|
+
url: https://pypi.org/p/watcherobot
|
|
219
|
+
permissions:
|
|
220
|
+
id-token: write
|
|
221
|
+
if: needs.gate.outputs.prerelease == 'false'
|
|
222
|
+
steps:
|
|
223
|
+
- uses: actions/download-artifact@v8
|
|
224
|
+
with:
|
|
225
|
+
name: watcherobot-${{ needs.gate.outputs.version }}-${{ github.run_attempt }}
|
|
226
|
+
path: artifact/
|
|
227
|
+
- name: Verify artifact hashes before production upload
|
|
228
|
+
run: (cd artifact && sha256sum --check SHA256SUMS)
|
|
229
|
+
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
230
|
+
- name: Publish the original distributions to PyPI with OIDC
|
|
231
|
+
run: uv publish --trusted-publishing always artifact/dist/*
|
|
232
|
+
|
|
233
|
+
verify-pypi:
|
|
234
|
+
name: Verify PyPI and publish GitHub Release
|
|
235
|
+
needs: [gate, publish-pypi]
|
|
236
|
+
runs-on: [self-hosted, Linux, X64, sdk-release]
|
|
237
|
+
permissions:
|
|
238
|
+
contents: write
|
|
239
|
+
if: needs.gate.outputs.prerelease == 'false'
|
|
240
|
+
steps:
|
|
241
|
+
- uses: actions/setup-python@v6
|
|
242
|
+
with:
|
|
243
|
+
python-version: "3.12"
|
|
244
|
+
- name: Install exact release from PyPI
|
|
245
|
+
run: |
|
|
246
|
+
python -m venv .venv-pypi
|
|
247
|
+
for attempt in {1..12}; do
|
|
248
|
+
if .venv-pypi/bin/python -m pip install "watcherobot==${{ needs.gate.outputs.version }}"; then break; fi
|
|
249
|
+
if [[ "${attempt}" == "12" ]]; then exit 1; fi
|
|
250
|
+
sleep 10
|
|
251
|
+
done
|
|
252
|
+
.venv-pypi/bin/python -m pip check
|
|
253
|
+
.venv-pypi/bin/python -c "import watcherobot; assert watcherobot.__version__ == '${{ needs.gate.outputs.version }}'"
|
|
254
|
+
.venv-pypi/bin/watcherobot --help
|
|
255
|
+
- name: Publish verified GitHub Release
|
|
256
|
+
env:
|
|
257
|
+
GH_TOKEN: ${{ github.token }}
|
|
258
|
+
run: gh release edit "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" --draft=false
|
|
259
|
+
|
|
260
|
+
finalize-prerelease:
|
|
261
|
+
name: Publish verified TestPyPI prerelease
|
|
262
|
+
needs: [gate, verify-testpypi]
|
|
263
|
+
if: needs.gate.outputs.prerelease == 'true'
|
|
264
|
+
runs-on: [self-hosted, Linux, X64, sdk-release]
|
|
265
|
+
permissions:
|
|
266
|
+
contents: write
|
|
267
|
+
steps:
|
|
268
|
+
- name: Publish GitHub prerelease after TestPyPI verification
|
|
269
|
+
env:
|
|
270
|
+
GH_TOKEN: ${{ github.token }}
|
|
271
|
+
run: gh release edit "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" --draft=false --prerelease
|
|
272
|
+
|
|
273
|
+
finish-clean:
|
|
274
|
+
name: Clean release workspace after use
|
|
275
|
+
if: always()
|
|
276
|
+
needs: [gate, build, draft-release, publish-testpypi, verify-testpypi, publish-pypi, verify-pypi, finalize-prerelease]
|
|
277
|
+
runs-on: [self-hosted, Linux, X64, sdk-release]
|
|
278
|
+
steps:
|
|
279
|
+
- name: Remove checked-out code, environments and downloaded artifacts
|
|
280
|
+
shell: bash
|
|
281
|
+
run: |
|
|
282
|
+
workspace=$(realpath -m "${GITHUB_WORKSPACE}")
|
|
283
|
+
[[ "${workspace}" == /opt/actions-runner-release/_work/* ]] || {
|
|
284
|
+
echo "Refusing to clean unexpected workspace: ${workspace}" >&2
|
|
285
|
+
exit 1
|
|
286
|
+
}
|
|
287
|
+
shopt -s dotglob nullglob
|
|
288
|
+
rm -rf -- "${workspace}"/*
|