secondshift 0.1.0b12__py3-none-any.whl
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.
- secondshift/__init__.py +1 -0
- secondshift/__main__.py +3 -0
- secondshift/cli.py +17 -0
- secondshift/config/__init__.py +0 -0
- secondshift/config/api.py +23 -0
- secondshift/config/app.py +62 -0
- secondshift/config/constants.py +279 -0
- secondshift/config/container_env.py +118 -0
- secondshift/config/first_run.py +40 -0
- secondshift/config/formatting.py +5 -0
- secondshift/config/gunicorn.py +31 -0
- secondshift/config/logging.py +40 -0
- secondshift/config/settings/__init__.py +0 -0
- secondshift/config/settings/base.py +524 -0
- secondshift/config/settings/dev.py +14 -0
- secondshift/config/settings/local.py +23 -0
- secondshift/config/settings/production.py +113 -0
- secondshift/config/settings/runner.py +140 -0
- secondshift/config/settings/test.py +47 -0
- secondshift/config/static_storage.py +45 -0
- secondshift/config/urls.py +42 -0
- secondshift/connections/__init__.py +0 -0
- secondshift/connections/access.py +27 -0
- secondshift/connections/admin.py +25 -0
- secondshift/connections/agents.py +224 -0
- secondshift/connections/api.py +747 -0
- secondshift/connections/apps.py +6 -0
- secondshift/connections/catalog.py +96 -0
- secondshift/connections/checks.py +22 -0
- secondshift/connections/dao.py +305 -0
- secondshift/connections/errors.py +13 -0
- secondshift/connections/execution.py +262 -0
- secondshift/connections/external.py +258 -0
- secondshift/connections/github.py +123 -0
- secondshift/connections/http_api/__init__.py +1 -0
- secondshift/connections/http_api/client.py +161 -0
- secondshift/connections/http_api/connections.py +187 -0
- secondshift/connections/http_api/spec.py +382 -0
- secondshift/connections/mcp/__init__.py +1 -0
- secondshift/connections/mcp/base.py +139 -0
- secondshift/connections/mcp/client.py +353 -0
- secondshift/connections/mcp/custom.py +86 -0
- secondshift/connections/mcp/github.py +256 -0
- secondshift/connections/mcp/oauth.py +352 -0
- secondshift/connections/mcp/providers.py +15 -0
- secondshift/connections/mcp/schemas.py +77 -0
- secondshift/connections/mcp/sentry.py +135 -0
- secondshift/connections/mcp/signin.py +206 -0
- secondshift/connections/migration_helpers.py +28 -0
- secondshift/connections/migrations/0001_initial.py +101 -0
- secondshift/connections/migrations/0002_alter_connection_kind.py +18 -0
- secondshift/connections/migrations/0003_require_approval.py +23 -0
- secondshift/connections/migrations/0004_new_connections_skip_approval.py +18 -0
- secondshift/connections/migrations/0005_agentconnection.py +36 -0
- secondshift/connections/migrations/0006_remove_is_default.py +23 -0
- secondshift/connections/migrations/0007_agent_last_check.py +18 -0
- secondshift/connections/migrations/0008_agentcredentialcleanup.py +25 -0
- secondshift/connections/migrations/0009_release_0_1_0b11.py +52 -0
- secondshift/connections/migrations/0010_connection_attribution.py +18 -0
- secondshift/connections/migrations/__init__.py +0 -0
- secondshift/connections/model_providers.py +441 -0
- secondshift/connections/models.py +246 -0
- secondshift/connections/outbound.py +317 -0
- secondshift/connections/repository_ref.py +19 -0
- secondshift/connections/schemas.py +313 -0
- secondshift/connections/serialization.py +100 -0
- secondshift/connections/services.py +94 -0
- secondshift/connections/tools.py +38 -0
- secondshift/connections/webhooks.py +107 -0
- secondshift/core/__init__.py +0 -0
- secondshift/core/adapters.py +38 -0
- secondshift/core/admin.py +5 -0
- secondshift/core/api.py +104 -0
- secondshift/core/apps.py +20 -0
- secondshift/core/launcher.py +311 -0
- secondshift/core/local_agents.py +90 -0
- secondshift/core/local_output.py +120 -0
- secondshift/core/management/__init__.py +0 -0
- secondshift/core/management/commands/__init__.py +0 -0
- secondshift/core/management/commands/bootstrap_dev.py +33 -0
- secondshift/core/management/commands/dev_reset_db.py +204 -0
- secondshift/core/management/commands/login.py +23 -0
- secondshift/core/management/commands/logs.py +58 -0
- secondshift/core/management/commands/status.py +46 -0
- secondshift/core/management/commands/up.py +244 -0
- secondshift/core/middleware.py +21 -0
- secondshift/core/migrations/__init__.py +0 -0
- secondshift/core/schemas.py +71 -0
- secondshift/core/signin.py +58 -0
- secondshift/core/static/core/account.css +149 -0
- secondshift/core/static/core/admin.css +135 -0
- secondshift/core/templatetags/__init__.py +0 -0
- secondshift/core/templatetags/accounts.py +20 -0
- secondshift/core/terminal.py +27 -0
- secondshift/core/views.py +10 -0
- secondshift/gateway/__init__.py +0 -0
- secondshift/gateway/apps.py +6 -0
- secondshift/gateway/dao.py +137 -0
- secondshift/gateway/metering.py +171 -0
- secondshift/gateway/migrations/0001_initial.py +54 -0
- secondshift/gateway/migrations/0002_modelusage_gateway_usage_created_at_idx.py +19 -0
- secondshift/gateway/migrations/__init__.py +0 -0
- secondshift/gateway/models.py +61 -0
- secondshift/gateway/pricing.py +39 -0
- secondshift/gateway/proxy.py +369 -0
- secondshift/gateway/tokens.py +68 -0
- secondshift/gateway/urls.py +17 -0
- secondshift/gateway/views.py +11 -0
- secondshift/github/__init__.py +6 -0
- secondshift/github/app.py +109 -0
- secondshift/github/authors.py +56 -0
- secondshift/github/credentials.py +59 -0
- secondshift/github/errors.py +2 -0
- secondshift/github/git.py +56 -0
- secondshift/github/identity.py +35 -0
- secondshift/github/installations.py +74 -0
- secondshift/github/issues.py +75 -0
- secondshift/github/permissions.py +49 -0
- secondshift/github/pulls.py +49 -0
- secondshift/github/repos.py +52 -0
- secondshift/github/rest.py +93 -0
- secondshift/github/ruff.toml +16 -0
- secondshift/github/tokens.py +52 -0
- secondshift/jobs/__init__.py +0 -0
- secondshift/jobs/access.py +12 -0
- secondshift/jobs/admin.py +83 -0
- secondshift/jobs/api.py +184 -0
- secondshift/jobs/apps.py +6 -0
- secondshift/jobs/automatic_grants.py +115 -0
- secondshift/jobs/autosync.py +78 -0
- secondshift/jobs/capabilities.py +43 -0
- secondshift/jobs/dao.py +258 -0
- secondshift/jobs/drafts.py +112 -0
- secondshift/jobs/grants.py +116 -0
- secondshift/jobs/limits.py +26 -0
- secondshift/jobs/management/__init__.py +0 -0
- secondshift/jobs/management/commands/__init__.py +0 -0
- secondshift/jobs/manifest.py +364 -0
- secondshift/jobs/migrations/0001_initial.py +87 -0
- secondshift/jobs/migrations/0002_job_history.py +18 -0
- secondshift/jobs/migrations/0003_job_skill_text.py +23 -0
- secondshift/jobs/migrations/0004_jobgrant_tool_rules.py +18 -0
- secondshift/jobs/migrations/0005_job_triggers.py +18 -0
- secondshift/jobs/migrations/0006_job_webhook.py +18 -0
- secondshift/jobs/migrations/0007_job_deliveries.py +23 -0
- secondshift/jobs/migrations/0008_job_removed_at.py +18 -0
- secondshift/jobs/migrations/0009_job_subagents.py +18 -0
- secondshift/jobs/migrations/0010_job_skill_source.py +18 -0
- secondshift/jobs/migrations/__init__.py +0 -0
- secondshift/jobs/models.py +199 -0
- secondshift/jobs/needs.py +154 -0
- secondshift/jobs/plan.py +176 -0
- secondshift/jobs/planning.py +386 -0
- secondshift/jobs/readiness.py +46 -0
- secondshift/jobs/repository_sync.py +199 -0
- secondshift/jobs/scheduling.py +26 -0
- secondshift/jobs/schemas.py +207 -0
- secondshift/jobs/serialization.py +218 -0
- secondshift/jobs/skill_needs.py +189 -0
- secondshift/jobs/sync.py +252 -0
- secondshift/jobs/tool_policy.py +82 -0
- secondshift/jobs/tool_rules.py +200 -0
- secondshift/mcp_server/__init__.py +6 -0
- secondshift/mcp_server/api.py +79 -0
- secondshift/mcp_server/apps.py +7 -0
- secondshift/mcp_server/audit.py +22 -0
- secondshift/mcp_server/core.py +12 -0
- secondshift/mcp_server/dao.py +23 -0
- secondshift/mcp_server/migrations/0001_initial.py +33 -0
- secondshift/mcp_server/migrations/__init__.py +0 -0
- secondshift/mcp_server/models.py +40 -0
- secondshift/mcp_server/schemas.py +204 -0
- secondshift/mcp_server/tools/__init__.py +9 -0
- secondshift/mcp_server/tools/jobs.py +106 -0
- secondshift/mcp_server/tools/manifests.py +127 -0
- secondshift/mcp_server/tools/repositories.py +42 -0
- secondshift/mcp_server/tools/runs.py +86 -0
- secondshift/mcp_server/tools/tasks.py +103 -0
- secondshift/mcp_server/tools_registry.py +81 -0
- secondshift/repositories/__init__.py +0 -0
- secondshift/repositories/access.py +13 -0
- secondshift/repositories/admin.py +19 -0
- secondshift/repositories/api.py +296 -0
- secondshift/repositories/apps.py +6 -0
- secondshift/repositories/dao.py +114 -0
- secondshift/repositories/external_skills.py +224 -0
- secondshift/repositories/github.py +27 -0
- secondshift/repositories/github_access.py +57 -0
- secondshift/repositories/github_check.py +279 -0
- secondshift/repositories/github_sync.py +181 -0
- secondshift/repositories/github_views.py +90 -0
- secondshift/repositories/local.py +198 -0
- secondshift/repositories/management/__init__.py +0 -0
- secondshift/repositories/management/commands/__init__.py +0 -0
- secondshift/repositories/management/commands/prune_source_cache.py +15 -0
- secondshift/repositories/migrations/0001_initial.py +45 -0
- secondshift/repositories/migrations/0002_repository_last_sync_error.py +18 -0
- secondshift/repositories/migrations/0003_repository_checked_sha.py +18 -0
- secondshift/repositories/migrations/0004_pull_request_settings.py +23 -0
- secondshift/repositories/migrations/0005_remove_repository_installation_id.py +17 -0
- secondshift/repositories/migrations/0006_allow_issues.py +18 -0
- secondshift/repositories/migrations/__init__.py +0 -0
- secondshift/repositories/mirrors.py +50 -0
- secondshift/repositories/models.py +104 -0
- secondshift/repositories/pull_requests.py +199 -0
- secondshift/repositories/schemas.py +102 -0
- secondshift/repositories/serialization.py +133 -0
- secondshift/repositories/skill_source.py +64 -0
- secondshift/repositories/snapshots.py +238 -0
- secondshift/repositories/sources.py +256 -0
- secondshift/repositories/status.py +67 -0
- secondshift/runner/README.md +65 -0
- secondshift/runner/__init__.py +1 -0
- secondshift/runner/agent_credentials.py +89 -0
- secondshift/runner/agent_image/Dockerfile +16 -0
- secondshift/runner/agent_image/agent.py +132 -0
- secondshift/runner/agent_image/claude.py +254 -0
- secondshift/runner/agent_image/codex.py +84 -0
- secondshift/runner/agent_image/outcomes.py +120 -0
- secondshift/runner/agent_image/subagent_hook.py +71 -0
- secondshift/runner/agent_protocol.py +41 -0
- secondshift/runner/agents.py +254 -0
- secondshift/runner/api.py +123 -0
- secondshift/runner/apps.py +6 -0
- secondshift/runner/client.py +168 -0
- secondshift/runner/collection.py +276 -0
- secondshift/runner/dao.py +64 -0
- secondshift/runner/dev.py +19 -0
- secondshift/runner/images.py +57 -0
- secondshift/runner/migrations/0001_initial.py +48 -0
- secondshift/runner/migrations/0002_session_source_sha256.py +18 -0
- secondshift/runner/migrations/0003_session_collect_changes.py +18 -0
- secondshift/runner/migrations/0004_session_history_sha256.py +18 -0
- secondshift/runner/migrations/0005_require_source_sha256.py +18 -0
- secondshift/runner/migrations/0006_session_skill_sha256.py +18 -0
- secondshift/runner/migrations/__init__.py +0 -0
- secondshift/runner/models.py +53 -0
- secondshift/runner/protocol.py +97 -0
- secondshift/runner/schemas.py +36 -0
- secondshift/runner/services.py +465 -0
- secondshift/runner/sources.py +130 -0
- secondshift/runner/urls.py +7 -0
- secondshift/runs/__init__.py +0 -0
- secondshift/runs/access.py +13 -0
- secondshift/runs/admin.py +89 -0
- secondshift/runs/api.py +211 -0
- secondshift/runs/apps.py +11 -0
- secondshift/runs/artifacts.py +261 -0
- secondshift/runs/dao.py +640 -0
- secondshift/runs/deliveries.py +80 -0
- secondshift/runs/execution/__init__.py +0 -0
- secondshift/runs/execution/access_needs.py +89 -0
- secondshift/runs/execution/agent.py +91 -0
- secondshift/runs/execution/agent_broker.py +198 -0
- secondshift/runs/execution/budgets.py +177 -0
- secondshift/runs/execution/cli_agent.py +167 -0
- secondshift/runs/execution/consent.py +58 -0
- secondshift/runs/execution/deadlines.py +47 -0
- secondshift/runs/execution/external_tools.py +84 -0
- secondshift/runs/execution/issues.py +99 -0
- secondshift/runs/execution/model_gateway.py +90 -0
- secondshift/runs/execution/payload.py +63 -0
- secondshift/runs/execution/result.py +59 -0
- secondshift/runs/execution/run.py +345 -0
- secondshift/runs/execution/subagents.py +246 -0
- secondshift/runs/execution/tool_broker.py +123 -0
- secondshift/runs/execution/workspace_tools.py +248 -0
- secondshift/runs/history.py +182 -0
- secondshift/runs/inputs.py +49 -0
- secondshift/runs/issue_delivery.py +144 -0
- secondshift/runs/management/__init__.py +0 -0
- secondshift/runs/management/commands/__init__.py +0 -0
- secondshift/runs/management/commands/prune_runs.py +12 -0
- secondshift/runs/management/commands/run_dbos_worker.py +129 -0
- secondshift/runs/migration_helpers.py +313 -0
- secondshift/runs/migrations/0001_initial.py +69 -0
- secondshift/runs/migrations/0002_run_started_by.py +18 -0
- secondshift/runs/migrations/0003_agentlease.py +25 -0
- secondshift/runs/migrations/0004_task_assignment.py +39 -0
- secondshift/runs/migrations/0005_backfill_tasks.py +19 -0
- secondshift/runs/migrations/0006_standalone_tasks.py +50 -0
- secondshift/runs/migrations/0007_run_delivery.py +35 -0
- secondshift/runs/migrations/0008_run_outputs.py +18 -0
- secondshift/runs/migrations/0009_pull_request_delivery.py +28 -0
- secondshift/runs/migrations/0010_run_issue.py +32 -0
- secondshift/runs/migrations/0011_issues_delivery.py +23 -0
- secondshift/runs/migrations/0012_alter_rundelivery_status.py +18 -0
- secondshift/runs/migrations/0013_release_0_1_0b11.py +113 -0
- secondshift/runs/migrations/0014_run_access_need.py +30 -0
- secondshift/runs/migrations/__init__.py +0 -0
- secondshift/runs/models.py +340 -0
- secondshift/runs/orchestration/__init__.py +0 -0
- secondshift/runs/orchestration/admission.py +15 -0
- secondshift/runs/orchestration/dbos_runtime.py +43 -0
- secondshift/runs/orchestration/dispatch.py +130 -0
- secondshift/runs/orchestration/periodic.py +39 -0
- secondshift/runs/orchestration/reconciliation.py +98 -0
- secondshift/runs/orchestration/schedules.py +51 -0
- secondshift/runs/orchestration/workflows.py +480 -0
- secondshift/runs/outputs.py +52 -0
- secondshift/runs/publishing.py +162 -0
- secondshift/runs/queue.py +26 -0
- secondshift/runs/reporting/__init__.py +0 -0
- secondshift/runs/reporting/collection.py +26 -0
- secondshift/runs/reporting/connection_usage.py +45 -0
- secondshift/runs/reporting/failures.py +150 -0
- secondshift/runs/reporting/health.py +158 -0
- secondshift/runs/reporting/notifications.py +258 -0
- secondshift/runs/reporting/queries.py +60 -0
- secondshift/runs/retention.py +35 -0
- secondshift/runs/schemas.py +192 -0
- secondshift/runs/serialization.py +245 -0
- secondshift/runs/services.py +176 -0
- secondshift/runs/signals.py +25 -0
- secondshift/runs/submissions.py +236 -0
- secondshift/runs/transitions.py +84 -0
- secondshift/templates/account/password_reset_help_text.html +1 -0
- secondshift/templates/admin/base_site.html +24 -0
- secondshift/templates/allauth/elements/provider.html +9 -0
- secondshift/templates/allauth/layouts/base.html +67 -0
- secondshift/templates/socialaccount/snippets/login.html +9 -0
- secondshift/templates/workspaces/invite_unusable.html +10 -0
- secondshift/triggers/__init__.py +0 -0
- secondshift/triggers/admin.py +19 -0
- secondshift/triggers/api.py +116 -0
- secondshift/triggers/apps.py +6 -0
- secondshift/triggers/dao.py +66 -0
- secondshift/triggers/filters.py +48 -0
- secondshift/triggers/migrations/0001_initial.py +55 -0
- secondshift/triggers/migrations/0002_ignored_events_and_hourly_limit.py +23 -0
- secondshift/triggers/migrations/__init__.py +0 -0
- secondshift/triggers/models.py +83 -0
- secondshift/triggers/schemas.py +58 -0
- secondshift/triggers/serialization.py +91 -0
- secondshift/triggers/services.py +246 -0
- secondshift/triggers/verification.py +84 -0
- secondshift/web/assets/index-Bwy0hvk_.js +2419 -0
- secondshift/web/assets/index-D27gB9_L.css +1 -0
- secondshift/web/favicon.svg +19 -0
- secondshift/web/index.html +16 -0
- secondshift/web/secondshift-mark.svg +16 -0
- secondshift/workspaces/__init__.py +0 -0
- secondshift/workspaces/access.py +181 -0
- secondshift/workspaces/admin.py +37 -0
- secondshift/workspaces/api.py +264 -0
- secondshift/workspaces/apps.py +6 -0
- secondshift/workspaces/context_processors.py +11 -0
- secondshift/workspaces/dao.py +281 -0
- secondshift/workspaces/invites.py +150 -0
- secondshift/workspaces/limits.py +197 -0
- secondshift/workspaces/management/__init__.py +0 -0
- secondshift/workspaces/management/commands/__init__.py +0 -0
- secondshift/workspaces/management/commands/create_workspace.py +39 -0
- secondshift/workspaces/migrations/0001_initial.py +67 -0
- secondshift/workspaces/migrations/0002_api_token.py +34 -0
- secondshift/workspaces/migrations/0003_execution_default.py +29 -0
- secondshift/workspaces/migrations/0004_copy_default_model_key.py +26 -0
- secondshift/workspaces/migrations/__init__.py +0 -0
- secondshift/workspaces/models.py +169 -0
- secondshift/workspaces/owners.py +58 -0
- secondshift/workspaces/schemas.py +149 -0
- secondshift/workspaces/serialization.py +138 -0
- secondshift/workspaces/server_settings.py +140 -0
- secondshift/workspaces/services.py +94 -0
- secondshift/workspaces/tokens.py +108 -0
- secondshift/workspaces/views.py +32 -0
- secondshift-0.1.0b12.dist-info/METADATA +270 -0
- secondshift-0.1.0b12.dist-info/RECORD +371 -0
- secondshift-0.1.0b12.dist-info/WHEEL +4 -0
- secondshift-0.1.0b12.dist-info/entry_points.txt +2 -0
- secondshift-0.1.0b12.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
import djclick as click
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.core.management import call_command
|
|
6
|
+
from django.utils import timezone
|
|
7
|
+
|
|
8
|
+
from secondshift.config.constants import RunStart, SandboxRuntime
|
|
9
|
+
from secondshift.connections.dao import create_connection
|
|
10
|
+
from secondshift.connections.models import Connection
|
|
11
|
+
from secondshift.github.identity import clone_url
|
|
12
|
+
from secondshift.jobs.dao import create_job
|
|
13
|
+
from secondshift.jobs.planning import build_execution_plan
|
|
14
|
+
from secondshift.repositories.dao import create_repository
|
|
15
|
+
from secondshift.runs.dao import add_event, create_run_record, create_task
|
|
16
|
+
from secondshift.runs.models import Run
|
|
17
|
+
from secondshift.workspaces.dao import get_workspace_by_slug
|
|
18
|
+
|
|
19
|
+
DEMO_SKILLS = {
|
|
20
|
+
"weekly-infra": """# Weekly infrastructure review
|
|
21
|
+
|
|
22
|
+
Review infrastructure changes merged in the last seven days.
|
|
23
|
+
|
|
24
|
+
## Steps
|
|
25
|
+
|
|
26
|
+
1. List commits under `infra/` and `deploy/` since the last report.
|
|
27
|
+
2. For each change, note what it affects and who merged it.
|
|
28
|
+
3. Query the `observability` binding for error rates around each deploy.
|
|
29
|
+
4. Flag drift between Terraform state and the committed configuration.
|
|
30
|
+
|
|
31
|
+
## Report
|
|
32
|
+
|
|
33
|
+
- Start with one line: how many changes, how many need attention.
|
|
34
|
+
- List each finding with the commit, the evidence and a suggested owner.
|
|
35
|
+
- Do not suggest changes you cannot point to evidence for.
|
|
36
|
+
""",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@click.command()
|
|
41
|
+
@click.option(
|
|
42
|
+
"--yes",
|
|
43
|
+
is_flag=True,
|
|
44
|
+
help="Confirm destroying every record in the development database.",
|
|
45
|
+
)
|
|
46
|
+
def command(yes: bool) -> None:
|
|
47
|
+
"""Reset the development database, migrate it, and load demo data."""
|
|
48
|
+
if not settings.DEV:
|
|
49
|
+
raise click.ClickException("dev_reset_db only runs with SECONDSHIFT_ENV=dev.")
|
|
50
|
+
if not yes:
|
|
51
|
+
raise click.ClickException(
|
|
52
|
+
"dev_reset_db destroys the development database. Pass --yes to continue."
|
|
53
|
+
)
|
|
54
|
+
click.echo("Resetting the development database...")
|
|
55
|
+
call_command("reset_db", interactive=False, close_sessions=True, verbosity=0)
|
|
56
|
+
call_command("migrate", interactive=False, verbosity=0)
|
|
57
|
+
call_command("bootstrap_dev")
|
|
58
|
+
seed_demo_data()
|
|
59
|
+
click.secho("Development demo data is ready.", fg="green")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def seed_demo_data() -> None:
|
|
63
|
+
"""Load safe, credential-free records for exploring the development UI."""
|
|
64
|
+
workspace = get_workspace_by_slug("secondshift")
|
|
65
|
+
now = timezone.now()
|
|
66
|
+
|
|
67
|
+
github = create_connection(
|
|
68
|
+
workspace=workspace,
|
|
69
|
+
provider=Connection.Provider.GITHUB,
|
|
70
|
+
kind=Connection.Kind.SOURCE,
|
|
71
|
+
display_name="GitHub · demo",
|
|
72
|
+
external_account_id="demo-github",
|
|
73
|
+
status=Connection.Status.CONNECTED,
|
|
74
|
+
auth_strategy=Connection.AuthStrategy.MANAGED,
|
|
75
|
+
metadata={"demo": True},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
repositories = {
|
|
79
|
+
name: create_repository(
|
|
80
|
+
workspace=workspace,
|
|
81
|
+
github_connection=github,
|
|
82
|
+
# GitHub repository ids are numbers.
|
|
83
|
+
external_id=str(900_000 + index),
|
|
84
|
+
full_name=name,
|
|
85
|
+
clone_url=clone_url(name),
|
|
86
|
+
default_branch=branch,
|
|
87
|
+
enabled=enabled,
|
|
88
|
+
config_sha=commit_sha,
|
|
89
|
+
last_synced_at=now - timedelta(minutes=index * 7),
|
|
90
|
+
)
|
|
91
|
+
for index, (name, branch, enabled, commit_sha) in enumerate(
|
|
92
|
+
(
|
|
93
|
+
("wild-edge/wild-edge", "main", True, "8f3c9d2" * 5 + "8f3c9"),
|
|
94
|
+
("secondshift/examples", "main", True, "34a0be7" * 5 + "34a0b"),
|
|
95
|
+
("acme/platform-infra", "production", False, "c77b103" * 5 + "c77b1"),
|
|
96
|
+
),
|
|
97
|
+
start=1,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
jobs = {
|
|
102
|
+
key: create_job(
|
|
103
|
+
workspace=workspace,
|
|
104
|
+
repository=repositories[repository],
|
|
105
|
+
key=key,
|
|
106
|
+
name=name,
|
|
107
|
+
skill_path=skill_path,
|
|
108
|
+
skill_text=DEMO_SKILLS.get(key, ""),
|
|
109
|
+
cron=cron,
|
|
110
|
+
timezone=job_timezone,
|
|
111
|
+
enabled=False,
|
|
112
|
+
config_hash=(key.encode().hex() + "0" * 64)[:64],
|
|
113
|
+
)
|
|
114
|
+
for key, name, repository, skill_path, cron, job_timezone in (
|
|
115
|
+
(
|
|
116
|
+
"weekly-infra",
|
|
117
|
+
"Weekly infrastructure review",
|
|
118
|
+
"wild-edge/wild-edge",
|
|
119
|
+
".agents/skills/infrastructure-review/SKILL.md",
|
|
120
|
+
"0 9 * * 1",
|
|
121
|
+
"Europe/Warsaw",
|
|
122
|
+
),
|
|
123
|
+
(
|
|
124
|
+
"dependency-sweep",
|
|
125
|
+
"Dependency freshness sweep",
|
|
126
|
+
"wild-edge/wild-edge",
|
|
127
|
+
".agents/skills/dependency-sweep/SKILL.md",
|
|
128
|
+
"30 7 * * 2",
|
|
129
|
+
"UTC",
|
|
130
|
+
),
|
|
131
|
+
(
|
|
132
|
+
"release-notes",
|
|
133
|
+
"Release notes digest",
|
|
134
|
+
"secondshift/examples",
|
|
135
|
+
".agents/skills/release-notes/SKILL.md",
|
|
136
|
+
"0 16 * * 5",
|
|
137
|
+
"America/New_York",
|
|
138
|
+
),
|
|
139
|
+
(
|
|
140
|
+
"incident-followup",
|
|
141
|
+
"Incident follow-up",
|
|
142
|
+
"acme/platform-infra",
|
|
143
|
+
".agents/skills/incident-review/SKILL.md",
|
|
144
|
+
"0 */6 * * *",
|
|
145
|
+
"UTC",
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
run_specs = (
|
|
151
|
+
(
|
|
152
|
+
"weekly-infra",
|
|
153
|
+
Run.Status.SUCCEEDED,
|
|
154
|
+
1,
|
|
155
|
+
"Reviewed 14 infrastructure changes; no critical drift found.",
|
|
156
|
+
0,
|
|
157
|
+
),
|
|
158
|
+
(
|
|
159
|
+
"dependency-sweep",
|
|
160
|
+
Run.Status.SUCCEEDED,
|
|
161
|
+
2,
|
|
162
|
+
"Found two dependency updates ready for review.",
|
|
163
|
+
0,
|
|
164
|
+
),
|
|
165
|
+
(
|
|
166
|
+
"weekly-infra",
|
|
167
|
+
Run.Status.FAILED,
|
|
168
|
+
3,
|
|
169
|
+
"The observability capability needs attention.",
|
|
170
|
+
1,
|
|
171
|
+
),
|
|
172
|
+
(
|
|
173
|
+
"release-notes",
|
|
174
|
+
Run.Status.SUCCEEDED,
|
|
175
|
+
5,
|
|
176
|
+
"Prepared a release digest for the engineering team.",
|
|
177
|
+
0,
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
for index, (job_key, status, days_ago, summary, exit_code) in enumerate(
|
|
181
|
+
run_specs, start=1
|
|
182
|
+
):
|
|
183
|
+
finished_at = now - timedelta(days=days_ago)
|
|
184
|
+
run = create_run_record(
|
|
185
|
+
task=create_task(repository=jobs[job_key].repository),
|
|
186
|
+
started_by=RunStart.SCHEDULE,
|
|
187
|
+
execution_plan=build_execution_plan(jobs[job_key]).stored(),
|
|
188
|
+
job=jobs[job_key],
|
|
189
|
+
status=status,
|
|
190
|
+
commit_sha=jobs[job_key].repository.config_sha,
|
|
191
|
+
scheduled_for=finished_at - timedelta(minutes=4),
|
|
192
|
+
started_at=finished_at - timedelta(minutes=4),
|
|
193
|
+
finished_at=finished_at,
|
|
194
|
+
sandbox_runtime=SandboxRuntime.GVISOR,
|
|
195
|
+
exit_code=exit_code,
|
|
196
|
+
summary=summary,
|
|
197
|
+
dbos_workflow_id=f"demo-run-{index}",
|
|
198
|
+
)
|
|
199
|
+
add_event(
|
|
200
|
+
run.pk,
|
|
201
|
+
summary,
|
|
202
|
+
level="info" if status == Run.Status.SUCCEEDED else "error",
|
|
203
|
+
metadata={"demo": True},
|
|
204
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import djclick as click
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
|
|
4
|
+
from secondshift.core import local_output
|
|
5
|
+
from secondshift.core.signin import SIGN_IN_LINK_SECONDS, sign_in_url
|
|
6
|
+
from secondshift.workspaces.owners import get_owner, has_owner
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command()
|
|
10
|
+
@click.option(
|
|
11
|
+
"--url",
|
|
12
|
+
default=None,
|
|
13
|
+
help="Where SecondShift is running. Defaults to where secondshift up serves it.",
|
|
14
|
+
)
|
|
15
|
+
def command(url: str | None) -> None:
|
|
16
|
+
"""Print a one-time link that signs you in as the owner."""
|
|
17
|
+
if not has_owner():
|
|
18
|
+
raise click.ClickException("There is no owner yet. Run secondshift up first.")
|
|
19
|
+
state = local_output.load_state()
|
|
20
|
+
base = url or (state["url"] if state else settings.FRONTEND_URL)
|
|
21
|
+
minutes = SIGN_IN_LINK_SECONDS // 60
|
|
22
|
+
click.echo(f"This link works once, within {minutes} minutes:")
|
|
23
|
+
click.echo(sign_in_url(base, get_owner()))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Inspect local service diagnostics."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections import deque
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import djclick as click
|
|
8
|
+
|
|
9
|
+
from secondshift.core.local_output import LOG_NAMES, log_directory
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
@click.option("--service", type=click.Choice(LOG_NAMES))
|
|
14
|
+
@click.option("--lines", default=50, type=click.IntRange(1, 10000), show_default=True)
|
|
15
|
+
@click.option("--follow", is_flag=True, help="Stream new log entries until Ctrl+C.")
|
|
16
|
+
def command(service: str | None, lines: int, follow: bool) -> None:
|
|
17
|
+
"""Show saved local service logs."""
|
|
18
|
+
names = [service] if service else LOG_NAMES
|
|
19
|
+
paths = [log_directory() / f"{name}.log" for name in names]
|
|
20
|
+
positions = {path: print_tail(path, lines) for path in paths if path.exists()}
|
|
21
|
+
if not positions and not follow:
|
|
22
|
+
click.echo("No local logs yet. Run secondshift up first.")
|
|
23
|
+
try:
|
|
24
|
+
while follow:
|
|
25
|
+
for path in paths:
|
|
26
|
+
position = read_new(path, positions.get(path))
|
|
27
|
+
if position is not None:
|
|
28
|
+
positions[path] = position
|
|
29
|
+
time.sleep(0.2)
|
|
30
|
+
except KeyboardInterrupt:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def print_tail(path: Path, lines: int) -> tuple[int, int]:
|
|
35
|
+
"""Print the last lines of a log and return where reading stopped."""
|
|
36
|
+
with path.open(errors="replace") as stream:
|
|
37
|
+
for line in deque(stream, maxlen=lines):
|
|
38
|
+
click.echo(line, nl=False)
|
|
39
|
+
return path.stat().st_ino, stream.tell()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def read_new(path: Path, position: tuple[int, int] | None) -> tuple[int, int] | None:
|
|
43
|
+
"""Print lines added since position, starting over if the file rotated.
|
|
44
|
+
|
|
45
|
+
A position is the file's inode and the offset already printed.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
with path.open(errors="replace") as stream:
|
|
49
|
+
stat = path.stat()
|
|
50
|
+
inode, offset = position or (stat.st_ino, 0)
|
|
51
|
+
if inode != stat.st_ino or stat.st_size < offset:
|
|
52
|
+
offset = 0
|
|
53
|
+
stream.seek(offset)
|
|
54
|
+
for line in stream:
|
|
55
|
+
click.echo(line, nl=False)
|
|
56
|
+
return stat.st_ino, stream.tell()
|
|
57
|
+
except FileNotFoundError:
|
|
58
|
+
return position
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Inspect the local supervisor without starting services."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import djclick as click
|
|
6
|
+
|
|
7
|
+
from secondshift.core import launcher, local_output, terminal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
def command() -> None:
|
|
12
|
+
"""Show local service status and the dashboard address."""
|
|
13
|
+
if not local_output.is_running():
|
|
14
|
+
click.echo("SecondShift is stopped. Start it with secondshift up.")
|
|
15
|
+
return
|
|
16
|
+
state = local_output.load_state()
|
|
17
|
+
if state is None:
|
|
18
|
+
click.echo("SecondShift is starting.")
|
|
19
|
+
return
|
|
20
|
+
click.echo(f"Dashboard: {terminal.link(state['url'])}")
|
|
21
|
+
all_ready = True
|
|
22
|
+
for name, service in state["services"].items():
|
|
23
|
+
ready = is_alive(service["pid"]) and launcher.is_ready(
|
|
24
|
+
name, service["health_url"]
|
|
25
|
+
)
|
|
26
|
+
all_ready &= ready
|
|
27
|
+
label = (
|
|
28
|
+
click.style("ready", fg="green")
|
|
29
|
+
if ready
|
|
30
|
+
else click.style("starting or unavailable", fg="yellow")
|
|
31
|
+
)
|
|
32
|
+
click.echo(f" {name}: {label}")
|
|
33
|
+
if not all_ready:
|
|
34
|
+
terminal.note(local_output.LOGS_HINT)
|
|
35
|
+
raise SystemExit(1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_alive(pid: int) -> bool:
|
|
39
|
+
try:
|
|
40
|
+
os.kill(pid, 0)
|
|
41
|
+
except ProcessLookupError:
|
|
42
|
+
return False
|
|
43
|
+
except PermissionError:
|
|
44
|
+
# The process exists but belongs to another user.
|
|
45
|
+
return True
|
|
46
|
+
return True
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
import os
|
|
3
|
+
import socket
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import webbrowser
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
|
|
9
|
+
import djclick as click
|
|
10
|
+
from django.conf import settings
|
|
11
|
+
from django.core.management import call_command
|
|
12
|
+
|
|
13
|
+
from secondshift.config.constants import AppEnv, Environment, Service
|
|
14
|
+
from secondshift.core import launcher, local_agents, local_output, terminal
|
|
15
|
+
from secondshift.core.signin import SIGN_IN_LINK_SECONDS, sign_in_url
|
|
16
|
+
from secondshift.workspaces.owners import (
|
|
17
|
+
ensure_owner,
|
|
18
|
+
get_owner,
|
|
19
|
+
has_owner,
|
|
20
|
+
set_owner_credentials,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@click.command()
|
|
25
|
+
@click.option(
|
|
26
|
+
"--host", default=None, help="Web listen host. Overrides SECONDSHIFT_HTTP_BIND."
|
|
27
|
+
)
|
|
28
|
+
@click.option(
|
|
29
|
+
"--port",
|
|
30
|
+
default=None,
|
|
31
|
+
type=int,
|
|
32
|
+
help="Web listen port. Overrides SECONDSHIFT_HTTP_BIND.",
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
"--email",
|
|
36
|
+
envvar="SECONDSHIFT_OWNER_EMAIL",
|
|
37
|
+
help="Owner email. Needed only to share on the network with --host.",
|
|
38
|
+
)
|
|
39
|
+
@click.option(
|
|
40
|
+
"--password",
|
|
41
|
+
envvar="SECONDSHIFT_OWNER_PASSWORD",
|
|
42
|
+
help="Owner password. Needed only to share on the network with --host.",
|
|
43
|
+
)
|
|
44
|
+
@click.option(
|
|
45
|
+
"--browser/--no-browser",
|
|
46
|
+
default=True,
|
|
47
|
+
show_default=True,
|
|
48
|
+
help="Open the sign-in link in your browser once started.",
|
|
49
|
+
)
|
|
50
|
+
@click.option(
|
|
51
|
+
"--agents/--no-agents",
|
|
52
|
+
default=True,
|
|
53
|
+
show_default=True,
|
|
54
|
+
help="Let jobs run through Claude Code or Codex subscriptions.",
|
|
55
|
+
)
|
|
56
|
+
@click.option("--debug", is_flag=True, help="Stream service diagnostics to stderr.")
|
|
57
|
+
def command(
|
|
58
|
+
host: str | None,
|
|
59
|
+
port: int | None,
|
|
60
|
+
email: str | None,
|
|
61
|
+
password: str | None,
|
|
62
|
+
browser: bool,
|
|
63
|
+
agents: bool,
|
|
64
|
+
debug: bool,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Start SecondShift on this computer."""
|
|
67
|
+
if settings.ENVIRONMENT != Environment.LOCAL:
|
|
68
|
+
raise click.ClickException(
|
|
69
|
+
"secondshift up runs with SECONDSHIFT_ENV=local. "
|
|
70
|
+
"In a checkout, use process-compose instead."
|
|
71
|
+
)
|
|
72
|
+
try:
|
|
73
|
+
with local_output.instance_lock():
|
|
74
|
+
start(host, port, email, password, browser, agents, debug)
|
|
75
|
+
except local_output.AlreadyRunning as error:
|
|
76
|
+
raise click.ClickException(
|
|
77
|
+
"SecondShift is already running or starting. Run secondshift status."
|
|
78
|
+
) from error
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def start(
|
|
82
|
+
host: str | None,
|
|
83
|
+
port: int | None,
|
|
84
|
+
email: str | None,
|
|
85
|
+
password: str | None,
|
|
86
|
+
browser: bool,
|
|
87
|
+
agents: bool,
|
|
88
|
+
debug: bool,
|
|
89
|
+
) -> None:
|
|
90
|
+
parent = dict(os.environ)
|
|
91
|
+
if debug:
|
|
92
|
+
parent[AppEnv.LOG_LEVEL] = "DEBUG"
|
|
93
|
+
configured_host, configured_port = launcher.web_address("127.0.0.1", 8000, parent)
|
|
94
|
+
host, port = host or configured_host, port if port is not None else configured_port
|
|
95
|
+
parent.pop(AppEnv.HTTP_BIND, None)
|
|
96
|
+
terminal.heading(f"SecondShift {version('secondshift')}")
|
|
97
|
+
check_port(host, port)
|
|
98
|
+
check_service_ports()
|
|
99
|
+
check_docker()
|
|
100
|
+
terminal.done("Docker is ready")
|
|
101
|
+
if not (settings.WEB_DIR / "index.html").exists():
|
|
102
|
+
raise click.ClickException(
|
|
103
|
+
"The dashboard isn't built. In a checkout, run "
|
|
104
|
+
"npm --prefix dashboard run build first."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
call_command("migrate", interactive=False, verbosity=0)
|
|
108
|
+
terminal.done("Local data is ready", str(settings.DATA_DIR))
|
|
109
|
+
agent_host = None
|
|
110
|
+
if agents:
|
|
111
|
+
try:
|
|
112
|
+
local_agents.prepare_agent_image(settings.AGENT_IMAGE, terminal.note)
|
|
113
|
+
agent_host = local_agents.container_gateway_host()
|
|
114
|
+
terminal.done("Agent image is ready")
|
|
115
|
+
except local_agents.AgentSetupError as error:
|
|
116
|
+
raise click.ClickException(
|
|
117
|
+
f"{error} Run with --no-agents to start without agent subscriptions."
|
|
118
|
+
) from error
|
|
119
|
+
else:
|
|
120
|
+
# An empty value overrides the local default, in every process.
|
|
121
|
+
parent[AppEnv.AGENT_IMAGE] = ""
|
|
122
|
+
services = launcher.services(host, port, parent, agent_host=agent_host)
|
|
123
|
+
runner = next(service for service in services if service.name == Service.RUNNER)
|
|
124
|
+
prepare_runner(runner, debug)
|
|
125
|
+
prepare_owner(host, email, password)
|
|
126
|
+
|
|
127
|
+
url = launcher.public_url(host, port)
|
|
128
|
+
|
|
129
|
+
def ready() -> None:
|
|
130
|
+
link = sign_in_url(url, get_owner())
|
|
131
|
+
minutes = SIGN_IN_LINK_SECONDS // 60
|
|
132
|
+
terminal.done("All services are ready")
|
|
133
|
+
click.echo()
|
|
134
|
+
click.secho("SecondShift is running at ", fg="green", bold=True, nl=False)
|
|
135
|
+
click.echo(terminal.link(url))
|
|
136
|
+
if browser and webbrowser.open(link):
|
|
137
|
+
click.echo("Opening your dashboard...")
|
|
138
|
+
else:
|
|
139
|
+
click.echo(f"Sign in with this link (valid for {minutes} minutes):")
|
|
140
|
+
click.echo(f" {terminal.link(link)}")
|
|
141
|
+
click.echo()
|
|
142
|
+
terminal.note("Keep this terminal open. Press Ctrl+C to stop.")
|
|
143
|
+
terminal.note("Logs: secondshift logs --follow")
|
|
144
|
+
|
|
145
|
+
terminal.note("Starting the web app, model gateway, runner and worker...")
|
|
146
|
+
code = launcher.run(services, ready, click.echo, debug=debug, url=url)
|
|
147
|
+
if code:
|
|
148
|
+
click.echo(local_output.LOGS_HINT, err=True)
|
|
149
|
+
sys.exit(code)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def is_loopback(host: str) -> bool:
|
|
153
|
+
if host == "localhost":
|
|
154
|
+
return True
|
|
155
|
+
try:
|
|
156
|
+
return ipaddress.ip_address(host).is_loopback
|
|
157
|
+
except ValueError:
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def check_docker() -> None:
|
|
162
|
+
try:
|
|
163
|
+
result = subprocess.run(
|
|
164
|
+
["docker", "info"], capture_output=True, timeout=15, check=False
|
|
165
|
+
)
|
|
166
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
167
|
+
result = None
|
|
168
|
+
if result is None or result.returncode:
|
|
169
|
+
raise click.ClickException(
|
|
170
|
+
"Docker isn't running. Skills run in Docker sandboxes. "
|
|
171
|
+
"Start Docker and try again."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def prepare_runner(runner: launcher.Process, debug: bool) -> None:
|
|
176
|
+
migration = subprocess.Popen(
|
|
177
|
+
[sys.executable, "-m", "secondshift", "migrate", "--verbosity", "0"],
|
|
178
|
+
env=runner.env,
|
|
179
|
+
stdout=subprocess.PIPE,
|
|
180
|
+
stderr=subprocess.STDOUT,
|
|
181
|
+
)
|
|
182
|
+
assert migration.stdout is not None
|
|
183
|
+
reader = local_output.capture(migration.stdout, local_output.SETUP_LOG, debug)
|
|
184
|
+
try:
|
|
185
|
+
code = migration.wait()
|
|
186
|
+
except KeyboardInterrupt:
|
|
187
|
+
migration.terminate()
|
|
188
|
+
migration.wait()
|
|
189
|
+
raise
|
|
190
|
+
finally:
|
|
191
|
+
reader.join(timeout=2)
|
|
192
|
+
if code:
|
|
193
|
+
raise click.ClickException(
|
|
194
|
+
"Couldn't prepare runner data. Run secondshift logs --service setup."
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def prepare_owner(host: str, email: str | None, password: str | None) -> None:
|
|
199
|
+
if not has_owner():
|
|
200
|
+
ensure_owner()
|
|
201
|
+
if not is_loopback(host) and not get_owner().has_usable_password():
|
|
202
|
+
# Other machines can reach the app, so a password must guard it.
|
|
203
|
+
click.echo("Sharing on the network needs an owner email and password.")
|
|
204
|
+
set_owner_credentials(
|
|
205
|
+
email or click.prompt("Owner email"),
|
|
206
|
+
password
|
|
207
|
+
or click.prompt(
|
|
208
|
+
"Owner password", hide_input=True, confirmation_prompt=True
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def check_port(
|
|
214
|
+
host: str,
|
|
215
|
+
port: int,
|
|
216
|
+
hint: str = "Try secondshift up --port with an available port.",
|
|
217
|
+
) -> None:
|
|
218
|
+
try:
|
|
219
|
+
addresses = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
|
|
220
|
+
family, kind, protocol, _, address = addresses[0]
|
|
221
|
+
with socket.socket(family, kind, protocol) as listener:
|
|
222
|
+
# The server binds the same way. Without this, closed connections
|
|
223
|
+
# from the last run keep the port looking busy for a minute.
|
|
224
|
+
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
225
|
+
listener.bind(address)
|
|
226
|
+
except OSError as error:
|
|
227
|
+
raise click.ClickException(
|
|
228
|
+
f"Couldn't listen on {host}:{port}: {error.strerror or error}. {hint}"
|
|
229
|
+
) from error
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def check_service_ports() -> None:
|
|
233
|
+
"""--port moves only the web app. The gateway and runner keep their URLs."""
|
|
234
|
+
for name, url, variable in (
|
|
235
|
+
("model gateway", settings.MODEL_GATEWAY_URL, AppEnv.MODEL_GATEWAY_URL),
|
|
236
|
+
("runner", settings.RUNNER_SERVICE_URL, AppEnv.RUNNER_SERVICE_URL),
|
|
237
|
+
):
|
|
238
|
+
check_port(
|
|
239
|
+
"127.0.0.1",
|
|
240
|
+
launcher.local_service_port(url),
|
|
241
|
+
f"The {name} needs this port. Another SecondShift may be running. "
|
|
242
|
+
f"To use another port, set {variable}, "
|
|
243
|
+
"for example to http://127.0.0.1:8202.",
|
|
244
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from django.http import HttpRequest, HttpResponse
|
|
4
|
+
|
|
5
|
+
LIVENESS_PATH = "/-/health/up/"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LivenessMiddleware:
|
|
9
|
+
"""Answer the proxy's liveness probe before host validation and SSL redirects.
|
|
10
|
+
|
|
11
|
+
Kamal's proxy probes the container directly over HTTP with an internal Host
|
|
12
|
+
header, so this must run ahead of SecurityMiddleware and ALLOWED_HOSTS.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
|
|
16
|
+
self.get_response = get_response
|
|
17
|
+
|
|
18
|
+
def __call__(self, request: HttpRequest) -> HttpResponse:
|
|
19
|
+
if request.path == LIVENESS_PATH:
|
|
20
|
+
return HttpResponse("ok", content_type="text/plain")
|
|
21
|
+
return self.get_response(request)
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
|
|
3
|
+
from ninja import Schema
|
|
4
|
+
|
|
5
|
+
from secondshift.connections.schemas import ExecutionStatusSchema
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UserSchema(Schema):
|
|
9
|
+
email: str
|
|
10
|
+
workspace_name: str | None
|
|
11
|
+
workspace_slug: str | None
|
|
12
|
+
# The person's role in the current workspace and what it lets them do.
|
|
13
|
+
role: str | None
|
|
14
|
+
permissions: list[str]
|
|
15
|
+
# Django admin only lets staff in, so links to it are shown only to them.
|
|
16
|
+
can_use_admin: bool
|
|
17
|
+
can_create_workspace: bool
|
|
18
|
+
local_repositories_enabled: bool
|
|
19
|
+
# The operator set up the GitHub App, so repositories can come from GitHub.
|
|
20
|
+
github_app_configured: bool = False
|
|
21
|
+
environment: str = "Production"
|
|
22
|
+
sandbox: str = "gVisor sandbox"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FailingJobSchema(Schema):
|
|
26
|
+
job_id: int
|
|
27
|
+
job: str
|
|
28
|
+
repository: str
|
|
29
|
+
run_id: int
|
|
30
|
+
failed_at: datetime | None
|
|
31
|
+
label: str
|
|
32
|
+
reason: str
|
|
33
|
+
paused: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class JobSpendSchema(Schema):
|
|
37
|
+
job_id: int
|
|
38
|
+
job: str
|
|
39
|
+
repository: str
|
|
40
|
+
runs: int
|
|
41
|
+
spend_usd: float
|
|
42
|
+
partial: bool
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RunDaySchema(Schema):
|
|
46
|
+
day: date
|
|
47
|
+
succeeded: int
|
|
48
|
+
failed: int
|
|
49
|
+
canceled: int
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class OverviewSchema(Schema):
|
|
53
|
+
repositories: int
|
|
54
|
+
local_repositories: int
|
|
55
|
+
enabled_jobs: int
|
|
56
|
+
runs_today: int
|
|
57
|
+
failed_runs: int
|
|
58
|
+
connected_services: int
|
|
59
|
+
failing_jobs: list[FailingJobSchema] = []
|
|
60
|
+
runs_7d: int = 0
|
|
61
|
+
runs_previous_7d: int = 0
|
|
62
|
+
finished_7d: int = 0
|
|
63
|
+
succeeded_7d: int = 0
|
|
64
|
+
spend_7d_usd: float = 0
|
|
65
|
+
spend_previous_7d_usd: float = 0
|
|
66
|
+
spend_partial: bool = False
|
|
67
|
+
spend_by_job: list[JobSpendSchema] = []
|
|
68
|
+
run_days: list[RunDaySchema] = []
|
|
69
|
+
execution: ExecutionStatusSchema | None = None
|
|
70
|
+
# Whether this person has a usable MCP token in this workspace.
|
|
71
|
+
has_agent_token: bool = False
|