Merge origin/main into the consolidated WS3/WS4 branch

This commit is contained in:
BenKurrek
2026-08-04 08:05:22 -04:00
4 changed files with 483 additions and 1 deletions

View File

@@ -0,0 +1,62 @@
name: Cut Ironclaw Release
on:
workflow_dispatch:
inputs:
version:
description: "Cargo version to release (for example, 1.1.0-rc.1)"
required: true
type: string
commit_sha:
description: "Full commit SHA approved for this immutable release"
required: true
type: string
permissions:
contents: read
concurrency:
group: cut-ironclaw-${{ inputs.version }}
cancel-in-progress: false
jobs:
tag:
name: Tag approved commit
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
runs-on: ubuntu-24.04
environment: ironclaw-release
steps:
- name: Checkout release tooling
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
ref: ${{ github.sha }}
path: release-tools
persist-credentials: false
- name: Checkout approved commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
ref: ${{ inputs.commit_sha }}
path: candidate
persist-credentials: false
- name: Install Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v6
with:
python-version: "3.12"
- name: Generate GitHub token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: generate-token
with:
app-id: ${{ secrets.GH_RELEASES_MANAGER_APP_ID }}
private-key: ${{ secrets.GH_RELEASES_MANAGER_APP_PRIVATE_KEY }}
- name: Create immutable cargo-dist tag
env:
COMMIT_SHA: ${{ inputs.commit_sha }}
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
RELEASE_VERSION: ${{ inputs.version }}
REPOSITORY: ${{ github.repository }}
run: |
python3 release-tools/scripts/ci/cut_ironclaw_release.py \
--version "$RELEASE_VERSION" \
--commit-sha "$COMMIT_SHA" \
--candidate-root candidate \
--repository "$REPOSITORY"

View File

@@ -36,7 +36,9 @@ owner, and deployment approver can synchronize on a predictable window.
artifact to a stable, production-like RC environment. The `ironclaw-`
namespace matches the tag-only cargo-dist publisher in
`.github/workflows/ironclaw-release.yml`; a tag without that namespace never
invokes the publisher.
invokes the publisher. The release owner creates that tag with the
`Cut Ironclaw Release` workflow, supplying the exact approved commit SHA and
matching Cargo version.
4. Freeze the release branch after the cut. Do not add features, merge `main`
into it, or rebase it. The approximately 30 PRs per day that continue landing
on `main` are for the next release.

View File

@@ -0,0 +1,188 @@
"""Create the cargo-dist tag for an explicitly approved Ironclaw commit."""
from __future__ import annotations
import argparse
import json
import re
import subprocess
from collections.abc import Callable
from pathlib import Path
import tomllib
NUMERIC_IDENTIFIER = r"(?:0|[1-9][0-9]*)"
PRERELEASE_IDENTIFIER = r"(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)"
# This intentionally excludes Cargo build metadata: the same release publishes a
# Docker image, whose tag grammar does not permit '+'.
VERSION_PATTERN = re.compile(
rf"{NUMERIC_IDENTIFIER}\.{NUMERIC_IDENTIFIER}\.{NUMERIC_IDENTIFIER}"
rf"(?:-{PRERELEASE_IDENTIFIER}(?:\.{PRERELEASE_IDENTIFIER})*)?"
)
SHA_PATTERN = re.compile(r"[0-9a-f]{40}")
MAX_ANNOTATED_TAG_DEPTH = 8
class ReleaseTagError(RuntimeError):
"""The requested immutable release tag is unsafe or invalid."""
def ensure_release_tag(
*,
requested_version: str,
requested_sha: str,
manifest_version: str,
checked_out_sha: str,
get_tag_target: Callable[[str], str | None],
create_tag: Callable[[str, str], None],
) -> str:
"""Validate release identity and create its tag exactly once."""
if VERSION_PATTERN.fullmatch(requested_version) is None:
raise ReleaseTagError(f"invalid release version: {requested_version!r}")
if SHA_PATTERN.fullmatch(requested_sha) is None:
raise ReleaseTagError("commit_sha must be a full lowercase commit SHA")
if checked_out_sha != requested_sha:
raise ReleaseTagError(
f"checked out {checked_out_sha}, expected approved commit {requested_sha}"
)
if manifest_version != requested_version:
raise ReleaseTagError(
f"approved commit declares version {manifest_version}, "
f"not requested version {requested_version}"
)
tag = f"ironclaw-v{requested_version}"
existing_target = get_tag_target(tag)
if existing_target is not None:
if existing_target != requested_sha:
raise ReleaseTagError(
f"{tag} already points to {existing_target}, not {requested_sha}"
)
return f"{tag} already points to approved commit {requested_sha}"
try:
create_tag(tag, requested_sha)
except ReleaseTagError:
# A retried or concurrently dispatched run is safe only when it created
# the exact same immutable mapping.
if get_tag_target(tag) != requested_sha:
raise
return f"{tag} was concurrently created at approved commit {requested_sha}"
return f"created {tag} at approved commit {requested_sha}"
class GitHubTags:
def __init__(self, repository: str) -> None:
self.repository = repository
def get_target(self, tag: str) -> str | None:
result = subprocess.run(
["gh", "api", f"repos/{self.repository}/git/ref/tags/{tag}"],
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
if "HTTP 404" in result.stderr:
return None
raise ReleaseTagError(f"failed to read {tag}: {result.stderr.strip()}")
target = json.loads(result.stdout)["object"]
for depth in range(MAX_ANNOTATED_TAG_DEPTH + 1):
object_type = str(target["type"])
object_sha = str(target["sha"])
if object_type == "commit":
return object_sha
if object_type != "tag":
raise ReleaseTagError(
f"{tag} resolves to unsupported Git object type {object_type!r}"
)
if depth == MAX_ANNOTATED_TAG_DEPTH:
break
result = subprocess.run(
[
"gh",
"api",
f"repos/{self.repository}/git/tags/{object_sha}",
],
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
raise ReleaseTagError(
f"failed to resolve annotated {tag}: {result.stderr.strip()}"
)
target = json.loads(result.stdout)["object"]
raise ReleaseTagError(f"{tag} exceeds the annotated-tag resolution depth limit")
def create(self, tag: str, commit_sha: str) -> None:
result = subprocess.run(
[
"gh",
"api",
"--method",
"POST",
f"repos/{self.repository}/git/refs",
"-f",
f"ref=refs/tags/{tag}",
"-f",
f"sha={commit_sha}",
],
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
raise ReleaseTagError(f"failed to create {tag}: {result.stderr.strip()}")
def _checked_out_sha(candidate_root: Path) -> str:
return subprocess.run(
["git", "rev-parse", "HEAD^{commit}"],
cwd=candidate_root,
check=True,
capture_output=True,
text=True,
).stdout.strip()
def _manifest_version(candidate_root: Path) -> str:
manifest = candidate_root / "crates/ironclaw_reborn_cli/Cargo.toml"
with manifest.open("rb") as manifest_file:
return str(tomllib.load(manifest_file)["package"]["version"])
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--version", required=True)
parser.add_argument("--commit-sha", required=True)
parser.add_argument("--candidate-root", required=True, type=Path)
parser.add_argument("--repository", required=True)
args = parser.parse_args()
tags = GitHubTags(args.repository)
message = ensure_release_tag(
requested_version=args.version,
requested_sha=args.commit_sha,
manifest_version=_manifest_version(args.candidate_root),
checked_out_sha=_checked_out_sha(args.candidate_root),
get_tag_target=tags.get_target,
create_tag=tags.create,
)
print(message)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (
ReleaseTagError,
subprocess.CalledProcessError,
KeyError,
ValueError,
) as error:
raise SystemExit(f"error: {error}") from error

View File

@@ -0,0 +1,230 @@
"""Behavioral tests for immutable Ironclaw release tag creation."""
from __future__ import annotations
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[2]
MODULE_PATH = ROOT / "scripts/ci/cut_ironclaw_release.py"
SPEC = importlib.util.spec_from_file_location("cut_ironclaw_release", MODULE_PATH)
assert SPEC is not None and SPEC.loader is not None
release = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = release
SPEC.loader.exec_module(release)
VERSION = "1.1.0-rc.1"
SHA = "a" * 40
class ReleaseTagTests(unittest.TestCase):
def run_release(
self, *, targets: list[str | None], create_error: bool = False
) -> tuple[str, list[tuple[str, str]]]:
lookups = iter(targets)
created: list[tuple[str, str]] = []
def create(tag: str, commit_sha: str) -> None:
created.append((tag, commit_sha))
if create_error:
raise release.ReleaseTagError("create failed")
message = release.ensure_release_tag(
requested_version=VERSION,
requested_sha=SHA,
manifest_version=VERSION,
checked_out_sha=SHA,
get_tag_target=lambda _tag: next(lookups),
create_tag=create,
)
return message, created
def test_creates_tag_for_exact_version_and_commit(self) -> None:
message, created = self.run_release(targets=[None])
self.assertEqual(created, [(f"ironclaw-v{VERSION}", SHA)])
self.assertIn("created", message)
def test_existing_tag_is_idempotent_only_at_approved_commit(self) -> None:
message, created = self.run_release(targets=[SHA])
self.assertEqual(created, [])
self.assertIn("already points", message)
with self.assertRaisesRegex(release.ReleaseTagError, "already points"):
self.run_release(targets=["b" * 40])
def test_create_race_is_safe_only_at_approved_commit(self) -> None:
message, created = self.run_release(targets=[None, SHA], create_error=True)
self.assertEqual(created, [(f"ironclaw-v{VERSION}", SHA)])
self.assertIn("concurrently created", message)
with self.assertRaisesRegex(release.ReleaseTagError, "create failed"):
self.run_release(targets=[None, "b" * 40], create_error=True)
def test_rejects_mismatched_checkout_and_manifest(self) -> None:
for field, value, message in (
("checked_out_sha", "b" * 40, "checked out"),
("manifest_version", "1.1.0-rc.2", "declares version"),
):
created: list[tuple[str, str]] = []
arguments = {
"requested_version": VERSION,
"requested_sha": SHA,
"manifest_version": VERSION,
"checked_out_sha": SHA,
"get_tag_target": lambda _tag: None,
"create_tag": lambda tag, sha, calls=created: calls.append((tag, sha)),
}
arguments[field] = value
with (
self.subTest(field=field),
self.assertRaisesRegex(release.ReleaseTagError, message),
):
release.ensure_release_tag(**arguments)
self.assertEqual(created, [])
def test_rejects_ambiguous_release_identity(self) -> None:
cases = (
{"requested_version": "v1.1.0", "requested_sha": SHA},
{"requested_version": "01.1.0", "requested_sha": SHA},
{"requested_version": "1.01.0", "requested_sha": SHA},
{"requested_version": "1.1.01", "requested_sha": SHA},
{"requested_version": "1.1.0-01", "requested_sha": SHA},
{"requested_version": "1.1.0-rc..1", "requested_sha": SHA},
# Docker release tags cannot contain Cargo build metadata.
{"requested_version": "1.1.0+build.7", "requested_sha": SHA},
{"requested_version": VERSION, "requested_sha": "abc123"},
)
for overrides in cases:
created: list[tuple[str, str]] = []
arguments = {
"requested_version": VERSION,
"requested_sha": SHA,
"manifest_version": VERSION,
"checked_out_sha": SHA,
"get_tag_target": lambda _tag: None,
"create_tag": lambda tag, sha, calls=created: calls.append((tag, sha)),
}
arguments.update(overrides)
if "requested_version" in overrides:
arguments["manifest_version"] = overrides["requested_version"]
with (
self.subTest(overrides=overrides),
self.assertRaisesRegex(release.ReleaseTagError, "invalid|commit_sha"),
):
release.ensure_release_tag(**arguments)
self.assertEqual(created, [])
def test_accepts_valid_semver_prerelease_identifiers(self) -> None:
for version in ("1.1.0-0", "1.1.0-rc.1", "1.1.0-01a"):
with self.subTest(version=version):
created: list[tuple[str, str]] = []
release.ensure_release_tag(
requested_version=version,
requested_sha=SHA,
manifest_version=version,
checked_out_sha=SHA,
get_tag_target=lambda _tag: None,
create_tag=lambda tag, sha, calls=created: calls.append((tag, sha)),
)
self.assertEqual(created, [(f"ironclaw-v{version}", SHA)])
def test_annotated_tag_is_resolved_to_its_commit(self) -> None:
tag_object_sha = "b" * 40
responses = (
mock.Mock(
returncode=0,
stdout=(f'{{"object":{{"type":"tag","sha":"{tag_object_sha}"}}}}'),
stderr="",
),
mock.Mock(
returncode=0,
stdout=(f'{{"object":{{"type":"commit","sha":"{SHA}"}}}}'),
stderr="",
),
)
with mock.patch.object(release.subprocess, "run", side_effect=responses) as run:
target = release.GitHubTags("nearai/ironclaw").get_target("ironclaw-v1.0.0")
self.assertEqual(target, SHA)
self.assertIn(f"git/tags/{tag_object_sha}", run.call_args_list[1].args[0][2])
def test_accepts_exact_annotated_tag_resolution_limit(self) -> None:
responses = [
mock.Mock(
returncode=0,
stdout=f'{{"object":{{"type":"tag","sha":"{index:x}{"0" * 39}"}}}}',
stderr="",
)
for index in range(1, release.MAX_ANNOTATED_TAG_DEPTH + 1)
]
responses.append(
mock.Mock(
returncode=0,
stdout=f'{{"object":{{"type":"commit","sha":"{SHA}"}}}}',
stderr="",
)
)
with mock.patch.object(release.subprocess, "run", side_effect=responses):
target = release.GitHubTags("nearai/ironclaw").get_target("ironclaw-v1.0.0")
self.assertEqual(target, SHA)
def test_rejects_annotated_tag_beyond_resolution_limit(self) -> None:
responses = [
mock.Mock(
returncode=0,
stdout=f'{{"object":{{"type":"tag","sha":"{index:x}{"0" * 39}"}}}}',
stderr="",
)
for index in range(1, release.MAX_ANNOTATED_TAG_DEPTH + 2)
]
with (
mock.patch.object(release.subprocess, "run", side_effect=responses) as run,
self.assertRaisesRegex(release.ReleaseTagError, "resolution depth"),
):
release.GitHubTags("nearai/ironclaw").get_target("ironclaw-v1.0.0")
self.assertEqual(run.call_count, release.MAX_ANNOTATED_TAG_DEPTH + 1)
def test_release_tooling_is_pinned_to_default_branch_dispatch(self) -> None:
workflow = (ROOT / ".github/workflows/cut-ironclaw-release.yml").read_text(
encoding="utf-8"
)
self.assertIn(
"if: github.ref == format('refs/heads/{0}', "
"github.event.repository.default_branch)",
workflow,
)
self.assertIn("ref: ${{ github.sha }}", workflow)
self.assertIn(
"python3 release-tools/scripts/ci/cut_ironclaw_release.py", workflow
)
self.assertNotIn("candidate/scripts/ci/cut_ironclaw_release.py", workflow)
def test_candidate_metadata_comes_from_supplied_checkout(self) -> None:
with tempfile.TemporaryDirectory() as directory:
candidate_root = Path(directory)
manifest = candidate_root / "crates/ironclaw_reborn_cli/Cargo.toml"
manifest.parent.mkdir(parents=True)
manifest.write_text(
f'[package]\nname = "ironclaw"\nversion = "{VERSION}"\n',
encoding="utf-8",
)
self.assertEqual(release._manifest_version(candidate_root), VERSION)
completed = mock.Mock(stdout=f"{SHA}\n")
with mock.patch.object(
release.subprocess, "run", return_value=completed
) as run:
self.assertEqual(release._checked_out_sha(candidate_root), SHA)
self.assertEqual(run.call_args.kwargs["cwd"], candidate_root)
if __name__ == "__main__":
unittest.main()