Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .castiron.stats.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
schema_version: 1
generation_id: e5b5d7d9-1b97-4d6a-a1ca-8cd00ff2fa0b
openapi_spec_hash: 72d34e76f62e434dc0b71e0b4ac0ce8a
openapi_transformed_spec_hash: bca4c6179df519f310841c4730b72c18
generation_id: d98efa97-403b-4b82-a984-699b0ca4d6a4
openapi_spec_hash: e856004bcb1faf4b5b91d43290188237
openapi_transformed_spec_hash: 7f2f62b14c75d5cd32be2baa653486ed
config_hash: 8a49d8603252fe24c2dc96f50eb0d05b
codegen_sha: 865a21f405fcf114702b18a248a26dd5ae716da5
codegen_hash: 3b7d96954238e7ffaf3fd7e28623a73939510e435fb58ef4c9a3914be6f857fb
public_codegen_sha: d60ab2223decd81f675989eed4b14fa5363ad9a9
codegen_sha: a670f483c2c742790400f62161f25a0044d74a40
codegen_hash: 2427f947d2dc5d6321110dc0749b2bf4888e2817b4be8818115de07d4097cffc
public_codegen_sha: a281bdfafbc91f0b4bd9980f446e418c89459cb1
10 changes: 7 additions & 3 deletions .github/workflows/castiron-custom-code-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,13 @@ jobs:
if (pr.state === 'open' && pr.head.sha === head &&
['main', 'next'].includes(pr.base.ref) && pr.base.repo.full_name === `${context.repo.owner}/${context.repo.repo}`) current.push(pr);
}
if (current.length !== 1) return;
branch = current[0].base.ref;
number = current[0].number;
// Match compute's preference for required main checks when
// fallback discovery finds an advisory next PR for the same head.
const main = current.filter(pr => pr.base.ref === 'main');
const selected = main.length ? main : current;
if (selected.length !== 1) return;
branch = selected[0].base.ref;
number = selected[0].number;
} else {
const match = run.head_branch.match(/^gh-readonly-queue\/(main|next)\//);
if (run.event !== 'merge_group' || !match) return;
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/castiron-custom-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ concurrency:
cancel-in-progress: false

env:
REPORTER_SHA256: 157ed30ac9d54d20fe5f7e625dc8ee5958beb91cc7e30aaa97cfba8fb3e06e21
REPORTER_SHA256: 4dc056b460d688cbe3035bd9649820f77e489424d0c9c48bd3f19ed4dfb7e9b4

jobs:
queue-signal:
Expand Down
5 changes: 5 additions & 0 deletions api_reference/openapi.transformed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78486,6 +78486,11 @@ components:
- AE_STORAGE
- AE_STORAGE_PROCESSING
UpdateProjectApiKeyBody:
anyOf:
- required:
- scopes
- required:
- expires_in_seconds
properties:
expires_in_seconds:
type: integer
Expand Down
21 changes: 19 additions & 2 deletions scripts/castiron/custom_code_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,18 +452,31 @@ def resolve_checkpoint(
if not isinstance(decoded, dict):
raise ReportError("invalid codegen checkpoint metadata")
metadata = cast(dict[str, Any], decoded)
namespace = ""
if "schema_version" in metadata:
if (
set(metadata) != {"schema_version", "release_target", "record"}
or metadata["schema_version"] != "castiron-checkpoint/v2"
or metadata["release_target"] != "next"
or not isinstance(metadata["record"], dict)
):
raise ReportError("invalid versioned checkpoint envelope")
metadata = cast(dict[str, Any], metadata["record"])
namespace = "castiron/next/"
if {"release_target", "schema_version", "record"}.intersection(metadata):
raise ReportError("checkpoint target metadata must use the versioned envelope")
if metadata.get("generation_id") != stats["generation_id"]:
raise ReportError("codegen checkpoint generation ID does not match stats")
branch = metadata.get("source_branch")
if not isinstance(branch, str) or not branch:
raise ReportError("codegen checkpoint has no source branch")
ref = f"refs/heads/codegen/{branch}"
ref = f"refs/heads/codegen/{namespace}{branch}"
git(repo, "check-ref-format", ref)
tracking = f"refs/castiron/report/{hashlib.sha256(ref.encode()).hexdigest()}"
if fetch:
git(repo, "fetch", "--quiet", "--no-tags", "origin", f"+{ref}:{tracking}")
else:
tracking = f"refs/remotes/origin/codegen/{branch}"
tracking = f"refs/remotes/origin/codegen/{namespace}{branch}"
git(repo, "merge-base", "--is-ancestor", commit, tracking)
actual = hash_codegen_commit(repo, commit)
expected = stats.get("codegen_hash")
Expand Down Expand Up @@ -807,6 +820,10 @@ def current_pull(repository: str, run: dict[str, Any]) -> tuple[int, str] | None
and pull["base"]["ref"] in TARGET_BRANCHES
):
current.append((number, pull["base"]["ref"]))
# Fallback discovery can find both targets for one head. Main's required
# checks take precedence over next's advisory report; keep same-target
# ambiguity fail-closed. Explicit run associations still limit discovery.
current = [pull for pull in current if pull[1] == "main"] or current
if len(current) > 1:
raise ReportError("workflow run has multiple current pull requests")
return current[0] if current else None
Expand Down
2 changes: 1 addition & 1 deletion scripts/castiron/fixtures/github_publisher.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const writes = [];
const github = {
rest: {
actions: { getWorkflowRun: async () => ({ data: data.run }) },
pulls: { get: async () => ({ data: data.current_sequence?.shift() || data.current }), list: 'pulls' },
pulls: { get: async ({ pull_number }) => ({ data: data.current_sequence?.shift() || data.pulls_by_number?.[pull_number] || data.current }), list: 'pulls' },
git: {
getRef: async ({ ref }) => ({
data: {
Expand Down
16 changes: 8 additions & 8 deletions scripts/castiron/generated-files.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
"files": {
".github/workflows/castiron-custom-code-comment.yml": {
"mode": "100644",
"sha256": "02b1c497461fcaf604e1389e7dcab06cfcca01635a8cb2e92f8769245a2aaead"
"sha256": "f0a54911a96ae55a6a9c71ed7e6b259b0da84642dd1716b68e75a7123519728c"
},
".github/workflows/castiron-custom-code.yml": {
"mode": "100644",
"sha256": "ccd67760a8cfcc17ebdf5c8df7e330ee7be5199cb62232afdf9035fea68bbc2f"
"sha256": "867db0c0cb67312a7682236b2ff04f2ccf611ad9e307a2ee650b3a997191f289"
},
"api.md": {
"mode": "100644",
Expand All @@ -27,15 +27,15 @@
},
"scripts/castiron/custom_code_report.py": {
"mode": "100755",
"sha256": "157ed30ac9d54d20fe5f7e625dc8ee5958beb91cc7e30aaa97cfba8fb3e06e21"
"sha256": "4dc056b460d688cbe3035bd9649820f77e489424d0c9c48bd3f19ed4dfb7e9b4"
},
"scripts/castiron/custom_code_test_support.py": {
"mode": "100644",
"sha256": "27a1c5ee377fa601f896191e3914cb74e4bf65ab814819a408e884332b713bde"
},
"scripts/castiron/fixtures/github_publisher.cjs": {
"mode": "100644",
"sha256": "fc80b1aff054f8a2ea16fc6bd9cff5ed4e4a48eebc7f3af8427947796fb8d865"
"sha256": "4f5475e905818875e37d686bf03553b55219ac05e30957628d4aaa834fced202"
},
"scripts/castiron/ruff.toml": {
"mode": "100644",
Expand All @@ -47,15 +47,15 @@
},
"scripts/castiron/test_custom_code_github.py": {
"mode": "100644",
"sha256": "aa170458b1b58523df78073a96ba8c58b75fce03206ff7d62d99b29791c6ca39"
"sha256": "338d54bf6b53a44b555dc7baa778c76af499b06959caf0339142f1b4293edd92"
},
"scripts/castiron/test_custom_code_publication.py": {
"mode": "100644",
"sha256": "99b266761f427d77990e68ce2fdea4b5f1a5b175612b3a9f4355c094939130ca"
"sha256": "1d93757664a6ffd26f21df2a4c07ac635393f5da1ab0283df75bad66faa2ef2e"
},
"scripts/castiron/test_custom_code_report.py": {
"mode": "100644",
"sha256": "56a748c56ccf137aee0e5997dd29484157d5566bf844cc4969270dd26fa38cbd"
"sha256": "8cb4add28b7e6acb4f8b3844073d33e7a95d8a554c4caf47bd1e09da86a3bd04"
},
"src/openai/__init__.py": {
"mode": "100644",
Expand Down Expand Up @@ -7271,7 +7271,7 @@
},
"tests/api_resources/admin/organization/projects/test_api_keys.py": {
"mode": "100644",
"sha256": "1fa9769ccdc65d47c8c17c1e525a69e70ebb6d9248a017eb5fe4de7a337ec8ee"
"sha256": "49c21791da11c49c01c26b40da8e826b37a496d8e209c4b76911be04435d5048"
},
"tests/api_resources/admin/organization/projects/test_certificates.py": {
"mode": "100644",
Expand Down
30 changes: 30 additions & 0 deletions scripts/castiron/test_custom_code_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,36 @@ def __call__(self, method: str, path: str, payload: dict[str, Any] | None = None


class GitHubReportTests(GitTestCase):
def test_same_head_targets_preserve_required_main_discovery(self) -> None:
head = "b" * 40
run = source_run(head, "pull_request")
run["pull_requests"] = []
main: dict[str, Any] = {
"state": "open",
"head": {"sha": head},
"base": {"ref": "main", "repo": {"full_name": REPOSITORY}},
}
next_pull: dict[str, Any] = {**main, "base": {**main["base"], "ref": "next"}}
for numbers in ([3, 4], [4, 3]):

def api_response(_method: str, path: str, numbers: list[int] = numbers) -> Any:
if "/commits/" in path:
return [{"number": number} for number in numbers]
return {"3": main, "4": next_pull}[path.rsplit("/", 1)[1]]

with (
self.subTest(numbers=numbers),
mock.patch.object(report, "api", side_effect=api_response),
):
self.assertEqual(report.current_pull(REPOSITORY, run), (3, "main"))
run["pull_requests"] = [{"number": 4}]
self.assertEqual(report.current_pull(REPOSITORY, run), (4, "next"))
run["pull_requests"] = []
next_pull["base"]["ref"] = "main"
with self.assertRaisesRegex(report.ReportError, "multiple current"):
report.current_pull(REPOSITORY, run)
next_pull["base"]["ref"] = "next"

def test_trusted_report_recomputes_pr_output_in_a_bare_repository(self) -> None:
generated, _ = self.baseline()
content_hash = report.hash_codegen_commit(self.repo, generated)
Expand Down
27 changes: 26 additions & 1 deletion scripts/castiron/test_custom_code_publication.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ def publish(
queue_absent: bool = False,
queue_pages: list[dict[str, Any]] | None = None,
retarget_during_publication: bool = False,
same_head_next_pr: bool = False,
) -> list[dict[str, Any]]:
base, head = "a" * 40, "b" * 40
run = source_run(head, event_name)
Expand Down Expand Up @@ -369,6 +370,12 @@ def publish(
"PUBLISH_ATTEMPT": "1",
},
}
if same_head_next_pr:
current = payload["current"]
payload["pulls_by_number"] = {
3: current,
4: {**current, "number": 4, "base": {**current["base"], "ref": "next"}},
}
if retarget_during_publication:
current = payload["current"]
payload["current_sequence"] = [
Expand Down Expand Up @@ -397,6 +404,22 @@ def test_next_cannot_overwrite_main_results(self) -> None:
result = self.publish(event_name=event, branch="next", previous_statuses=main)
self.assertEqual(result, [])

def test_same_head_next_pr_does_not_block_required_main_statuses(self) -> None:
for numbers in ([3, 4], [4, 3]):
with self.subTest(numbers=numbers):
results = self.publish(
run_overrides={"pull_requests": []},
fallback_pulls=[{"number": number} for number in numbers],
same_head_next_pr=True,
failed_budget=True,
)
self.assertEqual([r["state"] for r in results], ["success", "failure"])
self.assertTrue(all(r["sha"] == "b" * 40 for r in results))
self.assertEqual(
self.publish(run_overrides={"pull_requests": [{"number": 4}]}, same_head_next_pr=True),
[],
)

def test_next_queue_never_authorizes_against_a_stale_main_policy(self) -> None:
for changed in (False, True):
with self.subTest(policy_changed=changed):
Expand All @@ -413,7 +436,9 @@ def test_main_statuses_bind_target_policy_and_queue_membership(self) -> None:
for policy_changed, queue_absent in ((True, False), (False, True)):
with self.subTest(policy_changed=policy_changed, queue_absent=queue_absent):
result = self.publish(
event_name="merge_group", policy_changed=policy_changed, queue_absent=queue_absent
event_name="merge_group",
policy_changed=policy_changed,
queue_absent=queue_absent,
)
self.assertEqual([r["state"] for r in result], ["failure", "failure"])
self.assertEqual(self.publish(retarget_during_publication=True), [])
Expand Down
45 changes: 45 additions & 0 deletions scripts/castiron/test_custom_code_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,51 @@ def test_stats_symlink_is_rejected(self) -> None:
with self.assertRaisesRegex(report.ReportError, "regular Git file"):
report.read_stats(self.repo, self.commit())

def test_next_checkpoint_uses_its_namespaced_ref(self) -> None:
generated, base = self.baseline()
self.git("checkout", "--detach", generated)
metadata = {
"schema_version": "castiron-checkpoint/v2",
"release_target": "next",
"record": {"generation_id": GENERATION, "source_branch": "test"},
}
encoded = base64.b64encode(json.dumps(metadata).encode()).decode()
checkpoint = self.commit(f"codegen\n\nGeneration metadata: {encoded}")
ref = "refs/remotes/origin/codegen/castiron/next/test"
self.git("update-ref", ref, checkpoint)
stats = report.read_stats(self.repo, base) | {
"codegen_sha": checkpoint,
"codegen_hash": report.hash_codegen_commit(self.repo, checkpoint),
}
result = report.resolve_checkpoint(self.repo, stats, fetch=False, require_hash=True)
self.assertEqual(result["commit"], checkpoint)
self.git("update-ref", "refs/remotes/origin/codegen/test", checkpoint)
self.git("update-ref", "-d", ref)
with self.assertRaises(report.ReportError):
report.resolve_checkpoint(self.repo, stats, fetch=False, require_hash=True)

def test_invalid_checkpoint_envelopes_are_rejected(self) -> None:
_, base = self.baseline()
record = {"generation_id": GENERATION, "source_branch": "test"}
envelope = {
"schema_version": "castiron-checkpoint/v2",
"release_target": "next",
"record": record,
}
for metadata in (
envelope | {"schema_version": "unknown"},
envelope | {"release_target": "main"},
envelope | {"extra": True},
envelope | {"record": record | {"release_target": "next"}},
record | {"release_target": "next"},
):
with self.subTest(metadata=metadata):
encoded = base64.b64encode(json.dumps(metadata).encode()).decode()
checkpoint = self.commit(f"codegen\n\nGeneration metadata: {encoded}")
stats = report.read_stats(self.repo, base) | {"codegen_sha": checkpoint}
with self.assertRaisesRegex(report.ReportError, "envelope"):
report.resolve_checkpoint(self.repo, stats, fetch=False, require_hash=True)

def test_public_snapshot_rejects_private_governance_and_unpublished_paths(self) -> None:
entry = report.Entry(b"100644", "a" * 40)
blocked = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def test_method_update(self, client: OpenAI) -> None:
api_key = client.admin.organization.projects.api_keys.update(
api_key_id="api_key_id",
project_id="project_id",
scopes=["api.all.read"],
)
assert_matches_type(ProjectAPIKey, api_key, path=["response"])

Expand All @@ -92,6 +93,7 @@ def test_raw_response_update(self, client: OpenAI) -> None:
response = client.admin.organization.projects.api_keys.with_raw_response.update(
api_key_id="api_key_id",
project_id="project_id",
scopes=["api.all.read"],
)

assert response.is_closed is True
Expand All @@ -104,6 +106,7 @@ def test_streaming_response_update(self, client: OpenAI) -> None:
with client.admin.organization.projects.api_keys.with_streaming_response.update(
api_key_id="api_key_id",
project_id="project_id",
scopes=["api.all.read"],
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
Expand All @@ -119,12 +122,14 @@ def test_path_params_update(self, client: OpenAI) -> None:
client.admin.organization.projects.api_keys.with_raw_response.update(
api_key_id="api_key_id",
project_id="",
scopes=["api.all.read"],
)

with pytest.raises(ValueError, match=r"Expected a non-empty value for `api_key_id` but received ''"):
client.admin.organization.projects.api_keys.with_raw_response.update(
api_key_id="",
project_id="project_id",
scopes=["api.all.read"],
)

@parametrize
Expand Down Expand Up @@ -282,6 +287,7 @@ async def test_method_update(self, async_client: AsyncOpenAI) -> None:
api_key = await async_client.admin.organization.projects.api_keys.update(
api_key_id="api_key_id",
project_id="project_id",
scopes=["api.all.read"],
)
assert_matches_type(ProjectAPIKey, api_key, path=["response"])

Expand All @@ -300,6 +306,7 @@ async def test_raw_response_update(self, async_client: AsyncOpenAI) -> None:
response = await async_client.admin.organization.projects.api_keys.with_raw_response.update(
api_key_id="api_key_id",
project_id="project_id",
scopes=["api.all.read"],
)

assert response.is_closed is True
Expand All @@ -312,6 +319,7 @@ async def test_streaming_response_update(self, async_client: AsyncOpenAI) -> Non
async with async_client.admin.organization.projects.api_keys.with_streaming_response.update(
api_key_id="api_key_id",
project_id="project_id",
scopes=["api.all.read"],
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
Expand All @@ -327,12 +335,14 @@ async def test_path_params_update(self, async_client: AsyncOpenAI) -> None:
await async_client.admin.organization.projects.api_keys.with_raw_response.update(
api_key_id="api_key_id",
project_id="",
scopes=["api.all.read"],
)

with pytest.raises(ValueError, match=r"Expected a non-empty value for `api_key_id` but received ''"):
await async_client.admin.organization.projects.api_keys.with_raw_response.update(
api_key_id="",
project_id="project_id",
scopes=["api.all.read"],
)

@parametrize
Expand Down
Loading