From 02582016d6c2b8479f44fda66f529acf70ba6cbf Mon Sep 17 00:00:00 2001 From: Darren Reid Date: Thu, 21 Nov 2024 09:28:01 +1100 Subject: [PATCH 1/2] feat: add Kamal deployment configuration --- .github/workflows/release.yml | 101 ++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a41086..e1d7f62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,101 +1,202 @@ name: Release +name: Release +permissions: permissions: packages: write + packages: write + contents: write contents: write on: +on: + workflow_run: workflow_run: + workflows: ["Build Container"] workflows: ["Build Container"] types: + types: + - completed - completed workflow_dispatch: + workflow_dispatch: + +env: env: + DOCKER_BUILDKIT: 1 DOCKER_BUILDKIT: 1 KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + KAMAL_REGISTRY_USERNAME: ${{ github.actor }} KAMAL_REGISTRY_USERNAME: ${{ github.actor }} + jobs: +jobs: + release: release: + runs-on: ubuntu-latest runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: steps: + - name: Checkout code - name: Checkout code uses: actions/checkout@v3 + uses: actions/checkout@v3 + + - name: Set up environment variables - name: Set up environment variables run: | + run: | + echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV + echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV + echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV + echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV + if find . -maxdepth 2 -type f -name "Configure.Db.Migrations.cs" | grep -q .; then if find . -maxdepth 2 -type f -name "Configure.Db.Migrations.cs" | grep -q .; then echo "HAS_MIGRATIONS=true" >> $GITHUB_ENV + echo "HAS_MIGRATIONS=true" >> $GITHUB_ENV else + else + echo "HAS_MIGRATIONS=false" >> $GITHUB_ENV echo "HAS_MIGRATIONS=false" >> $GITHUB_ENV fi + fi if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then + if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then + echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV + else else echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV + echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV + fi fi + # This step is for the deployment of the templates only, safe to delete + # This step is for the deployment of the templates only, safe to delete + - name: Modify deploy.yml - name: Modify deploy.yml + if: env.HAS_DEPLOY_ACTION == 'true' if: env.HAS_DEPLOY_ACTION == 'true' run: | + run: | + sed -i "s/service: my-app/service: ${{ env.repository_name_lower }}/g" config/deploy.yml sed -i "s/service: my-app/service: ${{ env.repository_name_lower }}/g" config/deploy.yml sed -i "s#image: my-user/myapp#image: ${{ env.image_repository_name }}#g" config/deploy.yml + sed -i "s#image: my-user/myapp#image: ${{ env.image_repository_name }}#g" config/deploy.yml sed -i "s/- 192.168.0.1/- ${{ secrets.KAMAL_DEPLOY_IP }}/g" config/deploy.yml + sed -i "s/- 192.168.0.1/- ${{ secrets.KAMAL_DEPLOY_IP }}/g" config/deploy.yml + sed -i "s/host: my-app.example.com/host: ${{ secrets.KAMAL_DEPLOY_HOST }}/g" config/deploy.yml sed -i "s/host: my-app.example.com/host: ${{ secrets.KAMAL_DEPLOY_HOST }}/g" config/deploy.yml sed -i "s/MyApp/${{ env.repository_name }}/g" config/deploy.yml + sed -i "s/MyApp/${{ env.repository_name }}/g" config/deploy.yml + + - name: Login to GitHub Container Registry - name: Login to GitHub Container Registry uses: docker/login-action@v3 + uses: docker/login-action@v3 + with: with: registry: ghcr.io + registry: ghcr.io + username: ${{ env.KAMAL_REGISTRY_USERNAME }} username: ${{ env.KAMAL_REGISTRY_USERNAME }} password: ${{ env.KAMAL_REGISTRY_PASSWORD }} + password: ${{ env.KAMAL_REGISTRY_PASSWORD }} + + - name: Set up SSH key - name: Set up SSH key + uses: webfactory/ssh-agent@v0.9.0 uses: webfactory/ssh-agent@v0.9.0 with: + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Setup Ruby + - name: Setup Ruby + uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1 + with: with: ruby-version: 3.3.0 + ruby-version: 3.3.0 + bundler-cache: true bundler-cache: true + - name: Install Kamal + - name: Install Kamal + run: gem install kamal -v 2.2.2 run: gem install kamal -v 2.2.2 + - name: Set up Docker Buildx + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3 + with: with: driver-opts: image=moby/buildkit:master + driver-opts: image=moby/buildkit:master + - name: Kamal bootstrap + - name: Kamal bootstrap + run: kamal server bootstrap run: kamal server bootstrap + + - name: Check if first run and execute kamal app boot if necessary - name: Check if first run and execute kamal app boot if necessary + run: | run: | FIRST_RUN_FILE=".${{ env.repository_name }}" + FIRST_RUN_FILE=".${{ env.repository_name }}" + if ! kamal server exec --no-interactive -q "test -f $FIRST_RUN_FILE"; then if ! kamal server exec --no-interactive -q "test -f $FIRST_RUN_FILE"; then + kamal server exec --no-interactive -q "touch $FIRST_RUN_FILE" || true kamal server exec --no-interactive -q "touch $FIRST_RUN_FILE" || true kamal deploy -q -P --version latest || true + kamal deploy -q -P --version latest || true + else else echo "Not first run, skipping kamal app boot" + echo "Not first run, skipping kamal app boot" fi + fi + + - name: Ensure file permissions - name: Ensure file permissions run: kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}" + run: kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}" + + - name: Migration - name: Migration if: env.HAS_MIGRATIONS == 'true' + if: env.HAS_MIGRATIONS == 'true' + run: kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate" run: kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate" + - name: Deploy with Kamal + - name: Deploy with Kamal + run: | run: | kamal lock release -v + kamal lock release -v + kamal deploy -P --version latest kamal deploy -P --version latest \ No newline at end of file From fff93d980f4921ad624adc649f119aeafa2496c9 Mon Sep 17 00:00:00 2001 From: Darren Reid Date: Thu, 21 Nov 2024 09:30:04 +1100 Subject: [PATCH 2/2] Fix deploy config files --- .github/workflows/release.yml | 202 ++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e1d7f62..6cb8e9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,202 +1,404 @@ name: Release name: Release +name: Release +name: Release +permissions: +permissions: permissions: permissions: packages: write packages: write + packages: write + packages: write + contents: write contents: write contents: write + contents: write +on: +on: on: on: workflow_run: workflow_run: + workflow_run: + workflow_run: + workflows: ["Build Container"] workflows: ["Build Container"] workflows: ["Build Container"] + workflows: ["Build Container"] + types: types: types: + types: + - completed - completed - completed + - completed + workflow_dispatch: + workflow_dispatch: workflow_dispatch: workflow_dispatch: + + +env: env: env: +env: + DOCKER_BUILDKIT: 1 + DOCKER_BUILDKIT: 1 DOCKER_BUILDKIT: 1 DOCKER_BUILDKIT: 1 KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + KAMAL_REGISTRY_USERNAME: ${{ github.actor }} + KAMAL_REGISTRY_USERNAME: ${{ github.actor }} KAMAL_REGISTRY_USERNAME: ${{ github.actor }} KAMAL_REGISTRY_USERNAME: ${{ github.actor }} + + +jobs: jobs: jobs: +jobs: + release: + release: release: release: runs-on: ubuntu-latest runs-on: ubuntu-latest + runs-on: ubuntu-latest + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} if: ${{ github.event.workflow_run.conclusion == 'success' }} if: ${{ github.event.workflow_run.conclusion == 'success' }} + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + steps: steps: steps: - name: Checkout code - name: Checkout code + - name: Checkout code + - name: Checkout code + uses: actions/checkout@v3 uses: actions/checkout@v3 uses: actions/checkout@v3 + uses: actions/checkout@v3 + + - name: Set up environment variables - name: Set up environment variables + - name: Set up environment variables + - name: Set up environment variables + run: | run: | run: | + run: | + echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV + echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV + echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV + echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV + echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV + if find . -maxdepth 2 -type f -name "Configure.Db.Migrations.cs" | grep -q .; then if find . -maxdepth 2 -type f -name "Configure.Db.Migrations.cs" | grep -q .; then if find . -maxdepth 2 -type f -name "Configure.Db.Migrations.cs" | grep -q .; then + if find . -maxdepth 2 -type f -name "Configure.Db.Migrations.cs" | grep -q .; then + echo "HAS_MIGRATIONS=true" >> $GITHUB_ENV + echo "HAS_MIGRATIONS=true" >> $GITHUB_ENV echo "HAS_MIGRATIONS=true" >> $GITHUB_ENV echo "HAS_MIGRATIONS=true" >> $GITHUB_ENV else else + else + else + echo "HAS_MIGRATIONS=false" >> $GITHUB_ENV echo "HAS_MIGRATIONS=false" >> $GITHUB_ENV echo "HAS_MIGRATIONS=false" >> $GITHUB_ENV + echo "HAS_MIGRATIONS=false" >> $GITHUB_ENV + fi + fi fi fi if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then + if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then + if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then + echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV + echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV + else + else else else echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV + echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV + echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV + fi + fi fi fi + + + # This step is for the deployment of the templates only, safe to delete # This step is for the deployment of the templates only, safe to delete # This step is for the deployment of the templates only, safe to delete + # This step is for the deployment of the templates only, safe to delete + - name: Modify deploy.yml + - name: Modify deploy.yml - name: Modify deploy.yml - name: Modify deploy.yml if: env.HAS_DEPLOY_ACTION == 'true' if: env.HAS_DEPLOY_ACTION == 'true' + if: env.HAS_DEPLOY_ACTION == 'true' + if: env.HAS_DEPLOY_ACTION == 'true' + run: | run: | run: | + run: | + sed -i "s/service: my-app/service: ${{ env.repository_name_lower }}/g" config/deploy.yml + sed -i "s/service: my-app/service: ${{ env.repository_name_lower }}/g" config/deploy.yml sed -i "s/service: my-app/service: ${{ env.repository_name_lower }}/g" config/deploy.yml sed -i "s/service: my-app/service: ${{ env.repository_name_lower }}/g" config/deploy.yml sed -i "s#image: my-user/myapp#image: ${{ env.image_repository_name }}#g" config/deploy.yml sed -i "s#image: my-user/myapp#image: ${{ env.image_repository_name }}#g" config/deploy.yml + sed -i "s#image: my-user/myapp#image: ${{ env.image_repository_name }}#g" config/deploy.yml + sed -i "s#image: my-user/myapp#image: ${{ env.image_repository_name }}#g" config/deploy.yml + sed -i "s/- 192.168.0.1/- ${{ secrets.KAMAL_DEPLOY_IP }}/g" config/deploy.yml sed -i "s/- 192.168.0.1/- ${{ secrets.KAMAL_DEPLOY_IP }}/g" config/deploy.yml sed -i "s/- 192.168.0.1/- ${{ secrets.KAMAL_DEPLOY_IP }}/g" config/deploy.yml + sed -i "s/- 192.168.0.1/- ${{ secrets.KAMAL_DEPLOY_IP }}/g" config/deploy.yml + sed -i "s/host: my-app.example.com/host: ${{ secrets.KAMAL_DEPLOY_HOST }}/g" config/deploy.yml + sed -i "s/host: my-app.example.com/host: ${{ secrets.KAMAL_DEPLOY_HOST }}/g" config/deploy.yml sed -i "s/host: my-app.example.com/host: ${{ secrets.KAMAL_DEPLOY_HOST }}/g" config/deploy.yml sed -i "s/host: my-app.example.com/host: ${{ secrets.KAMAL_DEPLOY_HOST }}/g" config/deploy.yml sed -i "s/MyApp/${{ env.repository_name }}/g" config/deploy.yml sed -i "s/MyApp/${{ env.repository_name }}/g" config/deploy.yml + sed -i "s/MyApp/${{ env.repository_name }}/g" config/deploy.yml + sed -i "s/MyApp/${{ env.repository_name }}/g" config/deploy.yml + + + - name: Login to GitHub Container Registry - name: Login to GitHub Container Registry - name: Login to GitHub Container Registry + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 uses: docker/login-action@v3 uses: docker/login-action@v3 + uses: docker/login-action@v3 + with: + with: with: with: registry: ghcr.io registry: ghcr.io + registry: ghcr.io + registry: ghcr.io + username: ${{ env.KAMAL_REGISTRY_USERNAME }} username: ${{ env.KAMAL_REGISTRY_USERNAME }} username: ${{ env.KAMAL_REGISTRY_USERNAME }} + username: ${{ env.KAMAL_REGISTRY_USERNAME }} + password: ${{ env.KAMAL_REGISTRY_PASSWORD }} + password: ${{ env.KAMAL_REGISTRY_PASSWORD }} password: ${{ env.KAMAL_REGISTRY_PASSWORD }} password: ${{ env.KAMAL_REGISTRY_PASSWORD }} + + + - name: Set up SSH key + - name: Set up SSH key - name: Set up SSH key - name: Set up SSH key uses: webfactory/ssh-agent@v0.9.0 uses: webfactory/ssh-agent@v0.9.0 + uses: webfactory/ssh-agent@v0.9.0 + uses: webfactory/ssh-agent@v0.9.0 + with: with: with: + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + + - name: Setup Ruby - name: Setup Ruby - name: Setup Ruby + - name: Setup Ruby + uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1 + with: with: with: + with: + ruby-version: 3.3.0 + ruby-version: 3.3.0 ruby-version: 3.3.0 ruby-version: 3.3.0 bundler-cache: true bundler-cache: true + bundler-cache: true + bundler-cache: true + + + - name: Install Kamal + - name: Install Kamal - name: Install Kamal - name: Install Kamal run: gem install kamal -v 2.2.2 run: gem install kamal -v 2.2.2 + run: gem install kamal -v 2.2.2 + run: gem install kamal -v 2.2.2 + + + - name: Set up Docker Buildx + - name: Set up Docker Buildx - name: Set up Docker Buildx - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v3 + with: with: with: + with: + driver-opts: image=moby/buildkit:master + driver-opts: image=moby/buildkit:master driver-opts: image=moby/buildkit:master driver-opts: image=moby/buildkit:master + + + - name: Kamal bootstrap + - name: Kamal bootstrap - name: Kamal bootstrap - name: Kamal bootstrap run: kamal server bootstrap run: kamal server bootstrap + run: kamal server bootstrap + run: kamal server bootstrap + + + - name: Check if first run and execute kamal app boot if necessary + - name: Check if first run and execute kamal app boot if necessary - name: Check if first run and execute kamal app boot if necessary - name: Check if first run and execute kamal app boot if necessary run: | run: | + run: | + run: | + FIRST_RUN_FILE=".${{ env.repository_name }}" FIRST_RUN_FILE=".${{ env.repository_name }}" FIRST_RUN_FILE=".${{ env.repository_name }}" + FIRST_RUN_FILE=".${{ env.repository_name }}" + if ! kamal server exec --no-interactive -q "test -f $FIRST_RUN_FILE"; then + if ! kamal server exec --no-interactive -q "test -f $FIRST_RUN_FILE"; then if ! kamal server exec --no-interactive -q "test -f $FIRST_RUN_FILE"; then if ! kamal server exec --no-interactive -q "test -f $FIRST_RUN_FILE"; then kamal server exec --no-interactive -q "touch $FIRST_RUN_FILE" || true kamal server exec --no-interactive -q "touch $FIRST_RUN_FILE" || true + kamal server exec --no-interactive -q "touch $FIRST_RUN_FILE" || true + kamal server exec --no-interactive -q "touch $FIRST_RUN_FILE" || true + kamal deploy -q -P --version latest || true kamal deploy -q -P --version latest || true kamal deploy -q -P --version latest || true + kamal deploy -q -P --version latest || true + else + else else else echo "Not first run, skipping kamal app boot" echo "Not first run, skipping kamal app boot" + echo "Not first run, skipping kamal app boot" + echo "Not first run, skipping kamal app boot" + fi fi fi + fi + + + - name: Ensure file permissions - name: Ensure file permissions - name: Ensure file permissions + - name: Ensure file permissions + run: kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}" + run: kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}" run: kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}" run: kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}" + + + - name: Migration - name: Migration - name: Migration + - name: Migration + if: env.HAS_MIGRATIONS == 'true' + if: env.HAS_MIGRATIONS == 'true' if: env.HAS_MIGRATIONS == 'true' if: env.HAS_MIGRATIONS == 'true' run: kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate" run: kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate" + run: kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate" + run: kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate" + + + - name: Deploy with Kamal + - name: Deploy with Kamal - name: Deploy with Kamal - name: Deploy with Kamal run: | run: | + run: | + run: | + kamal lock release -v kamal lock release -v kamal lock release -v + kamal lock release -v + kamal deploy -P --version latest + kamal deploy -P --version latest kamal deploy -P --version latest kamal deploy -P --version latest \ No newline at end of file