supabase-postgres.git / summary / log / commit / refs

commit    6a97ebee6d173b44bf5a7d8df6ff02232c1f1b1a
Author:   Jean-François Roche <jfroche@pyxel.be>
Commit:   GitHub <noreply@github.com>
Date:     Wed Dec 10 19:33:34 2025 +0000

    feat: add stage 1 AMI build caching based on input hash (#1954)
    
    * feat(nix): add stage 1 AMI build caching based on input hash
    
    Implement content-based caching for stage 1 AMI builds by computing a hash from all input sources.
    Then the build-ami script checks for existing AMIs with matching input hash before building.
    
    * refactor(ci): extract common AMI build logic into reusable action and add actionlint
    
    Consolidate duplicate AMI build steps across workflows into a shared
    composite action. Also introduces actionlint configuration for GitHub
    Actions validation for the modified workflows.
    
    * fix: always set AWS region for AWS-related actions
    
    * fix: nix step parameters
    
    * fix: declare different ami name prefix for test and release builds
    
    In order to differentiate AMIs built for testing and those built for
    release, we need different AMI name. This commit introduces a new input
    parameter `ami_name_prefix`. For test builds, we set this prefix to
    include the GitHub run ID, ensuring uniqueness across test builds. For
    release builds, we use a static prefix "supabase-postgres".
    
    * chore: remove unused force-deregister variable from stage 2 AMI build
    
    The force-deregister variable is not used in the stage2-nix-psql.pkr.hcl
    
    * chore: remove unused packer variables
    
    * chore: display published AMI names in GitHub Actions logs
    
    * chore(ci): use the same matrix title format as testinfra-ami-build workflow
    
    We changed the version extraction in `testinfra-ami-build.yml` to avoid
    the quotes around the version numbers. This commit makes the same change
    in `test.yml` to keep things consistent.


.github/actionlint.yaml | 4 + .github/actions/build-ami/action.yml | 87 ++++++++++ .github/actions/nix-install-ephemeral/action.yml | 6 +- .github/workflows/ami-release-nix-single.yml | 60 +++---- .github/workflows/ami-release-nix.yml | 74 +++------ .github/workflows/nix-build.yml | 1 + .github/workflows/test.yml | 2 +- .github/workflows/testinfra-ami-build.yml | 125 +++++++------- amazon-arm64-nix.pkr.hcl | 30 ++-- nix/hooks.nix | 3 + nix/packages/build-ami.nix | 198 +++++++++++++++++++++++ nix/packages/build-test-ami.nix | 1 - nix/packages/default.nix | 1 + stage2-nix-psql.pkr.hcl | 35 +--- testinfra/README.md | 1 - testinfra/test_ami_nix.py | 10 +- 16 files changed, 421 insertions(+), 217 deletions(-) diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 6eea24e6..f238fb79 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -3,3 +3,7 @@ self-hosted-runner: - aarch64-darwin - aarch64-linux - blacksmith-32vcpu-ubuntu-2404 + - blacksmith-2vcpu-ubuntu-2404 + - blacksmith-2vcpu-ubuntu-2404-arm + - blacksmith-4vcpu-ubuntu-2404 + - large-linux-arm diff --git a/.github/actions/build-ami/action.yml b/.github/actions/build-ami/action.yml new file mode 100644 index 00000000..2cf3c125 --- /dev/null +++ b/.github/actions/build-ami/action.yml @@ -0,0 +1,87 @@ +name: Build AMI +description: Build both stage 1 and stage 2 AMIs + +inputs: + postgres_version: + description: 'PostgreSQL major version (e.g., 15)' + required: true + region: + description: 'AWS region' + required: true + ami_regions: + description: 'AMI regions as JSON array (e.g., ["us-east-1"])' + required: true + git_sha: + description: 'Git SHA for this build' + required: true + ami_name_prefix: + description: 'Prefix for the AMI name' + required: false + default: 'supabase-postgres' + +outputs: + stage2_ami_id: + description: 'The AMI ID of the stage 2 build' + value: ${{ steps.build-stage2.outputs.stage2_ami_id }} + postgres_release_version: + description: 'The PostgreSQL release version' + value: ${{ steps.generate-vars.outputs.version }} + execution_id: + description: 'The execution ID for this build' + value: ${{ steps.set-execution-id.outputs.execution_id }} + +runs: + using: "composite" + steps: + - name: Set execution ID + id: set-execution-id + shell: bash + run: | + EXECUTION_ID="${{ github.run_id }}-${{ inputs.postgres_version }}" + echo "EXECUTION_ID=$EXECUTION_ID" >> $GITHUB_ENV + echo "execution_id=$EXECUTION_ID" >> $GITHUB_OUTPUT + + - name: Generate common-nix.vars.pkr.hcl + id: generate-vars + shell: bash + run: | + PG_VERSION=$(nix run nixpkgs#yq -- -r '.postgres_release["postgres${{ inputs.postgres_version }}"]' ansible/vars.yml) + echo 'postgres-version = "'$PG_VERSION'"' > common-nix.vars.pkr.hcl + echo "" >> common-nix.vars.pkr.hcl + git add -f common-nix.vars.pkr.hcl + echo "version=$PG_VERSION" >> $GITHUB_OUTPUT + + - name: Build AMI stage 1 + shell: bash + env: + POSTGRES_MAJOR_VERSION: ${{ inputs.postgres_version }} + POSTGRES_VERSION: ${{ steps.generate-vars.outputs.version }} + AWS_MAX_ATTEMPTS: 10 + AWS_RETRY_MODE: adaptive + AWS_REGION: ${{ inputs.region }} + run: | + nix run .#build-ami -- stage1 \ + -var "git-head-version=${{ inputs.git_sha }}" \ + -var "packer-execution-id=${{ env.EXECUTION_ID }}" \ + -var "ansible_arguments=-e postgresql_major=${{ inputs.postgres_version }}" \ + -var 'ami_regions=${{ inputs.ami_regions }}' \ + amazon-arm64-nix.pkr.hcl + + - name: Build AMI stage 2 + id: build-stage2 + shell: bash + env: + POSTGRES_MAJOR_VERSION: ${{ inputs.postgres_version }} + POSTGRES_VERSION: ${{ steps.generate-vars.outputs.version }} + PACKER_EXECUTION_ID: ${{ env.EXECUTION_ID }} + AWS_MAX_ATTEMPTS: 10 + AWS_RETRY_MODE: adaptive + AWS_REGION: ${{ inputs.region }} + run: | + nix run .#build-ami -- stage2 \ + -var "git-head-version=${{ inputs.git_sha }}" \ + -var "packer-execution-id=${{ env.EXECUTION_ID }}" \ + -var "postgres_major_version=${{ inputs.postgres_version }}" \ + -var "ami_name=${{ inputs.ami_name_prefix }}" \ + -var "git_sha=${{ inputs.git_sha }}" \ + stage2-nix-psql.pkr.hcl diff --git a/.github/actions/nix-install-ephemeral/action.yml b/.github/actions/nix-install-ephemeral/action.yml index 047fb9be..b30a31db 100644 --- a/.github/actions/nix-install-ephemeral/action.yml +++ b/.github/actions/nix-install-ephemeral/action.yml @@ -5,6 +5,10 @@ inputs: description: 'Whether to push build outputs to the Nix binary cache' required: false default: 'false' + aws-region: + description: 'AWS region for the Nix binary cache S3 bucket' + required: false + default: 'us-east-1' runs: using: 'composite' steps: @@ -13,7 +17,7 @@ runs: if: ${{ inputs.push-to-cache == 'true' }} with: role-to-assume: ${{ env.DEV_AWS_ROLE }} - aws-region: "us-east-1" + aws-region: ${{ inputs.aws-region }} output-credentials: true role-duration-seconds: 7200 - name: Setup AWS credentials for Nix diff --git a/.github/workflows/ami-release-nix-single.yml b/.github/workflows/ami-release-nix-single.yml index bb6641ff..e4d18a7e 100644 --- a/.github/workflows/ami-release-nix-single.yml +++ b/.github/workflows/ami-release-nix-single.yml @@ -27,6 +27,7 @@ jobs: uses: supabase/postgres/.github/actions/shared-checkout@HEAD with: ref: ${{ github.event.inputs.branch }} + - name: aws-creds uses: aws-actions/configure-aws-credentials@v4 with: @@ -38,56 +39,35 @@ jobs: - name: Get current branch SHA id: get_sha run: | - echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Install nix - uses: cachix/install-nix-action@v27 + uses: ./.github/actions/nix-install-ephemeral with: - install_url: https://releases.nixos.org/nix/nix-2.29.1/install - extra_nix_config: | - substituters = https://cache.nixos.org https://nix-postgres-artifacts.s3.amazonaws.com - trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= - - - name: Set PostgreSQL version environment variable - run: | - echo "POSTGRES_MAJOR_VERSION=${{ github.event.inputs.postgres_version }}" >> $GITHUB_ENV - echo "EXECUTION_ID=${{ github.run_id }}-${{ matrix.postgres_version }}" >> $GITHUB_ENV - - - name: Generate common-nix.vars.pkr.hcl - run: | - PG_VERSION=$(nix run nixpkgs#yq -- '.postgres_release["postgres'${{ env.POSTGRES_MAJOR_VERSION }}'"]' ansible/vars.yml) - PG_VERSION=$(echo "$PG_VERSION" | tr -d '"') # Remove any surrounding quotes - echo 'postgres-version = "'$PG_VERSION'"' > common-nix.vars.pkr.hcl - # Ensure there's a newline at the end of the file - echo "" >> common-nix.vars.pkr.hcl - - - name: Build AMI stage 1 + push-to-cache: 'true' env: - POSTGRES_MAJOR_VERSION: ${{ env.POSTGRES_MAJOR_VERSION }} - run: | - GIT_SHA=${{ steps.get_sha.outputs.sha }} - nix run github:supabase/postgres/${GIT_SHA}#packer -- init amazon-arm64-nix.pkr.hcl - nix run github:supabase/postgres/${GIT_SHA}#packer -- build -var "git-head-version=${GIT_SHA}" -var "packer-execution-id=${EXECUTION_ID}" -var-file="development-arm.vars.pkr.hcl" -var-file="common-nix.vars.pkr.hcl" -var "ansible_arguments=-e postgresql_major=${POSTGRES_MAJOR_VERSION}" amazon-arm64-nix.pkr.hcl + DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }} + NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }} - - name: Build AMI stage 2 - env: - POSTGRES_MAJOR_VERSION: ${{ env.POSTGRES_MAJOR_VERSION }} - run: | - GIT_SHA=${{ steps.get_sha.outputs.sha }} - nix run github:supabase/postgres/${GIT_SHA}#packer -- init stage2-nix-psql.pkr.hcl - POSTGRES_MAJOR_VERSION=${{ env.POSTGRES_MAJOR_VERSION }} - nix run github:supabase/postgres/${GIT_SHA}#packer -- build -var "git_sha=${GIT_SHA}" -var "git-head-version=${GIT_SHA}" -var "packer-execution-id=${EXECUTION_ID}" -var "postgres_major_version=${POSTGRES_MAJOR_VERSION}" -var-file="development-arm.vars.pkr.hcl" -var-file="common-nix.vars.pkr.hcl" stage2-nix-psql.pkr.hcl + - name: Build AMI + id: build-ami + uses: ./.github/actions/build-ami + with: + postgres_version: ${{ github.event.inputs.postgres_version }} + region: us-east-1 + ami_regions: '["us-east-1"]' + git_sha: ${{ steps.get_sha.outputs.sha }} - name: Grab release version id: process_release_version run: | - VERSION=$(cat common-nix.vars.pkr.hcl | sed -e 's/postgres-version = "\(.*\)"/\1/g') - echo "version=$VERSION" >> $GITHUB_OUTPUT + VERSION="${{ steps.build-ami.outputs.postgres_release_version }}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Create nix flake revision tarball run: | GIT_SHA=${{ steps.get_sha.outputs.sha }} - MAJOR_VERSION=${{ env.POSTGRES_MAJOR_VERSION }} + MAJOR_VERSION=${{ github.event.inputs.postgres_version }} mkdir -p "/tmp/pg_upgrade_bin/${MAJOR_VERSION}" echo "$GIT_SHA" >> "/tmp/pg_upgrade_bin/${MAJOR_VERSION}/nix_flake_version" @@ -105,7 +85,7 @@ jobs: ansible-playbook -i localhost \ -e "ami_release_version=${{ steps.process_release_version.outputs.version }}" \ -e "internal_artifacts_bucket=${{ secrets.ARTIFACTS_BUCKET }}" \ - -e "postgres_major_version=${{ env.POSTGRES_MAJOR_VERSION }}" \ + -e "postgres_major_version=${{ github.event.inputs.postgres_version }}" \ manifest-playbook.yml - name: Upload nix flake revision to s3 staging @@ -126,7 +106,7 @@ jobs: ansible-playbook -i localhost \ -e "ami_release_version=${{ steps.process_release_version.outputs.version }}" \ -e "internal_artifacts_bucket=${{ secrets.PROD_ARTIFACTS_BUCKET }}" \ - -e "postgres_major_version=${{ env.POSTGRES_MAJOR_VERSION }}" \ + -e "postgres_major_version=${{ github.event.inputs.postgres_version }}" \ manifest-playbook.yml - name: Upload nix flake revision to s3 prod @@ -155,10 +135,12 @@ jobs: - name: Cleanup resources after build if: ${{ always() }} run: | + EXECUTION_ID="${{ steps.build-ami.outputs.execution_id }}" aws ec2 describe-instances --filters "Name=tag:packerExecutionId,Values=${EXECUTION_ID}" --query "Reservations[].Instances[].InstanceId" --output text | xargs -r aws ec2 terminate-instances --instance-ids - name: Cleanup resources on build cancellation if: ${{ cancelled() }} run: | + EXECUTION_ID="${{ steps.build-ami.outputs.execution_id }}" aws ec2 describe-instances --filters "Name=tag:packerExecutionId,Values=${EXECUTION_ID}" --query "Reservations[].Instances[].InstanceId" --output text | xargs -r aws ec2 terminate-instances --instance-ids diff --git a/.github/workflows/ami-release-nix.yml b/.github/workflows/ami-release-nix.yml index 3654de90..7748b98a 100644 --- a/.github/workflows/ami-release-nix.yml +++ b/.github/workflows/ami-release-nix.yml @@ -25,18 +25,13 @@ jobs: uses: supabase/postgres/.github/actions/shared-checkout@HEAD - name: Install nix - uses: cachix/install-nix-action@v27 - with: - install_url: https://releases.nixos.org/nix/nix-2.29.1/install - extra_nix_config: | - substituters = https://cache.nixos.org https://nix-postgres-artifacts.s3.amazonaws.com - trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= + uses: ./.github/actions/nix-install-ephemeral - name: Set PostgreSQL versions id: set-versions run: | - VERSIONS=$(nix run nixpkgs#yq -- '.postgres_major[]' ansible/vars.yml | nix run nixpkgs#jq -- -R -s -c 'split("\n")[:-1]') - echo "postgres_versions=$VERSIONS" >> $GITHUB_OUTPUT + VERSIONS=$(nix run nixpkgs#yq -- -r '.postgres_major[]' ansible/vars.yml | nix run nixpkgs#jq -- -R -s -c 'split("\n")[:-1]') + echo "postgres_versions=$VERSIONS" >> "$GITHUB_OUTPUT" build: needs: prepare @@ -51,6 +46,7 @@ jobs: steps: - name: Checkout Repo uses: supabase/postgres/.github/actions/shared-checkout@HEAD + - name: aws-creds uses: aws-actions/configure-aws-credentials@v4 with: @@ -60,12 +56,12 @@ jobs: role-duration-seconds: 7200 - name: Install nix - uses: cachix/install-nix-action@v27 + uses: ./.github/actions/nix-install-ephemeral with: - install_url: https://releases.nixos.org/nix/nix-2.29.1/install - extra_nix_config: | - substituters = https://cache.nixos.org https://nix-postgres-artifacts.s3.amazonaws.com - trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= + push-to-cache: 'true' + env: + DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }} + NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }} - name: Run checks if triggered manually if: ${{ github.event_name == 'workflow_dispatch' }} @@ -76,47 +72,25 @@ jobs: exit 1 fi - - name: Set PostgreSQL version environment variable - run: | - echo "POSTGRES_MAJOR_VERSION=${{ matrix.postgres_version }}" >> $GITHUB_ENV - echo "EXECUTION_ID=${{ github.run_id }}-${{ matrix.postgres_version }}" >> $GITHUB_ENV - - - name: Generate common-nix.vars.pkr.hcl - run: | - PG_VERSION=$(nix run nixpkgs#yq -- '.postgres_release["postgres'${{ matrix.postgres_version }}'"]' ansible/vars.yml) - PG_VERSION=$(echo "$PG_VERSION" | tr -d '"') # Remove any surrounding quotes - echo 'postgres-version = "'$PG_VERSION'"' > common-nix.vars.pkr.hcl - # Ensure there's a newline at the end of the file - echo "" >> common-nix.vars.pkr.hcl - - - name: Build AMI stage 1 - env: - POSTGRES_MAJOR_VERSION: ${{ env.POSTGRES_MAJOR_VERSION }} - run: | - GIT_SHA=${{github.sha}} - nix run github:supabase/postgres/${GIT_SHA}#packer -- init amazon-arm64-nix.pkr.hcl - # why is postgresql_major defined here instead of where the _three_ other postgresql_* variables are defined? - nix run github:supabase/postgres/${GIT_SHA}#packer -- build -var "git-head-version=${GIT_SHA}" -var "packer-execution-id=${EXECUTION_ID}" -var-file="development-arm.vars.pkr.hcl" -var-file="common-nix.vars.pkr.hcl" -var "ansible_arguments=-e postgresql_major=${POSTGRES_MAJOR_VERSION}" -var "region=us-east-1" -var 'ami_regions=["us-east-1"]' amazon-arm64-nix.pkr.hcl - - - name: Build AMI stage 2 - env: - POSTGRES_MAJOR_VERSION: ${{ env.POSTGRES_MAJOR_VERSION }} - run: | - GIT_SHA=${{github.sha}} - nix run github:supabase/postgres/${GIT_SHA}#packer -- init stage2-nix-psql.pkr.hcl - POSTGRES_MAJOR_VERSION=${{ env.POSTGRES_MAJOR_VERSION }} - nix run github:supabase/postgres/${GIT_SHA}#packer -- build -var "git_sha=${GIT_SHA}" -var "git-head-version=${GIT_SHA}" -var "packer-execution-id=${EXECUTION_ID}" -var "postgres_major_version=${POSTGRES_MAJOR_VERSION}" -var-file="development-arm.vars.pkr.hcl" -var-file="common-nix.vars.pkr.hcl" -var "region=us-east-1" -var 'ami_regions=["us-east-1"]' stage2-nix-psql.pkr.hcl + - name: Build AMI + id: build-ami + uses: ./.github/actions/build-ami + with: + postgres_version: ${{ matrix.postgres_version }} + region: us-east-1 + ami_regions: '["us-east-1"]' + git_sha: ${{ github.sha }} - name: Grab release version id: process_release_version run: | - VERSION=$(cat common-nix.vars.pkr.hcl | sed -e 's/postgres-version = "\(.*\)"/\1/g') - echo "version=$VERSION" >> $GITHUB_OUTPUT + VERSION="${{ steps.build-ami.outputs.postgres_release_version }}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Create nix flake revision tarball run: | GIT_SHA=${{github.sha}} - MAJOR_VERSION=${{ env.POSTGRES_MAJOR_VERSION }} + MAJOR_VERSION=${{ matrix.postgres_version }} mkdir -p "/tmp/pg_upgrade_bin/${MAJOR_VERSION}" echo "$GIT_SHA" >> "/tmp/pg_upgrade_bin/${MAJOR_VERSION}/nix_flake_version" @@ -134,7 +108,7 @@ jobs: ansible-playbook -i localhost \ -e "ami_release_version=${{ steps.process_release_version.outputs.version }}" \ -e "internal_artifacts_bucket=${{ secrets.ARTIFACTS_BUCKET }}" \ - -e "postgres_major_version=${{ env.POSTGRES_MAJOR_VERSION }}" \ + -e "postgres_major_version=${{ matrix.postgres_version }}" \ manifest-playbook.yml - name: Upload nix flake revision to s3 staging @@ -155,9 +129,9 @@ jobs: ansible-playbook -i localhost \ -e "ami_release_version=${{ steps.process_release_version.outputs.version }}" \ -e "internal_artifacts_bucket=${{ secrets.PROD_ARTIFACTS_BUCKET }}" \ - -e "postgres_major_version=${{ env.POSTGRES_MAJOR_VERSION }}" \ + -e "postgres_major_version=${{ matrix.postgres_version }}" \ manifest-playbook.yml - + - name: Upload nix flake revision to s3 prod run: | aws s3 cp /tmp/pg_binaries.tar.gz s3://${{ secrets.PROD_ARTIFACTS_BUCKET }}/upgrades/postgres/supabase-postgres-${{ steps.process_release_version.outputs.version }}/20.04.tar.gz @@ -184,9 +158,11 @@ jobs: - name: Cleanup resources after build if: ${{ always() }} run: | + EXECUTION_ID="${{ steps.build-ami.outputs.execution_id }}" aws ec2 --region us-east-1 describe-instances --filters "Name=tag:packerExecutionId,Values=${EXECUTION_ID}" --query "Reservations[].Instances[].InstanceId" --output text | xargs -r aws ec2 terminate-instances --region us-east-1 --instance-ids - name: Cleanup resources on build cancellation if: ${{ cancelled() }} run: | + EXECUTION_ID="${{ steps.build-ami.outputs.execution_id }}" aws ec2 --region us-east-1 describe-instances --filters "Name=tag:packerExecutionId,Values=${EXECUTION_ID}" --query "Reservations[].Instances[].InstanceId" --output text | xargs -r aws ec2 terminate-instances --region us-east-1 --instance-ids diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml index c1f9c35f..2b8945d6 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -199,6 +199,7 @@ jobs: uses: ./.github/workflows/testinfra-ami-build.yml secrets: DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }} + NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }} run-tests: needs: [nix-eval, nix-build-packages-aarch64-linux, nix-build-checks-aarch64-linux, nix-build-packages-aarch64-darwin, nix-build-checks-aarch64-darwin, nix-build-packages-x86_64-linux, nix-build-checks-x86_64-linux] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f462bc5c..3716c568 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: - name: Set PostgreSQL versions id: set-versions run: | - VERSIONS=$(nix run nixpkgs#yq -- '.postgres_major[]' ansible/vars.yml | nix run nixpkgs#jq -- -R -s -c "split(\"\n\")[:-1]") + VERSIONS=$(nix run nixpkgs#yq -- -r '.postgres_major[]' ansible/vars.yml | nix run nixpkgs#jq -- -R -s -c "split(\"\n\")[:-1]") echo "postgres_versions=$VERSIONS" >> $GITHUB_OUTPUT build: needs: prepare diff --git a/.github/workflows/testinfra-ami-build.yml b/.github/workflows/testinfra-ami-build.yml index 2677d1ce..c208b43d 100644 --- a/.github/workflows/testinfra-ami-build.yml +++ b/.github/workflows/testinfra-ami-build.yml @@ -7,6 +7,9 @@ on: DEV_AWS_ROLE: description: 'AWS role for dev environment' required: true + NIX_SIGN_SECRET_KEY: + description: 'Nix signing secret key' + required: true permissions: contents: write @@ -22,18 +25,13 @@ jobs: uses: supabase/postgres/.github/actions/shared-checkout@HEAD - name: Install nix - uses: cachix/install-nix-action@v27 - with: - install_url: https://releases.nixos.org/nix/nix-2.29.1/install - extra_nix_config: | - substituters = https://cache.nixos.org https://nix-postgres-artifacts.s3.amazonaws.com - trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= + uses: ./.github/actions/nix-install-ephemeral - name: Set PostgreSQL versions id: set-versions run: | - VERSIONS=$(nix run nixpkgs#yq -- '.postgres_major[]' ansible/vars.yml | nix run nixpkgs#jq -- -R -s -c 'split("\n")[:-1]') - echo "postgres_versions=$VERSIONS" >> $GITHUB_OUTPUT + VERSIONS=$(nix run nixpkgs#yq -- -r '.postgres_major[]' ansible/vars.yml | nix run nixpkgs#jq -- -R -r -s -c 'split("\n")[:-1]') + echo "postgres_versions=$VERSIONS" >> "$GITHUB_OUTPUT" test-ami-nix: needs: prepare @@ -72,13 +70,13 @@ jobs: role-duration-seconds: 7200 - name: Install nix - uses: cachix/install-nix-action@v27 + uses: ./.github/actions/nix-install-ephemeral with: - install_url: https://releases.nixos.org/nix/nix-2.29.1/install - extra_nix_config: | - substituters = https://cache.nixos.org https://nix-postgres-artifacts.s3.amazonaws.com - trusted-public-keys = nix-postgres-artifacts:dGZlQOvKcNEjvT7QEAJbcV6b6uk7VF/hWMjhYleiaLI= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= - + push-to-cache: 'true' + aws-region: "ap-southeast-1" + env: + DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }} + NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }} - id: args uses: mikefarah/yq@master @@ -91,77 +89,66 @@ jobs: with: endpoint: builders - - name: Generate random string - id: random - run: echo "random_string=$(openssl rand -hex 8)" >> $GITHUB_OUTPUT - - - name: Set PostgreSQL version environment variable - run: | - echo "POSTGRES_MAJOR_VERSION=${{ matrix.postgres_version }}" >> $GITHUB_ENV - echo "EXECUTION_ID=${{ github.run_id }}-${{ matrix.postgres_version }}" >> $GITHUB_ENV - - - name: Generate common-nix.vars.pkr.hcl - run: | - PG_VERSION=$(nix run nixpkgs#yq -- '.postgres_release["postgres'${{ matrix.postgres_version }}'"]' ansible/vars.yml) - PG_VERSION=$(echo "$PG_VERSION" | tr -d '"') # Remove any surrounding quotes - echo 'postgres-version = "'$PG_VERSION'"' > common-nix.vars.pkr.hcl - # Ensure there's a newline at the end of the file - echo "" >> common-nix.vars.pkr.hcl - - - name: Build AMI stage 1 - env: - AWS_MAX_ATTEMPTS: 10 - AWS_RETRY_MODE: adaptive - run: | - GIT_SHA=${{github.sha}} - nix run github:supabase/postgres/${GIT_SHA}#packer -- init amazon-arm64-nix.pkr.hcl - nix run github:supabase/postgres/${GIT_SHA}#packer -- build -var "git-head-version=${GIT_SHA}" -var "packer-execution-id=${EXECUTION_ID}" -var-file="development-arm.vars.pkr.hcl" -var-file="common-nix.vars.pkr.hcl" -var "ansible_arguments=" -var "postgres-version=${{ steps.random.outputs.random_string }}" -var "region=ap-southeast-1" -var 'ami_regions=["ap-southeast-1"]' -var "force-deregister=true" -var "ansible_arguments=-e postgresql_major=${POSTGRES_MAJOR_VERSION}" amazon-arm64-nix.pkr.hcl - - - name: Build AMI stage 2 - env: - AWS_MAX_ATTEMPTS: 10 - AWS_RETRY_MODE: adaptive - run: | - GIT_SHA=${{github.sha}} - nix run github:supabase/postgres/${GIT_SHA}#packer -- init stage2-nix-psql.pkr.hcl - nix run github:supabase/postgres/${GIT_SHA}#packer -- build -var "git-head-version=${GIT_SHA}" -var "packer-execution-id=${EXECUTION_ID}" -var "postgres_major_version=${POSTGRES_MAJOR_VERSION}" -var-file="development-arm.vars.pkr.hcl" -var-file="common-nix.vars.pkr.hcl" -var "postgres-version=${{ steps.random.outputs.random_string }}" -var "region=ap-southeast-1" -var 'ami_regions=["ap-southeast-1"]' -var "force-deregister=true" -var "git_sha=${GITHUB_SHA}" stage2-nix-psql.pkr.hcl + - name: Build AMI + id: build-ami + uses: ./.github/actions/build-ami + with: + postgres_version: ${{ matrix.postgres_version }} + region: ap-southeast-1 + ami_regions: '["ap-southeast-1"]' + git_sha: ${{ github.sha }} + ami_name_prefix: "supabase-postgres-${{ github.run_id }}" - name: Run tests timeout-minutes: 10 env: - AMI_NAME: "supabase-postgres-${{ steps.random.outputs.random_string }}" + AMI_ID: ${{ steps.build-ami.outputs.stage2_ami_id }} + EXECUTION_ID: ${{ steps.build-ami.outputs.execution_id }} run: | # TODO: use poetry for pkg mgmt - pip3 install boto3 boto3-stubs[essential] docker ec2instanceconnectcli pytest pytest-testinfra[paramiko,docker] requests + pip3 install boto3 "boto3-stubs[essential]" docker ec2instanceconnectcli pytest "pytest-testinfra[paramiko,docker]" requests pytest -vv -s testinfra/test_ami_nix.py - name: Cleanup resources on build cancellation if: ${{ cancelled() }} run: | - aws ec2 --region ap-southeast-1 describe-instances --filters "Name=tag:packerExecutionId,Values=${EXECUTION_ID}" --query "Reservations[].Instances[].InstanceId" --output text | xargs -r aws ec2 terminate-instances --region ap-southeast-1 --instance-ids + EXECUTION_ID="${{ steps.build-ami.outputs.execution_id }}" + INSTANCE_IDS=$(aws ec2 --region ap-southeast-1 describe-instances --filters "Name=tag:packerExecutionId,Values=${EXECUTION_ID}" --query "Reservations[].Instances[].InstanceId" --output text) + if [ -n "$INSTANCE_IDS" ]; then + echo "Terminating packer build instances: $INSTANCE_IDS" + echo "$INSTANCE_IDS" | xargs -r aws ec2 terminate-instances --region ap-southeast-1 --instance-ids + else + echo "No packer build instances to clean up" + fi - name: Cleanup resources after build if: ${{ always() }} run: | - aws ec2 --region ap-southeast-1 describe-instances --filters "Name=tag:testinfra-run-id,Values=${EXECUTION_ID}" --query "Reservations[].Instances[].InstanceId" --output text | xargs -r aws ec2 terminate-instances --region ap-southeast-1 --instance-ids || true + EXECUTION_ID="${{ steps.build-ami.outputs.execution_id }}" + INSTANCE_IDS=$(aws ec2 --region ap-southeast-1 describe-instances --filters "Name=tag:testinfra-run-id,Values=${EXECUTION_ID}" --query "Reservations[].Instances[].InstanceId" --output text) + if [ -n "$INSTANCE_IDS" ]; then + echo "Terminating testinfra instances: $INSTANCE_IDS" + echo "$INSTANCE_IDS" | xargs -r aws ec2 terminate-instances --region ap-southeast-1 --instance-ids || true + else + echo "No testinfra instances to clean up" + fi - - name: Cleanup AMIs + - name: Cleanup stage 2 AMI if: always() run: | - # Define AMI name patterns - STAGE1_AMI_NAME="supabase-postgres-ci-ami-test-stage-1" - STAGE2_AMI_NAME="${{ steps.random.outputs.random_string }}" - - # Function to deregister AMIs by name pattern - deregister_ami_by_name() { - local ami_name_pattern=$1 - local ami_ids=$(aws ec2 describe-images --region ap-southeast-1 --owners self --filters "Name=name,Values=${ami_name_pattern}" --query 'Images[*].ImageId' --output text) - for ami_id in $ami_ids; do - echo "Deregistering AMI: $ami_id" - aws ec2 deregister-image --region ap-southeast-1 --image-id $ami_id + EXECUTION_ID="${{ steps.build-ami.outputs.execution_id }}" + STAGE2_AMI_IDS=$(aws ec2 describe-images \ + --region ap-southeast-1 \ + --owners self \ + --filters "Name=tag:packerExecutionId,Values=${EXECUTION_ID}" \ + --query 'Images[*].ImageId' \ + --output text) + + if [ -n "$STAGE2_AMI_IDS" ]; then + for ami_id in $STAGE2_AMI_IDS; do + echo "Deregistering stage 2 AMI: $ami_id" + aws ec2 deregister-image --region ap-southeast-1 --image-id "$ami_id" || true done - } - - # Deregister AMIs - deregister_ami_by_name "$STAGE1_AMI_NAME" - deregister_ami_by_name "$STAGE2_AMI_NAME" + else + echo "No stage 2 AMI to clean up" + fi diff --git a/amazon-arm64-nix.pkr.hcl b/amazon-arm64-nix.pkr.hcl index 789a4853..630ab7c9 100644 --- a/amazon-arm64-nix.pkr.hcl +++ b/amazon-arm64-nix.pkr.hcl @@ -23,21 +23,6 @@ variable "ansible_arguments" { default = "--skip-tags install-postgrest,install-pgbouncer,install-supabase-internal" } -variable "aws_access_key" { - type = string - default = "" -} - -variable "aws_secret_key" { - type = string - default = "" -} - -variable "environment" { - type = string - default = "prod" -} - variable "region" { type = string } @@ -92,6 +77,12 @@ variable "force-deregister" { default = false } +variable "input-hash" { + type = string + default = "" + description = "Content hash of all input sources" +} + packer { required_plugins { amazon = { @@ -104,15 +95,12 @@ packer { # source block source "amazon-ebssurrogate" "source" { profile = "${var.profile}" - #access_key = "${var.aws_access_key}" - #ami_name = "${var.ami_name}-arm64-${formatdate("YYYY-MM-DD-hhmm", timestamp())}" - ami_name = "${var.ami_name}-${var.postgres-version}-stage-1" + ami_name = "${var.ami_name}-${var.postgres-version}-${var.input-hash}-stage-1" ami_virtualization_type = "hvm" ami_architecture = "arm64" ami_regions = "${var.ami_regions}" instance_type = "c6g.4xlarge" region = "${var.region}" - #secret_key = "${var.aws_secret_key}" force_deregister = var.force-deregister # Increase timeout for instance stop operations to handle large instances @@ -130,7 +118,8 @@ source "amazon-ebssurrogate" "source" { } owners = [ "099720109477" ] most_recent = true - } + } + ena_support = true launch_block_device_mappings { device_name = "/dev/xvdf" @@ -172,6 +161,7 @@ source "amazon-ebssurrogate" "source" { appType = "postgres" postgresVersion = "${var.postgres-version}-stage1" sourceSha = "${var.git-head-version}" + inputHash = "${var.input-hash}" } communicator = "ssh" diff --git a/nix/hooks.nix b/nix/hooks.nix index b659635f..9b78b8a5 100644 --- a/nix/hooks.nix +++ b/nix/hooks.nix @@ -4,6 +4,9 @@ let lintedWorkflows = [ "nix-eval.yml" "nix-build.yml" + "testinfra-ami-build.yml" + "ami-release-nix.yml" + "ami-release-nix-single.yml" ]; in { diff --git a/nix/packages/build-ami.nix b/nix/packages/build-ami.nix new file mode 100644 index 00000000..980223fd --- /dev/null +++ b/nix/packages/build-ami.nix @@ -0,0 +1,198 @@ +{ + lib, + stdenv, + writeShellApplication, + packer, + awscli2, + jq, + ... +}: + +let + root = ../..; + packerSources = stdenv.mkDerivation { + name = "packer-sources"; + src = lib.fileset.toSource { + inherit root; + fileset = lib.fileset.unions [ + (root + "/ebssurrogate") + (root + "/ansible") + (root + "/migrations") + (root + "/scripts") + (root + "/amazon-arm64-nix.pkr.hcl") + (root + "/development-arm.vars.pkr.hcl") + (lib.fileset.maybeMissing (root + "/common-nix.vars.pkr.hcl")) + ]; + }; + + phases = [ + "unpackPhase" + "installPhase" + ]; + installPhase = '' + mkdir -p $out + cp -r . $out/ + ''; + }; +in +writeShellApplication { + name = "build-ami"; + + runtimeInputs = [ + packer + awscli2 + jq + ]; + + text = '' + set -euo pipefail + + set -x + + # Parse stage parameter + STAGE="''${1:-stage1}" + shift || true # Remove first arg, ignore error if no args + + REGION="''${AWS_REGION:-ap-southeast-1}" + PACKER_SOURCES="${packerSources}" + INPUT_HASH=$(basename "$PACKER_SOURCES" | cut -d- -f1) + + find_stage1_ami() { + set +e + local ami_output + ami_output=$(aws ec2 describe-images \ + --region "$REGION" \ + --owners self \ + --filters \ + "Name=tag:inputHash,Values=$INPUT_HASH" \ + "Name=tag:postgresVersion,Values=$POSTGRES_VERSION-stage1" \ + "Name=state,Values=available" \ + --query 'Images[0].ImageId' \ + --output text 2>&1) + local exit_code=$? + set -e + + if [ $exit_code -ne 0 ] && [ $exit_code -ne 255 ]; then + echo "Error querying AWS: $ami_output" + exit 1 + fi + + if [ "$ami_output" = "None" ] || [ -z "$ami_output" ]; then + echo "" + else + echo "$ami_output" + fi + } + + if [ "$STAGE" = "stage1" ]; then + echo "Building stage 1..." + echo "Checking for existing AMI..." + + AMI_ID=$(find_stage1_ami) + if [ -n "$AMI_ID" ]; then + echo "Found existing AMI: $AMI_ID" + echo "STAGE1_AMI_ID=$AMI_ID" + + if [ -n "''${GITHUB_OUTPUT:-}" ]; then + AMI_NAME=$(aws ec2 describe-images \ + --region "$REGION" \ + --image-ids "$AMI_ID" \ + --query 'Images[0].Name' \ + --output text) + + if [ -n "$AMI_NAME" ]; then + echo "::notice title=Stage 1 AMI Found::AMI '$AMI_NAME' (ID: $AMI_ID) found in region $REGION" + fi + fi + + exit 0 + fi + + echo "No cached AMI found" + + cd "$PACKER_SOURCES" + packer init amazon-arm64-nix.pkr.hcl + packer build \ + -var-file="development-arm.vars.pkr.hcl" \ + -var "input-hash=$INPUT_HASH" \ + -var "postgres-version=$POSTGRES_VERSION" \ + -var "region=$REGION" \ + "$@" + + if [ -n "''${GITHUB_OUTPUT:-}" ]; then + STAGE1_AMI_ID=$(find_stage1_ami) + if [ -n "$STAGE1_AMI_ID" ]; then + AMI_NAME=$(aws ec2 describe-images \ + --region "$REGION" \ + --image-ids "$STAGE1_AMI_ID" \ + --query 'Images[0].Name' \ + --output text) + + if [ -n "$AMI_NAME" ]; then + echo "::notice title=Stage 1 AMI Built::AMI '$AMI_NAME' (ID: $STAGE1_AMI_ID) built in region $REGION" + fi + fi + fi + elif [ "$STAGE" = "stage2" ]; then + echo "Building stage 2..." + + STAGE1_AMI_ID=$(find_stage1_ami) + if [ -z "$STAGE1_AMI_ID" ]; then + echo "Error: Stage 1 AMI not found. Please build stage 1 first." + exit 1 + fi + + echo "Found stage 1 AMI: $STAGE1_AMI_ID" + + packer init stage2-nix-psql.pkr.hcl + packer build \ + -var-file="development-arm.vars.pkr.hcl" \ + -var-file="common-nix.vars.pkr.hcl" \ + -var "source_ami=$STAGE1_AMI_ID" \ + -var "region=$REGION" \ + "$@" + + if [ -n "''${PACKER_EXECUTION_ID:-}" ]; then + STAGE2_AMI_ID=$(aws ec2 describe-images \ + --region "$REGION" \ + --owners self \ + --filters \ + "Name=tag:packerExecutionId,Values=''${PACKER_EXECUTION_ID}" \ + "Name=state,Values=available" \ + --query 'Images[0].ImageId' \ + --output text) + + if [ -n "$STAGE2_AMI_ID" ] && [ "$STAGE2_AMI_ID" != "None" ]; then + echo "STAGE2_AMI_ID=$STAGE2_AMI_ID" + + if [ -n "''${GITHUB_OUTPUT:-}" ]; then + echo "stage2_ami_id=$STAGE2_AMI_ID" >> "$GITHUB_OUTPUT" + + AMI_NAME=$(aws ec2 describe-images \ + --region "$REGION" \ + --image-ids "$STAGE2_AMI_ID" \ + --query 'Images[0].Name' \ + --output text) + + if [ -n "$AMI_NAME" ]; then + echo "::notice title=Stage 2 AMI Published::AMI '$AMI_NAME' (ID: $STAGE2_AMI_ID) published in region $REGION" + fi + fi + fi + fi + else + echo "Error: Invalid stage '$STAGE'. Must be 'stage1' or 'stage2'" + exit 1 + fi + ''; + + meta = { + description = "Build AMI if not cached based on input hash"; + longDescription = '' + The input hash is computed from all source files that affect the build. + Before building, we verify the existence of an AMI with the same hash. + If found, the build is skipped. Otherwise, a new AMI is created and + tagged with the input hash for future cache hits. + ''; + }; +} diff --git a/nix/packages/build-test-ami.nix b/nix/packages/build-test-ami.nix index 115ea793..9a1d4c5d 100644 --- a/nix/packages/build-test-ami.nix +++ b/nix/packages/build-test-ami.nix @@ -120,7 +120,6 @@ runCommand "build-test-ami" -var "postgres-version=$RANDOM_STRING" \ -var "region=$REGION" \ -var 'ami_regions=["'"$REGION"'"]' \ - -var "force-deregister=true" \ -var "git_sha=$GIT_SHA" \ stage2-nix-psql.pkr.hcl diff --git a/nix/packages/default.nix b/nix/packages/default.nix index b3fc83a0..ed6bbdca 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -29,6 +29,7 @@ { packages = ( { + build-ami = pkgs.callPackage ./build-ami.nix { packer = self'.packages.packer; }; build-test-ami = pkgs.callPackage ./build-test-ami.nix { }; cleanup-ami = pkgs.callPackage ./cleanup-ami.nix { }; dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase) defaults; }; diff --git a/stage2-nix-psql.pkr.hcl b/stage2-nix-psql.pkr.hcl index 2f25b6ad..7ffeb7cb 100644 --- a/stage2-nix-psql.pkr.hcl +++ b/stage2-nix-psql.pkr.hcl @@ -1,18 +1,3 @@ -variable "profile" { - type = string - default = "${env("AWS_PROFILE")}" -} - -variable "ami_regions" { - type = list(string) - default = ["ap-southeast-1"] -} - -variable "environment" { - type = string - default = "prod" -} - variable "region" { type = string } @@ -37,10 +22,6 @@ variable "packer-execution-id" { default = "unknown" } -variable "force-deregister" { - type = bool - default = false -} variable "git_sha" { type = string default = env("GIT_SHA") @@ -51,6 +32,11 @@ variable "postgres_major_version" { default = "" } +variable "source_ami" { + type = string + description = "Source AMI ID from stage 1" +} + packer { required_plugins { amazon = { @@ -64,15 +50,7 @@ source "amazon-ebs" "ubuntu" { ami_name = "${var.ami_name}-${var.postgres-version}" instance_type = "c6g.4xlarge" region = "${var.region}" - source_ami_filter { - filters = { - name = "${var.ami_name}-${var.postgres-version}-stage-1" - root-device-type = "ebs" - virtualization-type = "hvm" - } - most_recent = true - owners = ["amazon", "self"] - } + source_ami = "${var.source_ami}" communicator = "ssh" ssh_pty = true @@ -107,6 +85,7 @@ source "amazon-ebs" "ubuntu" { appType = "postgres" postgresVersion = "${var.postgres-version}" sourceSha = "${var.git-head-version}" + packerExecutionId = "${var.packer-execution-id}" } } diff --git a/testinfra/README.md b/testinfra/README.md index 6ecdeadb..e0f34644 100644 --- a/testinfra/README.md +++ b/testinfra/README.md @@ -58,7 +58,6 @@ AWS_PROFILE=supabase-dev packer build \ -var "postgres-version=ci-ami-test" \ -var "region=ap-southeast-1" \ -var 'ami_regions=["ap-southeast-1"]' \ - -var "force-deregister=true" \ amazon-arm64.pkr.hcl # run tests diff --git a/testinfra/test_ami_nix.py b/testinfra/test_ami_nix.py index 2997d2fb..1b00f2c8 100644 --- a/testinfra/test_ami_nix.py +++ b/testinfra/test_ami_nix.py @@ -19,7 +19,7 @@ RUN_ID = os.environ.get( + "@" + socket.gethostname(), ) -AMI_NAME = os.environ.get("AMI_NAME") +AMI_ID = os.environ.get("AMI_ID") postgresql_schema_sql_content = """ ALTER DATABASE postgres SET "app.settings.jwt_secret" TO 'my_jwt_secret_which_is_not_so_secret'; ALTER DATABASE postgres SET "app.settings.jwt_exp" TO 3600; @@ -224,13 +224,7 @@ def run_ssh_command(ssh, command, timeout=None): @pytest.fixture(scope="session") def host(): ec2 = boto3.resource("ec2", region_name="ap-southeast-1") - images = list( - ec2.images.filter( - Filters=[{"Name": "name", "Values": [AMI_NAME]}], - ) - ) - assert len(images) == 1 - image = images[0] + image = ec2.Image(AMI_ID) def gzip_then_base64_encode(s: str) -> str: return base64.b64encode(gzip.compress(s.encode())).decode() [parent: 75b8362bfcee]