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
33 changes: 27 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ 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 }}
has_new_artifacts: ${{ steps.validate.outputs.has_new_artifacts }}
steps:
- uses: actions/checkout@v6

Expand All @@ -57,20 +57,39 @@ jobs:
--build-amd64 "${{ inputs.build_amd64 }}" \
--build-arm64 "${{ inputs.build_arm64 }}"


build:
needs: validate
if: needs.validate.outputs.skip_build != 'true'
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.validate.outputs.build_matrix) }}
runs-on: ${{ matrix.runner }}
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'
Comment thread
cursor[bot] marked this conversation as resolved.
uses: actions/upload-artifact@v7
with:
name: firecracker-${{ needs.validate.outputs.version_name }}-${{ matrix.arch }}
Expand All @@ -79,20 +98,21 @@ jobs:

publish:
needs: [validate, build]
if: needs.validate.outputs.skip_build != 'true'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
with:
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
Expand Down Expand Up @@ -163,9 +183,9 @@ jobs:

echo "Release URL: https://github.com/${{ github.repository }}/releases/tag/$version_name"


deploy:
needs: [validate, publish]
if: needs.validate.outputs.skip_build != 'true'
strategy:
fail-fast: false
matrix:
Expand All @@ -176,8 +196,9 @@ 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 }}
service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}

- name: Download release assets
env:
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test:
poetry install
poetry run pytest scripts/
171 changes: 118 additions & 53 deletions scripts/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
find_tag_for_commit,
resolve_tag_and_commit,
check_ci_status,
check_existing_artifacts,
generate_build_matrix,
get_existing_release_assets,
check_artifacts_needed,
gh_api,
)

Expand Down Expand Up @@ -354,55 +356,118 @@ 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": []}


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
Loading
Loading