From 8a83cf8030a5d0e970ea48bf86af6155ee77438c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Sat, 12 Sep 2026 13:53:32 +0300 Subject: [PATCH 1/6] Add draft supabox manifest-diff workflow (develop vs PR) Calls supabox's tag:manifest suite (supabase/supabox#216) once per ref, diffing the resulting rootfs. MPG-117 / RFC: declarative system management. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/supabox-manifest-diff.yml | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/supabox-manifest-diff.yml diff --git a/.github/workflows/supabox-manifest-diff.yml b/.github/workflows/supabox-manifest-diff.yml new file mode 100644 index 000000000..79848c5c2 --- /dev/null +++ b/.github/workflows/supabox-manifest-diff.yml @@ -0,0 +1,40 @@ +# Rootfs manifest diff: this PR's AMI vs develop's, via supabox's +# tag:manifest suite (supabase/supabox#216). Draft: manual dispatch +# only until validated. See MPG-117 / "RFC: declarative system +# management". +name: Supabox manifest diff (draft) + +on: + workflow_dispatch: + # TODO: enable once tag:manifest lands in supabox and this is validated manually + #pull_request: + # branches: [develop] + +jobs: + baseline: + uses: supabase/supabox/.github/workflows/e2e.yml@main + permissions: + contents: read + id-token: write + with: + filters: tag:manifest + merge_json: '{"repos":{"postgres":{"pin":"develop"}}}' + secrets: + APP_PRIVATE_KEY: ${{ secrets.SUPABOX_SB_SUPABOT_PRIVATE_KEY }} + + pr: + uses: supabase/supabox/.github/workflows/e2e.yml@main + permissions: + contents: read + id-token: write + with: + filters: tag:manifest + merge_json: '{"repos":{"postgres":{"pin":"${{ github.event.pull_request.head.sha || github.sha }}"}}}' + secrets: + APP_PRIVATE_KEY: ${{ secrets.SUPABOX_SB_SUPABOT_PRIVATE_KEY }} + + diff: + needs: [baseline, pr] + runs-on: ubuntu-latest + steps: + - run: echo "TODO: fetch both manifest artifacts, post diff as PR comment" From e2a7cff4e3c9db4a244ab9d08b7f8219c5567dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Sat, 12 Sep 2026 15:19:08 +0300 Subject: [PATCH 2/6] Serialize baseline/pr manifest-diff jobs to avoid cache clobbering e2e.yml's Blacksmith sticky disk is keyed by profiles+latest only (supabase/supabox#214), not by repo pin, so same-flavor parallel builds race on the same cache. Chain pr after baseline. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/supabox-manifest-diff.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/supabox-manifest-diff.yml b/.github/workflows/supabox-manifest-diff.yml index 79848c5c2..a7c1055e6 100644 --- a/.github/workflows/supabox-manifest-diff.yml +++ b/.github/workflows/supabox-manifest-diff.yml @@ -2,6 +2,12 @@ # tag:manifest suite (supabase/supabox#216). Draft: manual dispatch # only until validated. See MPG-117 / "RFC: declarative system # management". +# +# baseline/pr run sequentially, not in parallel: e2e.yml's Blacksmith +# build cache is a single sticky disk keyed only by profiles+latest +# (supabase/supabox#214), not by repo pin, so two same-flavor builds +# racing in parallel would clobber each other's cache (the exact bug +# #214 fixed for the periodic/PR builds). name: Supabox manifest diff (draft) on: @@ -23,6 +29,7 @@ jobs: APP_PRIVATE_KEY: ${{ secrets.SUPABOX_SB_SUPABOT_PRIVATE_KEY }} pr: + needs: [baseline] uses: supabase/supabox/.github/workflows/e2e.yml@main permissions: contents: read From fbbd816a66397d4944f3879d3fec8fde88442e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Sat, 12 Sep 2026 15:25:45 +0300 Subject: [PATCH 3/6] Queue concurrent PR runs on a shared workflow-level concurrency group e2e.yml's own concurrency group is per-run_id for workflow_call, so two PRs' calls race independently on the same profile-keyed sticky disk. A workflow-level group serializes across PRs instead. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/supabox-manifest-diff.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/supabox-manifest-diff.yml b/.github/workflows/supabox-manifest-diff.yml index a7c1055e6..cabeed356 100644 --- a/.github/workflows/supabox-manifest-diff.yml +++ b/.github/workflows/supabox-manifest-diff.yml @@ -16,6 +16,15 @@ on: #pull_request: # branches: [develop] +# One sticky disk, shared by every PR's baseline+pr build (same profile +# flavor, e2e.yml's own concurrency group is per-run_id for +# workflow_call and won't catch this). Queue concurrent PRs rather +# than race them on the same cache; never cancel one PR's run for +# another's. +concurrency: + group: supabox-manifest-diff-cache + cancel-in-progress: false + jobs: baseline: uses: supabase/supabox/.github/workflows/e2e.yml@main From 9dc17fdaeea5ce2b654ceb4d53091ce68c55589f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Sat, 12 Sep 2026 15:48:06 +0300 Subject: [PATCH 4/6] Add AMI manifest snapshot to testinfra, drop supabox-routed diff Replaces the supabox-based approach: snapshot rides the same session-scoped instance testinfra_ami_nix.py already boots per PR, zero extra AMI builds. Develop-side S3 caching is blocked pending a region/instance-profile decision for us-east-1 (see PR description). Co-Authored-By: Claude Sonnet 5 --- .github/workflows/supabox-manifest-diff.yml | 56 --------------------- .github/workflows/testinfra-ami-build.yml | 9 ++++ testinfra/manifest-snapshot.sh | 23 +++++++++ testinfra/test_ami_nix.py | 11 ++++ 4 files changed, 43 insertions(+), 56 deletions(-) delete mode 100644 .github/workflows/supabox-manifest-diff.yml create mode 100755 testinfra/manifest-snapshot.sh diff --git a/.github/workflows/supabox-manifest-diff.yml b/.github/workflows/supabox-manifest-diff.yml deleted file mode 100644 index cabeed356..000000000 --- a/.github/workflows/supabox-manifest-diff.yml +++ /dev/null @@ -1,56 +0,0 @@ -# Rootfs manifest diff: this PR's AMI vs develop's, via supabox's -# tag:manifest suite (supabase/supabox#216). Draft: manual dispatch -# only until validated. See MPG-117 / "RFC: declarative system -# management". -# -# baseline/pr run sequentially, not in parallel: e2e.yml's Blacksmith -# build cache is a single sticky disk keyed only by profiles+latest -# (supabase/supabox#214), not by repo pin, so two same-flavor builds -# racing in parallel would clobber each other's cache (the exact bug -# #214 fixed for the periodic/PR builds). -name: Supabox manifest diff (draft) - -on: - workflow_dispatch: - # TODO: enable once tag:manifest lands in supabox and this is validated manually - #pull_request: - # branches: [develop] - -# One sticky disk, shared by every PR's baseline+pr build (same profile -# flavor, e2e.yml's own concurrency group is per-run_id for -# workflow_call and won't catch this). Queue concurrent PRs rather -# than race them on the same cache; never cancel one PR's run for -# another's. -concurrency: - group: supabox-manifest-diff-cache - cancel-in-progress: false - -jobs: - baseline: - uses: supabase/supabox/.github/workflows/e2e.yml@main - permissions: - contents: read - id-token: write - with: - filters: tag:manifest - merge_json: '{"repos":{"postgres":{"pin":"develop"}}}' - secrets: - APP_PRIVATE_KEY: ${{ secrets.SUPABOX_SB_SUPABOT_PRIVATE_KEY }} - - pr: - needs: [baseline] - uses: supabase/supabox/.github/workflows/e2e.yml@main - permissions: - contents: read - id-token: write - with: - filters: tag:manifest - merge_json: '{"repos":{"postgres":{"pin":"${{ github.event.pull_request.head.sha || github.sha }}"}}}' - secrets: - APP_PRIVATE_KEY: ${{ secrets.SUPABOX_SB_SUPABOT_PRIVATE_KEY }} - - diff: - needs: [baseline, pr] - runs-on: ubuntu-latest - steps: - - run: echo "TODO: fetch both manifest artifacts, post diff as PR comment" diff --git a/.github/workflows/testinfra-ami-build.yml b/.github/workflows/testinfra-ami-build.yml index 728835839..948875b34 100644 --- a/.github/workflows/testinfra-ami-build.yml +++ b/.github/workflows/testinfra-ami-build.yml @@ -126,11 +126,20 @@ jobs: env: AMI_ID: ${{ steps.build-ami.outputs.stage2_ami_id }} EXECUTION_ID: ${{ steps.build-ami.outputs.execution_id }} + MANIFEST_OUTPUT: ami-manifest-${{ matrix.postgres_version }}-${{ matrix.target.arch }}.txt run: | # TODO: use uv for pkg mgmt pip3 install boto3 'boto3-stubs[essential]' ec2instanceconnectcli pytest 'pytest-testinfra[paramiko]' requests pytest -vv -s testinfra/test_ami_nix.py + - name: Upload AMI manifest + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ami-manifest-${{ matrix.postgres_version }}-${{ matrix.target.arch }} + path: ami-manifest-*.txt + overwrite: true + retention-days: 1 + - name: Cleanup resources on build cancellation if: ${{ cancelled() }} run: | diff --git a/testinfra/manifest-snapshot.sh b/testinfra/manifest-snapshot.sh new file mode 100755 index 000000000..0f5a949f9 --- /dev/null +++ b/testinfra/manifest-snapshot.sh @@ -0,0 +1,23 @@ +#!/bin/sh +set -eu + +EXCLUDE='^/(proc|run|tmp|var/log|data)(/|$)' + +find / -xdev \( -type f -o -type l \) 2>/dev/null | grep -Ev "$EXCLUDE" | sort | while read -r f; do + if [ -L "$f" ]; then + printf '%s\tlink\t%s\n' "$f" "$(readlink "$f")" + else + printf '%s\t%s\t%s\n' "$f" "$(stat -c '%a:%U:%G' "$f" 2>/dev/null || echo '?')" "$(sha256sum "$f" 2>/dev/null | cut -d' ' -f1)" + fi +done + +echo '--- units ---' +systemctl list-unit-files --no-pager 2>/dev/null | sort +echo '--- users ---' +getent passwd | sort +echo '--- groups ---' +getent group | sort +echo '--- nft ---' +nft list ruleset 2>/dev/null || true +echo '--- sysctl ---' +sysctl -a 2>/dev/null | sort diff --git a/testinfra/test_ami_nix.py b/testinfra/test_ami_nix.py index cfd77554d..ed0e31bf5 100644 --- a/testinfra/test_ami_nix.py +++ b/testinfra/test_ami_nix.py @@ -1407,3 +1407,14 @@ def test_apparmor_denies_access_to_sensitive_paths(host): f"to have succeeded.\nstdout: {result['stdout']}\nstderr: {result['stderr']}" ) print(f"Confirmed: access to {test_file} denied by AppArmor") + + +def test_manifest_snapshot(host): + ssh = host["ssh"] + script = os.path.join(os.path.dirname(__file__), "manifest-snapshot.sh") + upload_file_via_sftp(ssh, script, "/tmp/manifest-snapshot.sh") + run_ssh_command(ssh, "chmod +x /tmp/manifest-snapshot.sh") + result = run_ssh_command(ssh, "sudo /tmp/manifest-snapshot.sh") + assert result["succeeded"], result["stderr"] + with open(os.environ.get("MANIFEST_OUTPUT", "ami-manifest.txt"), "w") as f: + f.write(result["stdout"]) From c1aab5801fd2cad1139dc355f6affe77b0c5601a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Sat, 12 Sep 2026 15:57:08 +0300 Subject: [PATCH 5/6] Add us-east-1 manifest snapshot to ami-release-nix.yml Bare instance launch (temp SG + EC2 Instance Connect), same pattern Packer already uses in this account/region. Uploads to S3 as the develop baseline for the AMI manifest diff. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ami-release-nix.yml | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/ami-release-nix.yml b/.github/workflows/ami-release-nix.yml index 773aee018..fac08874e 100644 --- a/.github/workflows/ami-release-nix.yml +++ b/.github/workflows/ami-release-nix.yml @@ -105,6 +105,40 @@ jobs: postgres_version: ${{ matrix.postgres_version }} region: ${{ env.AWS_REGION }} + - name: Snapshot AMI manifest + if: matrix.target.arch == 'amd64' + env: + AMI_ID: ${{ steps.build-ami.outputs.stage2_ami_id }} + RUN_ID: ${{ github.run_id }} + run: | + ssh-keygen -t ed25519 -N '' -f /tmp/manifest-key -q + + VPC_ID=$(aws ec2 describe-vpcs --region "$AWS_REGION" --filters Name=is-default,Values=true --query 'Vpcs[0].VpcId' --output text) + SUBNET_ID=$(aws ec2 describe-subnets --region "$AWS_REGION" --filters Name=vpc-id,Values="$VPC_ID" --query 'Subnets[0].SubnetId' --output text) + SG_ID=$(aws ec2 create-security-group --region "$AWS_REGION" --group-name "manifest-snapshot-$RUN_ID" --description "temp sg for ami manifest snapshot" --vpc-id "$VPC_ID" --query GroupId --output text) + aws ec2 authorize-security-group-ingress --region "$AWS_REGION" --group-id "$SG_ID" --protocol tcp --port 22 --cidr 0.0.0.0/0 + + cleanup() { + [ -n "${INSTANCE_ID:-}" ] && aws ec2 terminate-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" && aws ec2 wait instance-terminated --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" + aws ec2 delete-security-group --region "$AWS_REGION" --group-id "$SG_ID" + } + trap cleanup EXIT + + INSTANCE_ID=$(aws ec2 run-instances --region "$AWS_REGION" --image-id "$AMI_ID" --instance-type t3.small --subnet-id "$SUBNET_ID" --security-group-ids "$SG_ID" --associate-public-ip-address --count 1 --query 'Instances[0].InstanceId' --output text) + aws ec2 wait instance-running --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" + IP=$(aws ec2 describe-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" --query 'Reservations[0].Instances[0].PublicIpAddress' --output text) + + for i in $(seq 1 30); do + aws ec2-instance-connect send-ssh-public-key --region "$AWS_REGION" --instance-id "$INSTANCE_ID" --instance-os-user ubuntu --ssh-public-key file:///tmp/manifest-key.pub + ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -i /tmp/manifest-key "ubuntu@$IP" true && break + sleep 5 + done + + scp -o StrictHostKeyChecking=no -i /tmp/manifest-key testinfra/manifest-snapshot.sh "ubuntu@$IP:/tmp/manifest-snapshot.sh" + ssh -o StrictHostKeyChecking=no -i /tmp/manifest-key "ubuntu@$IP" "chmod +x /tmp/manifest-snapshot.sh && sudo /tmp/manifest-snapshot.sh" > ami-manifest.txt + + aws s3 cp ami-manifest.txt "s3://${{ secrets.ARTIFACTS_BUCKET }}/ami-manifests/${{ matrix.postgres_version }}/latest.txt" + - name: Setup post build env vars run: | POSTGRES_SUPABASE_VERSION=${{ steps.build-ami.outputs.postgres_release_version }} From 31b2aa97b6a8923e7d41c3e1b88301218003bfd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Sat, 12 Sep 2026 15:58:32 +0300 Subject: [PATCH 6/6] Scope manifest snapshot SG ingress to the runner's own IP Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ami-release-nix.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ami-release-nix.yml b/.github/workflows/ami-release-nix.yml index fac08874e..a6cf2de67 100644 --- a/.github/workflows/ami-release-nix.yml +++ b/.github/workflows/ami-release-nix.yml @@ -116,7 +116,8 @@ jobs: VPC_ID=$(aws ec2 describe-vpcs --region "$AWS_REGION" --filters Name=is-default,Values=true --query 'Vpcs[0].VpcId' --output text) SUBNET_ID=$(aws ec2 describe-subnets --region "$AWS_REGION" --filters Name=vpc-id,Values="$VPC_ID" --query 'Subnets[0].SubnetId' --output text) SG_ID=$(aws ec2 create-security-group --region "$AWS_REGION" --group-name "manifest-snapshot-$RUN_ID" --description "temp sg for ami manifest snapshot" --vpc-id "$VPC_ID" --query GroupId --output text) - aws ec2 authorize-security-group-ingress --region "$AWS_REGION" --group-id "$SG_ID" --protocol tcp --port 22 --cidr 0.0.0.0/0 + RUNNER_IP=$(curl -s https://checkip.amazonaws.com) + aws ec2 authorize-security-group-ingress --region "$AWS_REGION" --group-id "$SG_ID" --protocol tcp --port 22 --cidr "$RUNNER_IP/32" cleanup() { [ -n "${INSTANCE_ID:-}" ] && aws ec2 terminate-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" && aws ec2 wait instance-terminated --region "$AWS_REGION" --instance-ids "$INSTANCE_ID"