flywheel-bootstrap-staging 0.1.9.202602011847__tar.gz → 0.1.9.202602021010__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.
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/PKG-INFO +1 -1
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/git_ops.py +30 -2
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/orchestrator.py +58 -11
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/payload.py +17 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/pyproject.toml +1 -1
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/tests/test_git_ops.py +26 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/tests/test_orchestrator.py +57 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/.gitignore +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/README.md +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/__init__.py +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/__main__.py +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/artifacts.py +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/config_loader.py +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/constants.py +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/install.py +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/prompts.py +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/py.typed +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/runner.py +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap/telemetry.py +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/bootstrap.sh +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/examples/config.example.toml +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/tests/test_artifacts.py +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/tests/test_entrypoint.py +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/tests/test_prompts.py +0 -0
- {flywheel_bootstrap_staging-0.1.9.202602011847 → flywheel_bootstrap_staging-0.1.9.202602021010}/uv.lock +0 -0
|
@@ -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
|
|
|
@@ -182,10 +200,20 @@ def setup_branch(config: GitConfig) -> bool:
|
|
|
182
200
|
)
|
|
183
201
|
|
|
184
202
|
if result.returncode == 0 and branch_name in result.stdout:
|
|
185
|
-
# Branch exists,
|
|
203
|
+
# Branch exists, fetch it explicitly (single-branch clones don't fetch other heads)
|
|
186
204
|
config.log("info", f"Checking out existing branch: {branch_name}")
|
|
205
|
+
fetch_result = _run_git(
|
|
206
|
+
["fetch", "origin", branch_name],
|
|
207
|
+
cwd=workspace,
|
|
208
|
+
)
|
|
209
|
+
if fetch_result.returncode != 0:
|
|
210
|
+
config.log(
|
|
211
|
+
"error", f"Failed to fetch branch {branch_name}: {fetch_result.stderr}"
|
|
212
|
+
)
|
|
213
|
+
return False
|
|
214
|
+
|
|
187
215
|
result = _run_git(
|
|
188
|
-
["checkout", "-B", branch_name,
|
|
216
|
+
["checkout", "-B", branch_name, "FETCH_HEAD"],
|
|
189
217
|
cwd=workspace,
|
|
190
218
|
)
|
|
191
219
|
else:
|
|
@@ -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
|
|
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
|
|
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},
|
|
267
|
+
f"Git: Codex exited with code {exit_code}, committing WIP",
|
|
252
268
|
level="warning",
|
|
253
269
|
)
|
|
254
|
-
|
|
255
|
-
|
|
270
|
+
else:
|
|
271
|
+
self._log("Git: Finalizing repository, committing and pushing changes")
|
|
256
272
|
|
|
257
|
-
|
|
258
|
-
|
|
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.
|
|
277
|
+
self._refresh_github_token()
|
|
264
278
|
|
|
265
|
-
if
|
|
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,
|
|
@@ -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
|
|
|
@@ -213,12 +234,17 @@ class TestSetupBranch:
|
|
|
213
234
|
mock_run_git.side_effect = [
|
|
214
235
|
MagicMock(returncode=0), # fetch
|
|
215
236
|
MagicMock(returncode=0, stdout=branch), # ls-remote (branch exists)
|
|
237
|
+
MagicMock(returncode=0), # fetch branch
|
|
216
238
|
MagicMock(returncode=0), # checkout -B
|
|
217
239
|
]
|
|
218
240
|
|
|
219
241
|
result = setup_branch(git_config)
|
|
220
242
|
assert result is True
|
|
221
243
|
|
|
244
|
+
call_args_list = [call.args[0] for call in mock_run_git.call_args_list]
|
|
245
|
+
assert ["fetch", "origin", branch] in call_args_list
|
|
246
|
+
assert ["checkout", "-B", branch, "FETCH_HEAD"] in call_args_list
|
|
247
|
+
|
|
222
248
|
|
|
223
249
|
class TestInitializeRepo:
|
|
224
250
|
"""Tests for initialize_repo (integration)."""
|
|
@@ -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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|