flywheel-bootstrap-staging 0.1.9.202602012017__tar.gz → 0.1.9.202602012332__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.
Files changed (25) hide show
  1. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/PKG-INFO +1 -1
  2. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/git_ops.py +18 -0
  3. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/orchestrator.py +58 -11
  4. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/payload.py +17 -0
  5. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/pyproject.toml +1 -1
  6. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/tests/test_git_ops.py +21 -0
  7. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/tests/test_orchestrator.py +57 -0
  8. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/.gitignore +0 -0
  9. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/README.md +0 -0
  10. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/__init__.py +0 -0
  11. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/__main__.py +0 -0
  12. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/artifacts.py +0 -0
  13. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/config_loader.py +0 -0
  14. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/constants.py +0 -0
  15. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/install.py +0 -0
  16. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/prompts.py +0 -0
  17. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/py.typed +0 -0
  18. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/runner.py +0 -0
  19. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap/telemetry.py +0 -0
  20. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/bootstrap.sh +0 -0
  21. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/examples/config.example.toml +0 -0
  22. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/tests/test_artifacts.py +0 -0
  23. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/tests/test_entrypoint.py +0 -0
  24. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/tests/test_prompts.py +0 -0
  25. {flywheel_bootstrap_staging-0.1.9.202602012017 → flywheel_bootstrap_staging-0.1.9.202602012332}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flywheel-bootstrap-staging
3
- Version: 0.1.9.202602012017
3
+ Version: 0.1.9.202602012332
4
4
  Summary: Bootstrap runner for Flywheel provisioned GPU instances
5
5
  Project-URL: Homepage, http://paradigma.inc/
6
6
  Author: Paradigma Labs
@@ -122,6 +122,24 @@ def setup_git_credentials(config: GitConfig) -> bool:
122
122
  return False
123
123
 
124
124
 
125
+ def update_git_auth(config: GitConfig, github_token: str) -> bool:
126
+ """Update git auth token and remote URL before pushing."""
127
+ config.github_token = github_token
128
+ repo_url = config.repo_context.repo_url
129
+
130
+ if "github.com" in repo_url:
131
+ auth_url = repo_url.replace(
132
+ "https://github.com", f"https://x-access-token:{github_token}@github.com"
133
+ )
134
+ result = _run_git(
135
+ ["remote", "set-url", "origin", auth_url], cwd=config.workspace
136
+ )
137
+ if result.returncode != 0:
138
+ config.log("warning", f"Failed to update origin URL: {result.stderr}")
139
+
140
+ return setup_git_credentials(config)
141
+
142
+
125
143
  def clone_repository(config: GitConfig) -> bool:
126
144
  """Clone the repository to the workspace.
127
145
 
@@ -26,9 +26,19 @@ from bootstrap.constants import (
26
26
  MAX_ARTIFACT_RETRIES,
27
27
  )
28
28
  from bootstrap.config_loader import UserConfig, load_codex_config
29
- from bootstrap.git_ops import GitConfig, initialize_repo, finalize_repo
29
+ from bootstrap.git_ops import (
30
+ GitConfig,
31
+ commit_changes,
32
+ initialize_repo,
33
+ push_changes,
34
+ update_git_auth,
35
+ )
30
36
  from bootstrap.install import codex_login_status_ok, codex_on_path, ensure_codex
31
- from bootstrap.payload import BootstrapPayload, fetch_bootstrap_payload
37
+ from bootstrap.payload import (
38
+ BootstrapPayload,
39
+ fetch_bootstrap_payload,
40
+ fetch_github_token,
41
+ )
32
42
  from bootstrap.prompts import build_prompt_text
33
43
  from bootstrap.runner import (
34
44
  CodexEvent,
@@ -246,27 +256,64 @@ class BootstrapOrchestrator:
246
256
  if self.git_config is None:
247
257
  return
248
258
 
259
+ commit_message = (
260
+ f"Flywheel experiment run: {self.config.run_id}"
261
+ if exit_code == 0
262
+ else f"[WIP] Flywheel experiment run (failed): {self.config.run_id}"
263
+ )
264
+
249
265
  if exit_code != 0:
250
266
  self._log(
251
- f"Git: Codex exited with code {exit_code}, skipping push",
267
+ f"Git: Codex exited with code {exit_code}, committing WIP",
252
268
  level="warning",
253
269
  )
254
- # Still commit changes so they're not lost
255
- from bootstrap.git_ops import commit_changes
270
+ else:
271
+ self._log("Git: Finalizing repository, committing and pushing changes")
256
272
 
257
- commit_changes(
258
- self.git_config,
259
- f"[WIP] Flywheel experiment run (failed): {self.config.run_id}",
260
- )
273
+ if not commit_changes(self.git_config, commit_message):
274
+ self._log("Git: Failed to commit changes", level="error")
261
275
  return
262
276
 
263
- self._log("Git: Finalizing repository, committing and pushing changes")
277
+ self._refresh_github_token()
264
278
 
265
- if finalize_repo(self.git_config, self.config.run_id):
279
+ if push_changes(self.git_config):
266
280
  self._log("Git: Changes pushed successfully")
267
281
  else:
268
282
  self._log("Git: Failed to push changes", level="error")
269
283
 
284
+ def _refresh_github_token(self) -> None:
285
+ """Refresh the GitHub token before pushing."""
286
+ if self.git_config is None:
287
+ return
288
+
289
+ try:
290
+ token = fetch_github_token(
291
+ self.config.server_url,
292
+ self.config.run_id,
293
+ self.config.capability_token,
294
+ )
295
+ except Exception as exc:
296
+ self._log(
297
+ f"Git: Failed to refresh GitHub token ({exc}); using existing token",
298
+ level="warning",
299
+ )
300
+ return
301
+
302
+ if not token:
303
+ self._log(
304
+ "Git: Empty GitHub token response; using existing token",
305
+ level="warning",
306
+ )
307
+ return
308
+
309
+ if update_git_auth(self.git_config, token):
310
+ self._log("Git: Refreshed GitHub token")
311
+ else:
312
+ self._log(
313
+ "Git: Failed to update git credentials after token refresh",
314
+ level="warning",
315
+ )
316
+
270
317
  def _ensure_codex_authenticated(self, codex_path: Path) -> None:
271
318
  """Fail fast if codex is present but not logged in."""
272
319
  if codex_login_status_ok(codex_path):
@@ -87,6 +87,23 @@ def fetch_bootstrap_payload(
87
87
  )
88
88
 
89
89
 
90
+ def fetch_github_token(
91
+ server_url: str,
92
+ run_id: str,
93
+ token: str,
94
+ ) -> str:
95
+ """Retrieve a fresh GitHub installation token for this run."""
96
+ url = f"{server_url.rstrip('/')}/runs/{run_id}/github-token"
97
+ req = urllib.request.Request(url, headers={"X-Run-Token": token}, method="POST")
98
+ payload = _urlopen_json_with_retries(
99
+ req,
100
+ timeout_seconds=30,
101
+ attempts=4,
102
+ base_delay_seconds=0.5,
103
+ )
104
+ return str(payload.get("token", ""))
105
+
106
+
90
107
  def _urlopen_json_with_retries(
91
108
  req: urllib.request.Request,
92
109
  timeout_seconds: int,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flywheel-bootstrap-staging"
3
- version = "0.1.9.202602012017"
3
+ version = "0.1.9.202602012332"
4
4
  description = "Bootstrap runner for Flywheel provisioned GPU instances"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -16,6 +16,7 @@ from bootstrap.git_ops import (
16
16
  push_changes,
17
17
  setup_branch,
18
18
  setup_git_credentials,
19
+ update_git_auth,
19
20
  )
20
21
  from bootstrap.payload import RepoContext
21
22
 
@@ -87,6 +88,26 @@ class TestSetupGitCredentials:
87
88
  assert "flywheel" in result.stdout.lower()
88
89
 
89
90
 
91
+ class TestUpdateGitAuth:
92
+ """Tests for update_git_auth."""
93
+
94
+ def test_update_git_auth_rewrites_credentials(self, git_config):
95
+ """Updated token should be written to credential store."""
96
+ _run_git(["init"], cwd=git_config.workspace)
97
+ _run_git(
98
+ ["remote", "add", "origin", "https://github.com/testuser/testrepo"],
99
+ cwd=git_config.workspace,
100
+ )
101
+ setup_git_credentials(git_config)
102
+
103
+ result = update_git_auth(git_config, "new-token-999")
104
+ assert result is True
105
+
106
+ credential_file = git_config.workspace / ".git-credentials"
107
+ content = credential_file.read_text()
108
+ assert "new-token-999" in content
109
+
110
+
90
111
  class TestCommitChanges:
91
112
  """Tests for commit_changes."""
92
113
 
@@ -5,6 +5,8 @@ import pytest
5
5
  from bootstrap.artifacts import ManifestResult, ManifestStatus
6
6
  from bootstrap.config_loader import UserConfig
7
7
  from bootstrap.orchestrator import BootstrapConfig, BootstrapOrchestrator
8
+ from bootstrap.git_ops import GitConfig
9
+ from bootstrap.payload import RepoContext
8
10
  from bootstrap.payload import BootstrapPayload
9
11
  from bootstrap.runner import CodexInvocation, CodexEvent
10
12
 
@@ -184,6 +186,61 @@ def test_resolve_workspace_expands_run_id_placeholder(tmp_path) -> None:
184
186
  assert orchestrator.workspace == (tmp_path / "runs" / "run-xyz").resolve()
185
187
 
186
188
 
189
+ def test_finalize_git_repo_pushes_on_failure(monkeypatch, tmp_path) -> None:
190
+ """Non-zero exit should still commit and push changes."""
191
+ cfg = BootstrapConfig(
192
+ run_id="run-123",
193
+ capability_token="token",
194
+ config_path=tmp_path / "config.toml",
195
+ server_url="http://server",
196
+ run_root=tmp_path,
197
+ )
198
+ orchestrator = BootstrapOrchestrator(cfg)
199
+ orchestrator.git_config = GitConfig(
200
+ workspace=tmp_path,
201
+ repo_context=RepoContext(
202
+ repo_url="https://github.com/testuser/repo",
203
+ repo_owner="testuser",
204
+ repo_name="repo",
205
+ branch_name="flywheel/test",
206
+ base_branch="main",
207
+ ),
208
+ github_token="old-token",
209
+ log_fn=lambda *_: None,
210
+ )
211
+
212
+ commit_calls = []
213
+ push_calls = []
214
+ update_calls = []
215
+
216
+ def _record_commit(config: GitConfig, message: str) -> bool:
217
+ commit_calls.append(message)
218
+ return True
219
+
220
+ def _record_push(config: GitConfig) -> bool:
221
+ push_calls.append(True)
222
+ return True
223
+
224
+ def _record_update_auth(config: GitConfig, token: str) -> bool:
225
+ update_calls.append(token)
226
+ return True
227
+
228
+ monkeypatch.setattr("bootstrap.orchestrator.commit_changes", _record_commit)
229
+ monkeypatch.setattr("bootstrap.orchestrator.push_changes", _record_push)
230
+ monkeypatch.setattr(
231
+ "bootstrap.orchestrator.fetch_github_token",
232
+ lambda *args, **kwargs: "new-token",
233
+ )
234
+ monkeypatch.setattr("bootstrap.orchestrator.update_git_auth", _record_update_auth)
235
+
236
+ orchestrator._finalize_git_repo(exit_code=2)
237
+
238
+ assert commit_calls
239
+ assert "[WIP]" in commit_calls[0]
240
+ assert update_calls == ["new-token"]
241
+ assert push_calls
242
+
243
+
187
244
  def test_malformed_manifest_triggers_two_retry_attempts(monkeypatch, tmp_path):
188
245
  """When manifest is malformed, the orchestrator retries up to 2 times with feedback."""
189
246