diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8139d93 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: "weekly" + groups: + github-actions: + patterns: + - "*" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a70cfe4..3cddcf1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,8 @@ name: LocalStack Test on: + push: + branches: + - 'main' pull_request: paths-ignore: - ./*.md @@ -9,45 +12,16 @@ on: - cron: '48 23 * * 0' jobs: - localstack-action-test: - name: 'Test LocalStack GitHub Action' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - # We must hack the action call as remote to be able to use the relative paths - # Could it break with different CWD? 🤔 - - name: Start LocalStack - uses: jenseng/dynamic-uses@v1 - with: - uses: LocalStack/setup-localstack@${{ env.GH_ACTION_VERSION }} - with: |- - { - "image-tag": "latest", - "install-awslocal": "true", - "configuration": "DEBUG=1", - "use-pro": "true", - } - env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} - GH_ACTION_VERSION: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }} - - - name: Run Tests Against LocalStack - run: | - awslocal s3 mb s3://test - awslocal s3 ls - echo "Test Execution complete!" - localstack-action-version-test: name: 'Test LocalStack Version with Github Actions' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # We must hack the action call as remote to be able to use the relative paths # Could it break with different CWD? 🤔 - name: Start LocalStack - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 with: uses: LocalStack/setup-localstack@${{ env.GH_ACTION_VERSION }} with: |- @@ -58,23 +32,26 @@ jobs: "use-pro": "true", } env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} - GH_ACTION_VERSION: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }} + # intentially left `LOCALSTACK_API_KEY` as env here, to ensure the fallback still works for old version + LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + GH_ACTION_VERSION: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.ref_name }} - name: Run Version Test Against LocalStack run: | LS_VERSION=$(docker ps | grep localstack | cut -d " " -f4 | cut -d ":" -f2) exit $(test "x${LS_VERSION}" = "x3.2.0") - cloud-pods-test: + cloud-pods-save-test: name: 'Test Cloud Pods Action' runs-on: ubuntu-latest + outputs: + pod-name: ${{ steps.pod_name.outputs.name }} steps: - name: ⚡️ Checkout the repository - uses: actions/checkout@v3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Start LocalStack - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 with: uses: LocalStack/setup-localstack@${{ env.GH_ACTION_VERSION }} with: |- @@ -85,8 +62,12 @@ jobs: "use-pro": "true", } env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} - GH_ACTION_VERSION: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + GH_ACTION_VERSION: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.ref_name }} + + - name: Generate random pod name + id: pod_name + run: echo "name=cloud-pods-test-$RANDOM" >> $GITHUB_OUTPUT - name: Run AWS commands run: | @@ -94,27 +75,35 @@ jobs: awslocal sqs create-queue --queue-name test-queue - name: Save the Cloud Pod - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 with: uses: LocalStack/setup-localstack@${{ env.GH_ACTION_VERSION }} with: |- { - "state-name": "cloud-pods-test", + "state-name": "${{ steps.pod_name.outputs.name }}", "state-action": "save", } env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} - GH_ACTION_VERSION: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + GH_ACTION_VERSION: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.ref_name }} + + - name: Verify Cloud Pod + run: | + localstack pod list | grep ${{ steps.pod_name.outputs.name }} + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + - local-state-test: - name: 'Test Local State Action' + load-cloud-pod-test: + name: 'Test Loading Cloud Pod' runs-on: ubuntu-latest + needs: cloud-pods-save-test steps: - name: ⚡️ Checkout the repository - uses: actions/checkout@v3 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - name: Start LocalStack - uses: jenseng/dynamic-uses@v1 + - name: Start LocalStack and Load Pod + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 with: uses: LocalStack/setup-localstack@${{ env.GH_ACTION_VERSION }} with: |- @@ -123,31 +112,108 @@ jobs: "install-awslocal": "true", "configuration": "DEBUG=1", "use-pro": "true", - "state-name": "cloud-pods-test", - "state-action": "load", - "state-backend": "local", + "state-name": "${{ needs.cloud-pods-save-test.outputs.pod-name }}", + "state-action": "load" } env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} - GH_ACTION_VERSION: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + GH_ACTION_VERSION: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.ref_name }} - - name: Run AWS Commands + - name: Verify loaded resources run: | - awslocal s3 mb s3://test - awslocal s3 rb s3://test - awslocal sqs create-queue --queue-name test-queue - awslocal sqs delete-queue --queue-url $(awslocal sqs get-queue-url --queue-name test-queue --output text) + echo "Verifying S3 bucket..." + awslocal s3 ls | grep "test" + echo "Verifying SQS queue..." + awslocal sqs list-queues | grep "test-queue" + + - name: Clean up remote pod + run: localstack pod delete ${{ needs.cloud-pods-save-test.outputs.pod-name }} + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + + + - name: Verify successful cleanup of Cloud Pod + run: | + echo "Verifying that the pod has been deleted..." + if localstack pod list | grep -q ${{ needs.cloud-pods-save-test.outputs.pod-name }}; then + echo "Cleanup failed! Pod ${{ needs.cloud-pods-save-test.outputs.pod-name }} was not deleted." + exit 1 + fi + echo "Pod successfully deleted." + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + + + local-state-save-test: + name: 'Test Local State Save Action' + runs-on: ubuntu-latest + steps: + - name: ⚡️ Checkout the repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Start LocalStack + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 + with: + uses: LocalStack/setup-localstack@${{ env.GH_ACTION_VERSION }} + with: |- + { + "image-tag": "latest", + "install-awslocal": "true", + "configuration": "DEBUG=1", + "use-pro": "true", + } + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + GH_ACTION_VERSION: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.ref_name }} + + - name: Run AWS commands + run: | + awslocal s3 mb s3://test-local + awslocal sqs create-queue --queue-name test-queue-local - name: Save the State Artifact - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 with: uses: LocalStack/setup-localstack@${{ env.GH_ACTION_VERSION }} with: |- { - "state-name": "cloud-pods-test", + "state-name": "local-pods-test", "state-action": "save", "state-backend": "local", } env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} - GH_ACTION_VERSION: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + GH_ACTION_VERSION: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.ref_name }} + + local-state-load-test: + name: 'Test Local State Load Action' + needs: local-state-save-test + runs-on: ubuntu-latest + steps: + - name: ⚡️ Checkout the repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Start LocalStack + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 + with: + uses: LocalStack/setup-localstack@${{ env.GH_ACTION_VERSION }} + with: |- + { + "image-tag": "latest", + "install-awslocal": "true", + "configuration": "DEBUG=1", + "use-pro": "true", + "state-name": "local-pods-test", + "state-action": "load", + "state-backend": "local", + } + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + GH_ACTION_VERSION: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref_name }} + + - name: Run AWS Commands + run: | + echo "Verifying S3 bucket..." + awslocal s3 ls | grep "test-local" + echo "Verifying SQS queue..." + awslocal sqs list-queues | grep "test-queue-local" \ No newline at end of file diff --git a/.github/workflows/ephemeral.yml b/.github/workflows/ephemeral.yml index 35041ad..4c4fc09 100644 --- a/.github/workflows/ephemeral.yml +++ b/.github/workflows/ephemeral.yml @@ -1,5 +1,9 @@ name: LocalStack Ephemeral Instance Test -on: pull_request +on: + pull_request: + paths-ignore: + - ./*.md + - LICENSE jobs: preview-test: @@ -7,10 +11,10 @@ jobs: name: 'Test ephemeral instance workflow' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Deploy Ephemeral Instance - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 with: uses: LocalStack/setup-localstack@${{ env.GH_ACTION_VERSION }} with: |- @@ -25,8 +29,8 @@ jobs: "extension-auto-install": "localstack-extension-mailhog", } env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} - GH_ACTION_VERSION: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }} + LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + GH_ACTION_VERSION: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.ref_name }} PREVIEW_CMD: |- awslocal s3 mb s3://test-bucket awslocal sqs create-queue --queue-name=test-queue @@ -67,7 +71,7 @@ jobs: # We want explicit shutdown - name: Shutdown ephemeral instance if: ${{ always() }} - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 with: uses: LocalStack/setup-localstack@${{ env.GH_ACTION_VERSION }} with: |- @@ -77,5 +81,5 @@ jobs: "state-action": "stop" } env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} - GH_ACTION_VERSION: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }} + LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + GH_ACTION_VERSION: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.ref_name }} diff --git a/LICENSE b/LICENSE index 8061eeb..dc9b6f9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Harsh Mishra +Copyright (c) 2022-2026 LocalStack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 817a293..907ca51 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A GitHub Action to setup [LocalStack](https://github.com/localstack/localstack) on your GitHub Actions runner workflow by: - Pulling a specific version of the LocalStack Docker Image into the GitHub Action runner. -- Configuring the [LocalStack CLI](https://docs.localstack.cloud/get-started/#localstack-cli) to launch the Docker container with an optional API token for pro usage. +- Configuring the [LocalStack CLI](https://docs.localstack.cloud/get-started/#localstack-cli) to launch the Docker container with an optional Auth Token for pro usage. - Installing [LocalStack AWS CLI](https://github.com/localstack/awscli-local), a thin wrapper around the `aws` command line interface for use with LocalStack to run integration tests over AWS services. - Export/import [LocalStack state](https://docs.localstack.cloud/user-guide/state-management/export-import-state/) as an artifact - Save/load [LocalStack Cloud Pods](https://docs.localstack.cloud/user-guide/state-management/cloud-pods/) @@ -18,20 +18,20 @@ A GitHub Action to setup [LocalStack](https://github.com/localstack/localstack) ```yml - name: Start LocalStack - uses: LocalStack/setup-localstack@v0.2.3 + uses: LocalStack/setup-localstack@v0.2.5 with: image-tag: 'latest' install-awslocal: 'true' env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} ``` -> **NOTE**: The `LOCALSTACK_API_KEY` environment variable is required to be set if `use-pro` is set to `true`. +> **NOTE**: The `LOCALSTACK_AUTH_TOKEN` environment variable is required to be set if `use-pro` is set to `true`. If the key is not found LocalStack by default falls back to the CE edition and displays a warning. ### Install only CLIs and startup later ```yml - name: Install LocalStack CLIs - uses: LocalStack/setup-localstack@v0.2.3 + uses: LocalStack/setup-localstack@v0.2.5 with: skip-startup: 'true' install-awslocal: 'true' @@ -39,46 +39,46 @@ If the key is not found LocalStack by default falls back to the CE edition and d ... - name: Start LocalStack - uses: LocalStack/setup-localstack@v0.2.3 + uses: LocalStack/setup-localstack@v0.2.5 with: image-tag: 'latest' env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} ``` ### Save a state later on in the pipeline ```yml - name: Save LocalStack State - uses: LocalStack/setup-localstack@v0.2.3 + uses: LocalStack/setup-localstack@v0.2.5 with: install-awslocal: 'true' state-backend: cloud-pods state-action: save state-name: my-cloud-pod env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} ``` -> **NOTE**: The `LOCALSTACK_API_KEY` environment variable is required to be set to save/load LocalStack's state either as a Cloud Pod or as a file artifact. +> **NOTE**: The `LOCALSTACK_AUTH_TOKEN` environment variable is required to be set to save/load LocalStack's state either as a Cloud Pod or as a file artifact. ### Load an already saved state ```yml - name: Start LocalStack and Load State - uses: LocalStack/setup-localstack@v0.2.3 + uses: LocalStack/setup-localstack@v0.2.5 with: install-awslocal: 'true' state-backend: cloud-pods state-action: load state-name: my-cloud-pod env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} ``` > **NOTE**: To load a **local state** from a different GitHub Actions workflow, one must set the `WORKFLOW_ID` environment variable. -> **NOTE**: The `LOCALSTACK_API_KEY` environment variable is required to be set to **save/load** LocalStack's state either as a Cloud Pod or as a file artifact. +> **NOTE**: The `LOCALSTACK_AUTH_TOKEN` environment variable is required to be set to **save/load** LocalStack's state either as a Cloud Pod or as a file artifact. ### Manage Application Previews (on an Ephemeral Instance) ```yml -uses: LocalStack/setup-localstack@v0.2.3 +uses: LocalStack/setup-localstack@v0.2.5 with: github-token: ${{ secrets.GITHUB_TOKEN }} state-backend: ephemeral @@ -88,18 +88,18 @@ uses: LocalStack/setup-localstack@v0.2.3 # Optional script/command to run preview-cmd: deploy.sh env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} ... with: - uses: LocalStack/setup-localstack@v0.2.3 + uses: LocalStack/setup-localstack@v0.2.5 with: github-token: ${{ secrets.GITHUB_TOKEN }} state-backend: ephemeral state-action: stop env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} ``` ## Inputs @@ -119,10 +119,10 @@ with: | `skip-ephemeral-stop` | Skip stopping LocalStack Ephemeral Instance | `false` | | `skip-startup` | Explicitly prevent LocalStack start up, only installs CLI(s). Recommended to manage state later on in the pipeline or start up an ephemeral instance. | `false` | | `skip-wait` | Skip waiting for LocalStack to start up | `false` | -| `state-action` | Valid values are `load`, `save`, `start`, `stop`, `` (empty, don't manage state). Values `start`/`stop` only usable with app previews. | `` | +| `state-action` | Valid values are `load`, `save`, `start`, `stop`, `''` (empty, don't manage state). Values `start`/`stop` only usable with app previews. | `''` | | `state-backend` | Either store the state of LocalStack locally, as a Cloud Pod or start an Ephemeral Instance. Valid values are `cloud-pods`, `ephemeral` or `local`. Use this option in unison with `state-action` to control behaviour. | `cloud-pods` | | `state-name` | Name of the state artifact (without extension) | `false` | -| `use-pro` | Whether to use the Pro version of LocalStack (requires API key to be configured) | `false` | +| `use-pro` | Whether to use the Pro version of LocalStack (requires Auth Token to be configured) | `false` | ## Example workflow ```yml @@ -137,7 +137,7 @@ jobs: - uses: actions/checkout@v3 - name: Start LocalStack - uses: LocalStack/setup-localstack@v0.2.3 + uses: LocalStack/setup-localstack@v0.2.5 with: image-tag: 'latest' install-awslocal: 'true' @@ -147,7 +147,7 @@ jobs: state-action: load state-name: my-cloud-pod env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} - name: Run Tests against LocalStack run: | @@ -156,13 +156,13 @@ jobs: echo "Test Execution complete!" - name: Save LocalStack State - uses: LocalStack/setup-localstack@v0.2.3 + uses: LocalStack/setup-localstack@v0.2.5 with: state-backend: local state-action: save state-name: my-ls-state-artifact env: - LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }} + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} WORKFLOW_ID: ${{ env.MY_GOLDEN_LS_STATE }} ``` diff --git a/action.yml b/action.yml index 587adc5..e889eee 100644 --- a/action.yml +++ b/action.yml @@ -16,7 +16,7 @@ inputs: required: true default: 'true' use-pro: - description: 'Whether to use LocalStack Pro (requires a valid API key)' + description: 'Whether to use LocalStack Pro (requires a valid CI Auth Token)' required: false default: 'false' configuration: @@ -97,7 +97,7 @@ runs: shell: bash - name: Install tools - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 if: ${{ inputs.skip-startup == 'true' || inputs.state-backend == 'ephemeral' || inputs.state-action == 'save' }} with: uses: ${{ env.GH_ACTION_ROOT }}/tools @@ -107,7 +107,7 @@ runs: } - name: Start Localstack - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 if: ${{ inputs.skip-startup != 'true' && inputs.state-backend != 'ephemeral' && inputs.state-action != 'save' }} with: # now we can dynamically determine sub-action path 🥳 @@ -126,7 +126,7 @@ runs: - name: Create Ephemeral Instance if: ${{ inputs.state-action == 'start' && inputs.state-backend == 'ephemeral' }} - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 with: uses: ${{ env.GH_ACTION_ROOT }}/ephemeral/startup with: |- @@ -141,7 +141,7 @@ runs: # Use different artifact from current workflow's by passing the workflow's id as WORKFLOW_ID env variable - name: Manage state if: ${{ inputs.state-action == 'save' || inputs.state-action == 'load' }} - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 with: uses: ${{ env.GH_ACTION_ROOT }}/${{ inputs.state-backend }} with: |- @@ -152,7 +152,7 @@ runs: - name: Display Ephemeral Instance URL if: ${{ inputs.state-action == 'start' && inputs.state-backend == 'ephemeral' && (inputs.include-preview == 'true' || inputs.ci-project != '') }} - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 with: uses: ${{ env.GH_ACTION_ROOT }}/finish with: |- @@ -164,7 +164,7 @@ runs: - name: Stop Ephemeral Instance if: ${{ (inputs.skip-ephemeral-stop == 'false' || inputs.state-action == 'stop') && inputs.state-backend == 'ephemeral' }} - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@8bc24f0360175e710da532c4d19eafdbed489a06 # v1 with: uses: ${{ env.GH_ACTION_ROOT }}/ephemeral/shutdown with: |- diff --git a/cloud-pods/action.yml b/cloud-pods/action.yml index 8058c1c..da73596 100644 --- a/cloud-pods/action.yml +++ b/cloud-pods/action.yml @@ -1,4 +1,5 @@ name: 'Save/Load a LocalStack Cloud Pod' +description: 'Save or load a LocalStack Cloud Pod' inputs: name: diff --git a/ephemeral/retry-function.sh b/ephemeral/retry-function.sh new file mode 100644 index 0000000..0ee20e1 --- /dev/null +++ b/ephemeral/retry-function.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# retry() function: Retries a given command up to 'retries' times with a 'wait' interval. +# Usage: retry +# Example: retry my_api_call_function +retry() { + local retries=5 + local count=0 + local wait=5 + local output + while [ $count -lt $retries ]; do + # We disable set -e for the command and capture its output. + output=$(set +e; "$@") + local exit_code=$? + if [ $exit_code -eq 0 ]; then + echo "$output" + return 0 + fi + count=$((count + 1)) + echo "Command failed with exit code $exit_code. Retrying in $wait seconds... ($count/$retries)" >&2 + sleep $wait + done + echo "Command failed after $retries retries." >&2 + echo "$output" # Also return the output of the last failed attempt for debugging + return 1 +} + +# Helper function to check for a JSON error response from the API +# Usage: check_for_api_error "" "" +check_for_api_error() { + local response="$1" + local context_message="$2" + if echo "$response" | jq -e 'if type == "object" and has("error") then true else false end' > /dev/null; then + echo "API error during '$context_message': $response" >&2 + return 1 + fi + return 0 +} \ No newline at end of file diff --git a/ephemeral/shutdown/action.yml b/ephemeral/shutdown/action.yml index 6b7910b..9acfd98 100644 --- a/ephemeral/shutdown/action.yml +++ b/ephemeral/shutdown/action.yml @@ -1,9 +1,10 @@ name: Shutdown Ephemeral Instance +description: 'Shutdowns an Ephemeral Instance (PR Preview)' inputs: localstack-api-key: - description: 'LocalStack API key used to access the platform api' - required: true + description: 'LocalStack Auth Token used to access the platform api' + required: false github-token: description: 'Github token used to create PR comments' required: true @@ -13,7 +14,7 @@ runs: using: composite steps: - name: Download PR artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: pr-id @@ -34,19 +35,41 @@ runs: - name: Shutdown ephemeral instance shell: bash run: | - response=$(curl -X DELETE \ - -s -o /dev/null -w "%{http_code}" \ - -H "ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" \ - -H "content-type: application/json" \ - https://api.localstack.cloud/v1/compute/instances/$previewName) - if [[ "$response" -ne 200 ]]; then - # In case the deletion fails, e.g. if the instance cannot be found, we raise a proper error on the platform - echo "Unable to delete preview environment. API response: $response" - exit 1 - fi + AUTH_HEADER="ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" + CONTENT_TYPE_HEADER="content-type: application/json" + API_URL_BASE="https://api.localstack.cloud/v1/compute/instances" + + source ${{ github.action_path }}/../retry-function.sh + shutdown_instance() { + # The API returns a 200 on successful deletion. + # We use --fail-with-body so curl fails on server errors (5xx) and triggers the retry. + local response + response=$(curl --fail-with-body -s -w "\n%{http_code}" -X DELETE \ + -H "$AUTH_HEADER" \ + -H "$CONTENT_TYPE_HEADER" \ + "$API_URL_BASE/$previewName") + local exit_code=$? + local http_code=$(echo "$response" | tail -n1) + local body=$(echo "$response" | sed '$d') + + if [ $exit_code -ne 0 ]; then + # A 404 means it's already gone, which is a success case for shutdown. + if [ "$http_code" -ne 404 ]; then + echo "Error deleting instance, curl failed with exit code $exit_code. API response: $body" >&2 + return 1 + fi + fi + if [ "$http_code" -eq 200 ]; then + echo "Instance '$previewName' deleted successfully." + elif [ "$http_code" -eq 404 ]; then + echo "Instance '$previewName' was already deleted (not found)." + fi + } + + retry shutdown_instance - name: Update status comment - uses: actions-cool/maintain-one-comment@v3.1.1 + uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3 with: token: ${{ inputs.github-token }} body: | diff --git a/ephemeral/startup/action.yml b/ephemeral/startup/action.yml index 5a580b3..c3d563d 100644 --- a/ephemeral/startup/action.yml +++ b/ephemeral/startup/action.yml @@ -1,11 +1,12 @@ name: Create PR Preview +description: 'Spins up an Ephemeral Instance for a PR Preview' inputs: github-token: description: 'Github token used to create PR comments' required: true localstack-api-key: - description: 'LocalStack API key used to create the preview environment' + description: 'LocalStack Auth Token used to create the preview environment' required: false preview-cmd: description: 'Command(s) used to create a preview of the PR (can use $AWS_ENDPOINT_URL)' @@ -28,21 +29,9 @@ inputs: runs: using: composite steps: - - run: > - echo "GH_ACTION_ROOT=$( - ls -d $( - ls -d ./../../_actions/* | - grep -i localstack | - tail -n1 - )/setup-localstack/* | - grep -v completed | - tail -n1 - )" >> $GITHUB_ENV - shell: bash - - name: Initial PR comment if: inputs.github-token - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@5175289a9a87978dcfcb9cf512b821d23b2a53eb # v1 with: uses: ${{ env.GH_ACTION_ROOT }}/prepare with: |- @@ -51,50 +40,102 @@ runs: } - name: Download PR artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: pr-id - name: Setup preview name shell: bash + id: preview-name run: | prId=$(> $GITHUB_ENV + echo "name=$previewName" >> $GITHUB_OUTPUT - name: Create preview environment shell: bash id: create-instance run: | + AUTH_HEADER="ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" + CONTENT_TYPE_HEADER="content-type: application/json" + API_URL_BASE="https://api.localstack.cloud/v1/compute/instances" + + source ${{ github.action_path }}/../retry-function.sh + + fetch_instances() { + local list_response + list_response=$(curl --fail-with-body -s -X GET \ + -H "$AUTH_HEADER" \ + -H "$CONTENT_TYPE_HEADER" \ + "$API_URL_BASE") + if [ $? -ne 0 ]; then echo "curl command failed while fetching instances. Response: $list_response" >&2; return 1; fi + if ! check_for_api_error "$list_response" "fetch instances"; then return 1; fi + echo "$list_response" + } + + if ! list_response=$(retry fetch_instances); then + echo "Error: Failed to fetch instances after multiple retries." + exit 1 + fi + autoLoadPod="${AUTO_LOAD_POD:-${{ inputs.auto-load-pod }}}" extensionAutoInstall="${EXTENSION_AUTO_INSTALL:-${{ inputs.extension-auto-install }}}" lifetime="${{ inputs.lifetime }}" - list_response=$(curl -X GET \ - -H "ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" \ - -H "content-type: application/json" \ - https://api.localstack.cloud/v1/compute/instances) - instance_exists=$(echo "$list_response" | jq --arg NAME "$previewName" '.[] | select(.instance_name == $NAME)') + delete_instance() { + # We expect a 200 on success or 404 if it's already gone. Other codes are errors. + local response + response=$(curl --fail-with-body -s -w "\n%{http_code}" -X DELETE \ + -H "$AUTH_HEADER" \ + -H "$CONTENT_TYPE_HEADER" \ + "$API_URL_BASE/$previewName") + local exit_code=$? + local http_code=$(echo "$response" | tail -n1) + local body=$(echo "$response" | sed '$d') + + if [ $exit_code -ne 0 ]; then echo "curl command failed while deleting instance. Response: $body" >&2; return 1; fi + if ! check_for_api_error "$body" "delete instance"; then return 1; fi + + if [ "$http_code" -eq 200 ]; then + echo "Instance '$previewName' deleted successfully." + fi + } + if [ -n "$instance_exists" ]; then - del_response=$(curl -X DELETE \ - -H "ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" \ - -H "content-type: application/json" \ - https://api.localstack.cloud/v1/compute/instances/$previewName) + echo "Found existing instance using '$previewName', trying to delete the old one..." + if ! retry delete_instance; then + echo "Error: Failed to delete existing instance after multiple retries." + exit 1 + fi fi - response=$(curl -X POST -d "{\"instance_name\": \"${previewName}\", \"lifetime\": ${lifetime} ,\"env_vars\": {\"AUTO_LOAD_POD\": \"${autoLoadPod}\", \"EXTENSION_AUTO_INSTALL\": \"${extensionAutoInstall}\"}}"\ - -H "ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" \ - -H "content-type: application/json" \ - https://api.localstack.cloud/v1/compute/instances) - endpointUrl=$(echo "$response" | jq -r .endpoint_url) - if [ "$endpointUrl" = "null" ] || [ "$endpointUrl" = "" ]; then - echo "Unable to create preview environment. API response: $response" + create_instance_func() { + local response + response=$(curl --fail-with-body -s -X POST -d "{\"instance_name\": \"${previewName}\", \"lifetime\": ${lifetime} ,\"env_vars\": {\"AUTO_LOAD_POD\": \"${autoLoadPod}\", \"EXTENSION_AUTO_INSTALL\": \"${extensionAutoInstall}\"}}"\ + -H "$AUTH_HEADER" \ + -H "$CONTENT_TYPE_HEADER" \ + "$API_URL_BASE") + if [ $? -ne 0 ]; then echo "curl command failed while creating instance. Response: $response" >&2; return 1; fi + if ! check_for_api_error "$response" "create instance"; then return 1; fi + if ! echo "$response" | jq -e 'has("endpoint_url") and (.endpoint_url | test(".+"))' > /dev/null; then + echo "Invalid response from instance creation API: $response" >&2; return 1; + fi + echo "$response" + } + + echo "Creating preview environment ..." + if ! response=$(retry create_instance_func); then + echo "Error: Failed to create preview environment after multiple retries." exit 1 fi + + endpointUrl=$(echo "$response" | jq -r .endpoint_url) + echo "Created preview environment with endpoint URL: $endpointUrl" echo $endpointUrl > ./ls-preview-url.txt @@ -102,7 +143,7 @@ runs: echo "AWS_ENDPOINT_URL=$endpointUrl" >> $GITHUB_ENV - name: Upload preview instance URL - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: preview-instance-url path: ./ls-preview-url.txt @@ -110,16 +151,46 @@ runs: - name: Run preview deployment if: ${{ inputs.preview-cmd != '' }} shell: bash - run: + run: | ${{ inputs.preview-cmd }} - name: Print logs of ephemeral instance if: ${{ !cancelled() && steps.create-instance.outcome == 'success' }} shell: bash + env: + previewName: ${{ steps.preview-name.outputs.name }} run: | - log_response=$(curl -X GET \ - -H "ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" \ - -H "content-type: application/json" \ - https://api.localstack.cloud/v1/compute/instances/$previewName/logs) - + AUTH_HEADER="ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" + CONTENT_TYPE_HEADER="content-type: application/json" + API_URL_BASE="https://api.localstack.cloud/v1/compute/instances" + + source ${{ github.action_path }}/../retry-function.sh + fetch_logs() { + local log_response + log_response=$(curl --fail-with-body -s -X GET \ + -H "$AUTH_HEADER" \ + -H "$CONTENT_TYPE_HEADER" \ + "$API_URL_BASE/$previewName/logs") + if [ $? -ne 0 ]; then echo "curl command failed while fetching logs. Response: $log_response" >&2; return 1; fi + if ! check_for_api_error "$log_response" "fetch logs"; then return 1; fi + + # A valid log response must be a JSON array. + if ! echo "$log_response" | jq -e 'if type == "array" then true else false end' > /dev/null; then + echo "Invalid response from logs API (expected a JSON array): $log_response" >&2; return 1; + fi + + # Check if the logs contain the "Ready." message, indicating the instance is fully started. + if ! echo "$log_response" | jq -e '.[] | select(.content | contains("Ready."))' > /dev/null; then + echo "Instance is not ready yet, waiting for 'Ready.' message in logs..." >&2 + return 1 + fi + + echo "$log_response" + } + echo "Fetching logs for $previewName ..." + if ! log_response=$(retry fetch_logs); then + echo "Error: Failed to fetch logs after multiple retries." + exit 1 + fi + echo "$previewName logs:" echo "$log_response" | jq -r '.[].content' diff --git a/finish/action.yml b/finish/action.yml index a8e539c..a960837 100644 --- a/finish/action.yml +++ b/finish/action.yml @@ -1,4 +1,5 @@ name: Finish CI Build +description: Finish CI Build inputs: github-token: @@ -20,14 +21,14 @@ runs: # Try to get pr artifact from current workflow - name: Download current PR artifact id: get-pr-artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 continue-on-error: true with: name: pr-id # If the above fails, try to get the latest pr artifact from the PR related workflows - name: Download latest PR artifact - uses: dawidd6/action-download-artifact@v6 + uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12 if: ${{ steps.get-pr-artifact.outcome == 'failure' }} with: name: pr-id @@ -43,13 +44,13 @@ runs: - name: Download preview instance URL id: get-preview-instance-url-artifact if: inputs.include-preview - uses: actions/download-artifact@v4 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: name: preview-instance-url # If the above fails, try to get the latest pr artifact from the PR related workflows - name: Download latest PR artifact - uses: dawidd6/action-download-artifact@v6 + uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12 if: ${{ steps.get-preview-instance-url-artifact.outcome == 'failure' }} with: name: preview-instance-url @@ -70,7 +71,7 @@ runs: fi - name: Update status comment - uses: actions-cool/maintain-one-comment@v3.1.1 + uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3 with: token: ${{ inputs.github-token }} body: | diff --git a/local/action.yml b/local/action.yml index e28824d..c0fb62c 100644 --- a/local/action.yml +++ b/local/action.yml @@ -1,4 +1,5 @@ name: 'Save/Load LocalStack state' +description: 'Save or load LocalStack state' inputs: name: @@ -17,14 +18,14 @@ runs: - name: Download current workflow's Local State artifact id: get-state-artifact if: ${{ inputs.action == 'load' }} - uses: actions/download-artifact@v4 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 #v7 continue-on-error: true with: name: ${{ inputs.name }} # If the above fails, try to get the latest artifact from given workflow - name: Download latest Local State artifact - uses: dawidd6/action-download-artifact@v6 + uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 #v12 if: ${{ inputs.action == 'load' && steps.get-state-artifact.outcome == 'failure' }} with: name: ${{ inputs.name }} @@ -48,7 +49,7 @@ runs: ACTION: "${{ inputs.action }}" - name: Upload LocalStack State - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 if: ${{ inputs.action == 'save' }} with: name: ${{ inputs.name }} diff --git a/prepare/action.yml b/prepare/action.yml index 0b48846..48ef59e 100644 --- a/prepare/action.yml +++ b/prepare/action.yml @@ -1,4 +1,5 @@ name: Start CI Build +description: Start CI Build inputs: github-token: @@ -16,13 +17,13 @@ runs: run: echo ${{ github.event.number }} > ./pr-id.txt - name: Upload PR number - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: pr-id path: ./pr-id.txt - name: Create initial PR comment - uses: actions-cool/maintain-one-comment@v3.1.1 + uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3 with: token: ${{ inputs.github-token }} body: | diff --git a/startup/action.yml b/startup/action.yml index ec88fd5..3cc4361 100644 --- a/startup/action.yml +++ b/startup/action.yml @@ -1,4 +1,5 @@ name: 'Start up Localstack' +description: 'Starts up Localstack' inputs: image-tag: @@ -10,7 +11,7 @@ inputs: required: true default: 'true' use-pro: - description: 'Whether to use LocalStack Pro (requires a valid API key)' + description: 'Whether to use LocalStack Pro (requires a valid CI Auth Token)' required: false default: 'false' configuration: @@ -50,7 +51,7 @@ runs: shell: bash - name: Install tools - uses: jenseng/dynamic-uses@v1 + uses: jenseng/dynamic-uses@5175289a9a87978dcfcb9cf512b821d23b2a53eb # v1 with: uses: ${{ env.GH_ACTION_ROOT }}/tools with: |- @@ -62,7 +63,7 @@ runs: run: | if [ "$USE_PRO" = true ]; then if [ "x$LOCALSTACK_AUTH_TOKEN" = "x" -o "x$LOCALSTACK_API_KEY" = "x" ]; then - echo "WARNING: LocalStack API key not detected, please verify your configuration..." + echo "WARNING: LocalStack Auth Token not detected, please verify your configuration..." fi CONFIGURATION="DNS_ADDRESS=127.0.0.1 ${CONFIGURATION}" IMAGE_NAME="${IMAGE_NAME:-localstack/localstack-pro:${IMAGE_TAG}}" diff --git a/tools/action.yml b/tools/action.yml index ee90f84..b4b1c33 100644 --- a/tools/action.yml +++ b/tools/action.yml @@ -1,4 +1,5 @@ name: 'Install Localstack tools' +description: 'Installs `localstack` and optionally `awslocal` CLI' inputs: install-awslocal: