Skip to content

Commit 3c5fbd3

Browse files
fix[ci]: correct perms to upload back compat fixtures (#7207)
Fixes the perms to upload fixtures Uses s3 dryrun command to make dryrun test s3 bucket perms --------- Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
1 parent f3a9ff5 commit 3c5fbd3

2 files changed

Lines changed: 78 additions & 62 deletions

File tree

.github/workflows/compat-gen-upload.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
&& format('runs-on={0}/runner=amd64-medium/image=ubuntu24-full-x64-pre-v2/tag=compat-gen-dry-run', github.run_id)
2929
|| 'ubuntu-latest' }}
3030
permissions:
31+
id-token: write
3132
contents: read
3233
outputs:
3334
version: ${{ steps.detect.outputs.version }}
@@ -41,6 +42,12 @@ jobs:
4142
fetch-depth: 0
4243
- uses: ./.github/actions/setup-prebuild
4344

45+
- name: Configure AWS credentials
46+
uses: aws-actions/configure-aws-credentials@v6
47+
with:
48+
role-to-assume: "arn:aws:iam::245040174862:role/GitHubCompatUploadRole"
49+
aws-region: us-east-1
50+
4451
- name: Detect version
4552
id: detect
4653
run: |
@@ -87,7 +94,7 @@ jobs:
8794
- name: Configure AWS credentials
8895
uses: aws-actions/configure-aws-credentials@v6
8996
with:
90-
role-to-assume: "arn:aws:iam::245040174862:role/GitHubBenchmarkRole"
97+
role-to-assume: "arn:aws:iam::245040174862:role/GitHubCompatUploadRole"
9198
aws-region: us-east-1
9299

93100
- name: Upload fixtures for v${{ needs.dry-run.outputs.version }}

vortex-test/compat-gen/scripts/compat.py

Lines changed: 70 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,47 @@ def display_name(self) -> str:
211211
return f"s3://{self.bucket}"
212212

213213

214+
class DryRunStore(Store):
215+
"""Wrapper that validates write credentials without actually uploading.
216+
217+
Reads are delegated to the inner store. Writes use ``aws s3 cp --dryrun``
218+
(for S3 stores) so that IAM permissions are verified, but no data is
219+
written. For local stores, writes are silently skipped.
220+
"""
221+
222+
def __init__(self, inner: Store):
223+
self.inner = inner
224+
225+
def read(self, key: str) -> bytes | None:
226+
return self.inner.read(key)
227+
228+
def write(self, key: str, data: bytes) -> None:
229+
with tempfile.NamedTemporaryFile(delete=False) as f:
230+
f.write(data)
231+
tmp_path = f.name
232+
try:
233+
self.write_file(key, Path(tmp_path))
234+
finally:
235+
os.unlink(tmp_path)
236+
237+
def write_file(self, key: str, local_path: Path) -> None:
238+
if isinstance(self.inner, S3Store):
239+
dest = f"s3://{self.inner.bucket}/{key}"
240+
_info(f" {local_path.name} -> {dest} (dry-run)")
241+
subprocess.run(
242+
["aws", "s3", "cp", str(local_path), dest, "--dryrun"],
243+
check=True,
244+
)
245+
else:
246+
_info(f" dry-run: would write {key}")
247+
248+
def list_versions(self) -> list[str]:
249+
return self.inner.list_versions()
250+
251+
def display_name(self) -> str:
252+
return self.inner.display_name()
253+
254+
214255
def _parse_store(spec: str) -> Store:
215256
"""Parse a store specification into a Store instance."""
216257
if spec.startswith("s3://"):
@@ -409,56 +450,23 @@ def _publish_full(
409450
manifest = _merge_manifest(store, fixtures_json, version, prev)
410451
manifest_json = json.dumps(manifest, indent=2) + "\n"
411452

412-
if args.dry_run:
413-
_info(f"dry run — would publish to {store.display_name()}")
414-
existing = _read_manifest(store, version)
415-
if existing:
416-
existing.pop("_prefix", None)
417-
existing_names = {e["name"] for e in existing["fixtures"]}
418-
new_names = {e["name"] for e in manifest["fixtures"]}
419-
added = new_names - existing_names
420-
removed = existing_names - new_names
421-
if added:
422-
_info(f" new fixtures: {', '.join(sorted(added))}")
423-
if removed:
424-
_info(f" removed fixtures: {', '.join(sorted(removed))}")
425-
if not added and not removed:
426-
_info(f" same {len(new_names)} fixtures as existing")
427-
_info(" target paths:")
428-
for entry in manifest["fixtures"]:
429-
_info(f" {store.display_name()}/v{version}/arrays/{entry['name']}")
430-
_info(f" {store.display_name()}/v{version}/arrays/manifest.json")
431-
_info(f" {store.display_name()}/versions.json")
432-
if version not in versions:
433-
updated_versions = sorted(versions + [version], key=_version_sort_key)
434-
_info(f" versions.json would update: {versions} -> {updated_versions}")
435-
else:
436-
_info(f" versions.json unchanged: {versions}")
437-
return
438-
439-
if not args.yes:
440-
_info(f"\nabout to upload {len(manifest['fixtures'])} fixtures for v{version} to {store.display_name()}")
441-
answer = input("proceed? [y/N] ").strip().lower()
442-
if answer not in ("y", "yes"):
443-
_info("aborted")
444-
sys.exit(1)
453+
write_store = _write_store(
454+
store,
455+
args,
456+
f"\nabout to upload {len(manifest['fixtures'])} fixtures for v{version} to {store.display_name()}",
457+
)
445458

446-
_info(f"uploading {len(manifest['fixtures'])} fixtures to {store.display_name()}...")
447459
_parallel_upload(
448-
store,
460+
write_store,
449461
[(f"v{version}/arrays/{e['name']}", output / e["name"]) for e in manifest["fixtures"]],
450462
)
451463

452-
store.write(f"v{version}/arrays/manifest.json", manifest_json.encode())
453-
_info(" uploaded manifest.json")
464+
write_store.write(f"v{version}/arrays/manifest.json", manifest_json.encode())
454465

455466
if version not in versions:
456467
versions.append(version)
457468
versions.sort(key=_version_sort_key)
458-
store.write("versions.json", (json.dumps(versions, indent=2) + "\n").encode())
459-
_info(" updated versions.json")
460-
461-
_info(f"\ndone: {len(manifest['fixtures'])} fixtures for v{version} published to {store.display_name()}")
469+
write_store.write("versions.json", (json.dumps(versions, indent=2) + "\n").encode())
462470

463471

464472
def _publish_update(
@@ -510,24 +518,15 @@ def _publish_update(
510518
_info("no new fixtures to add")
511519
return
512520

513-
if args.dry_run:
514-
_info(f"dry run — would upload {len(new_fixtures)} new fixture(s):")
515-
for name in new_fixtures:
516-
_info(f" {store.display_name()}/{prefix}/{name}")
517-
_info(f" {store.display_name()}/{prefix}/manifest.json (updated)")
518-
return
519-
520-
if not args.yes:
521-
_info(f"\nabout to upload {len(new_fixtures)} new fixture(s) for v{version} to {store.display_name()}")
522-
answer = input("proceed? [y/N] ").strip().lower()
523-
if answer not in ("y", "yes"):
524-
_info("aborted")
525-
sys.exit(1)
521+
write_store = _write_store(
522+
store,
523+
args,
524+
f"\nabout to upload {len(new_fixtures)} new fixture(s) for v{version} to {store.display_name()}",
525+
)
526526

527-
# Upload only new fixture files.
528527
new_fixture_names = set(new_fixtures)
529528
_parallel_upload(
530-
store,
529+
write_store,
531530
[(f"{prefix}/{name}", output / name) for name in new_fixtures],
532531
)
533532

@@ -542,10 +541,7 @@ def _publish_update(
542541
"generated_at": datetime.now(UTC).isoformat(),
543542
"fixtures": new_entries,
544543
}
545-
store.write(f"{prefix}/manifest.json", (json.dumps(updated_manifest, indent=2) + "\n").encode())
546-
_info(" updated manifest.json")
547-
548-
_info(f"\ndone: added {len(new_fixtures)} new fixture(s) to v{version} in {store.display_name()}")
544+
write_store.write(f"{prefix}/manifest.json", (json.dumps(updated_manifest, indent=2) + "\n").encode())
549545

550546

551547
def cmd_check(args: argparse.Namespace) -> None:
@@ -748,6 +744,19 @@ def cmd_validate_manifest(args: argparse.Namespace) -> None:
748744
# ---------------------------------------------------------------------------
749745

750746

747+
def _write_store(store: Store, args: argparse.Namespace, prompt: str) -> Store:
748+
"""Return a DryRunStore wrapper or the real store, with optional confirmation."""
749+
if args.dry_run:
750+
return DryRunStore(store)
751+
if not args.yes:
752+
_info(prompt)
753+
answer = input("proceed? [y/N] ").strip().lower()
754+
if answer not in ("y", "yes"):
755+
_info("aborted")
756+
sys.exit(1)
757+
return store
758+
759+
751760
def _parallel_upload(store: Store, items: list[tuple[str, Path]], max_workers: int = 8) -> None:
752761
"""Upload files to the store in parallel."""
753762
with ThreadPoolExecutor(max_workers=max_workers) as pool:
@@ -944,7 +953,7 @@ def main() -> None:
944953
p.add_argument(
945954
"--dry-run",
946955
action="store_true",
947-
help="Generate and show manifest, but don't upload",
956+
help="Validate credentials and show what would be uploaded, without writing",
948957
)
949958
p.add_argument(
950959
"--force",

0 commit comments

Comments
 (0)