From 553b1f839eff6ae8fca2300fef8454357a2b33c5 Mon Sep 17 00:00:00 2001 From: Joe Lombrozo Date: Fri, 17 Apr 2026 17:10:24 -0700 Subject: [PATCH 1/6] clean up --- .github/workflows/release.yml | 23 ++++++++-- scripts/test_validate.py | 82 +++++++++++++---------------------- scripts/validate.py | 71 ++++-------------------------- 3 files changed, 56 insertions(+), 120 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0db99b577..804ed9d8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,6 @@ jobs: version_name: ${{ steps.validate.outputs.version_name }} commit_hash: ${{ steps.validate.outputs.commit_hash }} build_matrix: ${{ steps.validate.outputs.build_matrix }} - skip_build: ${{ steps.validate.outputs.skip_build }} steps: - uses: actions/checkout@v6 @@ -59,7 +58,6 @@ jobs: build: needs: validate - if: needs.validate.outputs.skip_build != 'true' strategy: fail-fast: false matrix: ${{ fromJson(needs.validate.outputs.build_matrix) }} @@ -67,10 +65,29 @@ jobs: steps: - uses: actions/checkout@v6 + - name: Check if artifact exists in release + id: check_release + env: + GH_TOKEN: ${{ github.token }} + run: | + version_name="${{ needs.validate.outputs.version_name }}" + arch="${{ matrix.arch }}" + asset_name="firecracker-${arch}" + + if gh release view "$version_name" --json assets -q ".assets[].name" 2>/dev/null | grep -q "^${asset_name}$"; then + echo "Release: $arch artifact already exists, skipping build" + echo "skip=true" >> $GITHUB_OUTPUT + else + echo "Release: $arch artifact missing, will build" + echo "skip=false" >> $GITHUB_OUTPUT + fi + - name: Build Firecracker ${{ needs.validate.outputs.version_name }} (${{ matrix.arch }}) + if: steps.check_release.outputs.skip != 'true' run: ./build.sh "${{ needs.validate.outputs.commit_hash }}" "${{ needs.validate.outputs.version_name }}" "${{ matrix.arch }}" - name: Upload build artifact + if: steps.check_release.outputs.skip != 'true' uses: actions/upload-artifact@v7 with: name: firecracker-${{ needs.validate.outputs.version_name }}-${{ matrix.arch }} @@ -79,7 +96,6 @@ jobs: publish: needs: [validate, build] - if: needs.validate.outputs.skip_build != 'true' runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 @@ -165,7 +181,6 @@ jobs: deploy: needs: [validate, publish] - if: needs.validate.outputs.skip_build != 'true' strategy: fail-fast: false matrix: diff --git a/scripts/test_validate.py b/scripts/test_validate.py index d396d666a..449f7f910 100644 --- a/scripts/test_validate.py +++ b/scripts/test_validate.py @@ -14,7 +14,7 @@ find_tag_for_commit, resolve_tag_and_commit, check_ci_status, - check_existing_artifacts, + generate_build_matrix, gh_api, ) @@ -354,55 +354,31 @@ def test_ci_skipped_checks_count_as_success(self): assert success is True -class TestCheckExistingArtifacts: - """Tests for check_existing_artifacts function.""" - - def test_all_artifacts_exist_skip_build(self): - """Test when all artifacts exist in release.""" - with patch("validate.check_release_artifacts", return_value={"firecracker-amd64", "firecracker-arm64"}): - matrix, skip = check_existing_artifacts( - "v1.0.0_abc1234", True, True, "owner/repo" - ) - assert skip is True - assert matrix == {"include": []} - - def test_no_artifacts_exist_build_both(self): - """Test when no artifacts exist.""" - with patch("validate.check_release_artifacts", return_value=set()): - matrix, skip = check_existing_artifacts( - "v1.0.0_abc1234", True, True, "owner/repo" - ) - assert skip is False - assert len(matrix["include"]) == 2 - archs = {item["arch"] for item in matrix["include"]} - assert archs == {"amd64", "arm64"} - - def test_only_amd64_missing(self): - """Test when only amd64 is missing.""" - with patch("validate.check_release_artifacts", return_value={"firecracker-arm64"}): - matrix, skip = check_existing_artifacts( - "v1.0.0_abc1234", True, True, "owner/repo" - ) - assert skip is False - assert len(matrix["include"]) == 1 - assert matrix["include"][0]["arch"] == "amd64" - - def test_only_arm64_requested_and_missing(self): - """Test when only arm64 is requested and missing.""" - with patch("validate.check_release_artifacts", return_value=set()): - matrix, skip = check_existing_artifacts( - "v1.0.0_abc1234", False, True, "owner/repo" - ) - assert skip is False - assert len(matrix["include"]) == 1 - assert matrix["include"][0]["arch"] == "arm64" - assert matrix["include"][0]["runner"] == "ubuntu-24.04-arm" - - def test_amd64_exists_in_release(self): - """Test when amd64 exists in release (skip build for amd64).""" - with patch("validate.check_release_artifacts", return_value={"firecracker-amd64"}): - matrix, skip = check_existing_artifacts( - "v1.0.0_abc1234", True, False, "owner/repo" - ) - assert skip is True - assert matrix == {"include": []} +class TestGenerateBuildMatrix: + """Tests for generate_build_matrix function.""" + + def test_build_both_architectures(self): + """Test generating matrix for both architectures.""" + matrix = generate_build_matrix(True, True) + assert len(matrix["include"]) == 2 + archs = {item["arch"] for item in matrix["include"]} + assert archs == {"amd64", "arm64"} + + def test_build_amd64_only(self): + """Test generating matrix for amd64 only.""" + matrix = generate_build_matrix(True, False) + assert len(matrix["include"]) == 1 + assert matrix["include"][0]["arch"] == "amd64" + assert matrix["include"][0]["runner"] == "ubuntu-24.04" + + def test_build_arm64_only(self): + """Test generating matrix for arm64 only.""" + matrix = generate_build_matrix(False, True) + assert len(matrix["include"]) == 1 + assert matrix["include"][0]["arch"] == "arm64" + assert matrix["include"][0]["runner"] == "ubuntu-24.04-arm" + + def test_build_neither_architecture(self): + """Test generating empty matrix when no architectures requested.""" + matrix = generate_build_matrix(False, False) + assert matrix == {"include": []} diff --git a/scripts/validate.py b/scripts/validate.py index c85ad8747..c993cce90 100755 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -208,68 +208,19 @@ def check_ci_status(commit_hash: str, repo: str = "e2b-dev/firecracker") -> tupl return True, f"Could not definitively verify CI status (status={status}, check_conclusion={check_conclusion}) - proceeding anyway" -def check_release_artifacts(github_repo: str, version_name: str) -> set[str]: - """Get the set of artifact names in a GitHub release.""" - result = run_command([ - "gh", "release", "view", version_name, - "--repo", github_repo, - "--json", "assets", - "-q", ".assets[].name" - ], check=False) - - if result.returncode != 0: - return set() - - return set(result.stdout.strip().split("\n")) if result.stdout.strip() else set() - - -def check_existing_artifacts( - version_name: str, - build_amd64: bool, - build_arm64: bool, - github_repo: str -) -> tuple[dict, bool]: +def generate_build_matrix(build_amd64: bool, build_arm64: bool) -> dict: """ - Check existing artifacts and generate build matrix. + Generate build matrix for all requested architectures. - Returns (build_matrix, skip_build). + Build and deploy jobs always run; individual steps check for existing artifacts. """ - need_amd64 = False - need_arm64 = False - - release_assets = check_release_artifacts(github_repo, version_name) - - for arch, requested in [("amd64", build_amd64), ("arm64", build_arm64)]: - if not requested: - continue - - release_exists = f"firecracker-{arch}" in release_assets - - print(f"Release: {arch} artifact {'exists' if release_exists else 'missing'}", file=sys.stderr) - - if not release_exists: - if arch == "amd64": - need_amd64 = True - else: - need_arm64 = True - - if not need_amd64 and not need_arm64: - print("", file=sys.stderr) - print("==============================================", file=sys.stderr) - print("SKIPPING BUILD: All requested artifacts already exist", file=sys.stderr) - print("==============================================", file=sys.stderr) - print("", file=sys.stderr) - print("::notice::Skipped build - all requested artifacts already exist in GitHub release", file=sys.stderr) - return {"include": []}, True - - # Generate build matrix include = [] - if need_amd64: + if build_amd64: include.append({"arch": "amd64", "runner": "ubuntu-24.04"}) - if need_arm64: + if build_arm64: include.append({"arch": "arm64", "runner": "ubuntu-24.04-arm"}) - return {"include": include}, False + return {"include": include} def write_github_output(outputs: dict[str, str]) -> None: @@ -334,13 +285,8 @@ def main() -> int: return 1 print(ci_message, file=sys.stderr) - # Step 4: Check existing artifacts and generate build matrix - build_matrix, skip_build = check_existing_artifacts( - version_name, - args.build_amd64, - args.build_arm64, - args.github_repo - ) + # Step 4: Generate build matrix for all requested architectures + build_matrix = generate_build_matrix(args.build_amd64, args.build_arm64) print(f"Build matrix: {json.dumps(build_matrix)}", file=sys.stderr) @@ -349,7 +295,6 @@ def main() -> int: "commit_hash": commit_hash, "version_name": version_name, "build_matrix": json.dumps(build_matrix), - "skip_build": "true" if skip_build else "false" }) return 0 From 3da9fc54500c6bf5bbbfc32e2a8e808dc86031cd Mon Sep 17 00:00:00 2001 From: Joe Lombrozo Date: Fri, 17 Apr 2026 17:22:48 -0700 Subject: [PATCH 2/6] use vars, not secrets --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 804ed9d8c..7fea01c24 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -191,8 +191,8 @@ jobs: - name: Setup GCS credentials uses: google-github-actions/auth@v3 with: - project_id: ${{ secrets.GCP_PROJECT_ID }} - workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + project_id: ${{ vars.GCP_PROJECT_ID }} + workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} - name: Download release assets env: From 2a638dfac28afef8f0455a61c78f660351b7d6d5 Mon Sep 17 00:00:00 2001 From: Joe Lombrozo Date: Fri, 17 Apr 2026 23:40:52 -0700 Subject: [PATCH 3/6] set service_account as well as the rest --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7fea01c24..50fe60761 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -193,6 +193,7 @@ jobs: with: project_id: ${{ vars.GCP_PROJECT_ID }} workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} - name: Download release assets env: From acbd117b61f56e7dc0e47f28eb4205ee9de46de0 Mon Sep 17 00:00:00 2001 From: Joe Lombrozo Date: Wed, 6 May 2026 10:11:26 -0700 Subject: [PATCH 4/6] add some whitespace --- .github/workflows/release.yml | 2 ++ README.md | 7 +++++-- scripts/validate.py | 2 -- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 50fe60761..13207ebaf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,6 +56,7 @@ jobs: --build-amd64 "${{ inputs.build_amd64 }}" \ --build-arm64 "${{ inputs.build_arm64 }}" + build: needs: validate strategy: @@ -179,6 +180,7 @@ jobs: echo "Release URL: https://github.com/${{ github.repository }}/releases/tag/$version_name" + deploy: needs: [validate, publish] strategy: diff --git a/README.md b/README.md index 6b402c65e..bfe0c593c 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,12 @@ This project automates the building of custom Firecracker versions. It supports - Linux environment (for building firecracker) -## Building Firecrackers +## Releasing Firecrackers -Run the `release.yml` GitHub Actions workflow (Actions → Manual Build & Release → Run workflow) to build and upload Firecracker binaries. +1. Run the `release.yml` GitHub Actions workflow (Actions → Manual Build & Release → Run workflow) to build and upload Firecracker binaries. +2. Create a new variation for the `firecracker-versions` feature flag to point a subset of users to the new firecracker version. +3. After testing, promote the new variation to all users. +4. Once the new variation is confirmed to be stable, update the DefaultFirecrackerV___Version constant in github.com/e2b-dev/infra, packages/shared/pkg/featureflags/flags.go. ### Workflow Inputs diff --git a/scripts/validate.py b/scripts/validate.py index c993cce90..5d942df88 100755 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -244,8 +244,6 @@ def main() -> int: help="Build for amd64 architecture") parser.add_argument("--build-arm64", type=lambda x: x.lower() == "true", default=True, help="Build for arm64 architecture") - parser.add_argument("--github-repo", default=os.environ.get("GITHUB_REPOSITORY", ""), - help="GitHub repository (owner/repo)") args = parser.parse_args() From 5e215e60823f714d569f3f8a691a56fca32d1a23 Mon Sep 17 00:00:00 2001 From: Joe Lombrozo Date: Wed, 6 May 2026 10:15:40 -0700 Subject: [PATCH 5/6] more complexity to prevent failures --- .github/workflows/release.yml | 3 +++ scripts/validate.py | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13207ebaf..b04d07a24 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,7 @@ jobs: version_name: ${{ steps.validate.outputs.version_name }} commit_hash: ${{ steps.validate.outputs.commit_hash }} build_matrix: ${{ steps.validate.outputs.build_matrix }} + has_new_artifacts: ${{ steps.validate.outputs.has_new_artifacts }} steps: - uses: actions/checkout@v6 @@ -104,12 +105,14 @@ jobs: fetch-depth: 0 - name: Download all build artifacts + if: needs.validate.outputs.has_new_artifacts == 'true' uses: actions/download-artifact@v8 with: path: ./builds merge-multiple: true - name: Display build artifacts + if: needs.validate.outputs.has_new_artifacts == 'true' run: | echo "Build artifacts:" find ./builds -type f | head -50 diff --git a/scripts/validate.py b/scripts/validate.py index 5d942df88..e67568d3c 100755 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -208,6 +208,42 @@ def check_ci_status(commit_hash: str, repo: str = "e2b-dev/firecracker") -> tupl return True, f"Could not definitively verify CI status (status={status}, check_conclusion={check_conclusion}) - proceeding anyway" +def get_existing_release_assets(version_name: str) -> set[str]: + """ + Get the set of existing asset names for a release. + + Returns empty set if release doesn't exist. + """ + repo = os.environ.get("GITHUB_REPOSITORY", "") + if not repo: + return set() + + result = run_command( + ["gh", "release", "view", version_name, "--json", "assets", "-q", ".assets[].name"], + check=False + ) + if result.returncode != 0: + return set() + + return set(result.stdout.strip().split("\n")) if result.stdout.strip() else set() + + +def check_artifacts_needed(version_name: str, build_amd64: bool, build_arm64: bool) -> bool: + """ + Check if any requested architectures are missing from the release. + + Returns True if at least one artifact needs to be built and uploaded. + """ + existing_assets = get_existing_release_assets(version_name) + + if build_amd64 and "firecracker-amd64" not in existing_assets: + return True + if build_arm64 and "firecracker-arm64" not in existing_assets: + return True + + return False + + def generate_build_matrix(build_amd64: bool, build_arm64: bool) -> dict: """ Generate build matrix for all requested architectures. @@ -288,11 +324,16 @@ def main() -> int: print(f"Build matrix: {json.dumps(build_matrix)}", file=sys.stderr) + # Step 5: Check if any artifacts need to be built + has_new_artifacts = check_artifacts_needed(version_name, args.build_amd64, args.build_arm64) + print(f"Has new artifacts to build: {has_new_artifacts}", file=sys.stderr) + # Write outputs write_github_output({ "commit_hash": commit_hash, "version_name": version_name, "build_matrix": json.dumps(build_matrix), + "has_new_artifacts": str(has_new_artifacts).lower(), }) return 0 From 61d32f05584b08ae11b363876129858e6a94b133 Mon Sep 17 00:00:00 2001 From: Joe Lombrozo Date: Wed, 6 May 2026 10:21:26 -0700 Subject: [PATCH 6/6] add pytest just script --- justfile | 3 ++ scripts/test_validate.py | 89 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 justfile diff --git a/justfile b/justfile new file mode 100644 index 000000000..dd7711943 --- /dev/null +++ b/justfile @@ -0,0 +1,3 @@ +test: + poetry install + poetry run pytest scripts/ diff --git a/scripts/test_validate.py b/scripts/test_validate.py index 449f7f910..b66b7d166 100644 --- a/scripts/test_validate.py +++ b/scripts/test_validate.py @@ -15,6 +15,8 @@ resolve_tag_and_commit, check_ci_status, generate_build_matrix, + get_existing_release_assets, + check_artifacts_needed, gh_api, ) @@ -382,3 +384,90 @@ def test_build_neither_architecture(self): """Test generating empty matrix when no architectures requested.""" matrix = generate_build_matrix(False, False) assert matrix == {"include": []} + + +class TestGetExistingReleaseAssets: + """Tests for get_existing_release_assets function.""" + + def test_no_github_repository_env(self): + """Test returns empty set when GITHUB_REPOSITORY is not set.""" + with patch.dict("os.environ", {}, clear=True): + assets = get_existing_release_assets("v1.0.0_abc1234") + assert assets == set() + + def test_release_not_found(self): + """Test returns empty set when release doesn't exist.""" + with patch.dict("os.environ", {"GITHUB_REPOSITORY": "owner/repo"}): + with patch("validate.run_command") as mock_run: + mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="release not found") + assets = get_existing_release_assets("v1.0.0_abc1234") + assert assets == set() + + def test_release_with_assets(self): + """Test returns set of asset names when release exists.""" + with patch.dict("os.environ", {"GITHUB_REPOSITORY": "owner/repo"}): + with patch("validate.run_command") as mock_run: + mock_run.return_value = MagicMock( + returncode=0, + stdout="firecracker-amd64\nfirecracker-arm64\nfirecracker\n", + stderr="" + ) + assets = get_existing_release_assets("v1.0.0_abc1234") + assert assets == {"firecracker-amd64", "firecracker-arm64", "firecracker"} + + def test_release_with_no_assets(self): + """Test returns empty set when release exists but has no assets.""" + with patch.dict("os.environ", {"GITHUB_REPOSITORY": "owner/repo"}): + with patch("validate.run_command") as mock_run: + mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") + assets = get_existing_release_assets("v1.0.0_abc1234") + assert assets == set() + + +class TestCheckArtifactsNeeded: + """Tests for check_artifacts_needed function.""" + + def test_both_requested_neither_exists(self): + """Test returns True when both requested and neither exists.""" + with patch("validate.get_existing_release_assets", return_value=set()): + assert check_artifacts_needed("v1.0.0_abc1234", True, True) is True + + def test_both_requested_amd64_missing(self): + """Test returns True when both requested but amd64 is missing.""" + with patch("validate.get_existing_release_assets", return_value={"firecracker-arm64"}): + assert check_artifacts_needed("v1.0.0_abc1234", True, True) is True + + def test_both_requested_arm64_missing(self): + """Test returns True when both requested but arm64 is missing.""" + with patch("validate.get_existing_release_assets", return_value={"firecracker-amd64"}): + assert check_artifacts_needed("v1.0.0_abc1234", True, True) is True + + def test_both_requested_both_exist(self): + """Test returns False when both requested and both exist.""" + with patch("validate.get_existing_release_assets", return_value={"firecracker-amd64", "firecracker-arm64"}): + assert check_artifacts_needed("v1.0.0_abc1234", True, True) is False + + def test_amd64_only_exists(self): + """Test returns False when only amd64 requested and it exists.""" + with patch("validate.get_existing_release_assets", return_value={"firecracker-amd64"}): + assert check_artifacts_needed("v1.0.0_abc1234", True, False) is False + + def test_amd64_only_missing(self): + """Test returns True when only amd64 requested and it's missing.""" + with patch("validate.get_existing_release_assets", return_value=set()): + assert check_artifacts_needed("v1.0.0_abc1234", True, False) is True + + def test_arm64_only_exists(self): + """Test returns False when only arm64 requested and it exists.""" + with patch("validate.get_existing_release_assets", return_value={"firecracker-arm64"}): + assert check_artifacts_needed("v1.0.0_abc1234", False, True) is False + + def test_arm64_only_missing(self): + """Test returns True when only arm64 requested and it's missing.""" + with patch("validate.get_existing_release_assets", return_value=set()): + assert check_artifacts_needed("v1.0.0_abc1234", False, True) is True + + def test_neither_requested(self): + """Test returns False when no architectures are requested.""" + with patch("validate.get_existing_release_assets", return_value=set()): + assert check_artifacts_needed("v1.0.0_abc1234", False, False) is False