Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/kamal deployment #3

Merged
merged 2 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/build-container.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Build Container
permissions:
packages: write
contents: write
on:
workflow_run:
workflows: ["Build"]
types:
- completed
branches:
- main
- master
workflow_dispatch:

env:
DOCKER_BUILDKIT: 1
KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
KAMAL_REGISTRY_USERNAME: ${{ github.actor }}

jobs:
build-container:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up environment variables
run: |
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_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
if [ -n "${{ secrets.APPSETTINGS_PATCH }}" ]; then
echo "HAS_APPSETTINGS_PATCH=true" >> $GITHUB_ENV
else
echo "HAS_APPSETTINGS_PATCH=false" >> $GITHUB_ENV
fi
if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then
echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV
else
echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV
fi

# This step is for the deployment of the templates only, safe to delete
- name: Modify csproj for template deploy
if: env.HAS_DEPLOY_ACTION == 'true'
run: |
sed -i 's#<ContainerLabel Include="service" Value="my-app" />#<ContainerLabel Include="service" Value="${{ env.repository_name_lower }}" />#g' MyApp/MyApp.csproj

- name: Check for Client directory and package.json
id: check_client
run: |
if [ -d "MyApp.Client" ] && [ -f "MyApp.Client/package.json" ]; then
echo "client_exists=true" >> $GITHUB_OUTPUT
else
echo "client_exists=false" >> $GITHUB_OUTPUT
fi

- name: Setup Node.js
if: steps.check_client.outputs.client_exists == 'true'
uses: actions/setup-node@v3
with:
node-version: 22

- name: Install npm dependencies
if: steps.check_client.outputs.client_exists == 'true'
working-directory: ./MyApp.Client
run: npm install

- name: Install x tool
run: dotnet tool install -g x

- name: Apply Production AppSettings
if: env.HAS_APPSETTINGS_PATCH == 'true'
working-directory: ./MyApp
run: |
cat <<EOF >> appsettings.json.patch
${{ secrets.APPSETTINGS_PATCH }}
EOF
x patch appsettings.json.patch

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ env.KAMAL_REGISTRY_USERNAME }}
password: ${{ env.KAMAL_REGISTRY_PASSWORD }}

- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0'

- name: Build and push Docker image
run: |
dotnet publish --os linux --arch x64 -c Release -p:ContainerRepository=${{ env.image_repository_name }} -p:ContainerRegistry=ghcr.io -p:ContainerImageTags=latest -p:ContainerPort=80
258 changes: 82 additions & 176 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,202 +3,108 @@ permissions:
packages: write
contents: write
on:
# Triggered on new GitHub Release
release:
types: [published]
# Triggered on every successful Build action
workflow_run:
workflows: ["Build"]
branches: [main,master]
workflows: ["Build Container"]
types:
- completed
# Manual trigger for rollback to specific release or redeploy latest
branches:
- main
- master
workflow_dispatch:
inputs:
version:
default: latest
description: Tag you want to release.
required: true

env:
DOCKER_BUILDKIT: 1
KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
KAMAL_REGISTRY_USERNAME: ${{ github.actor }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}

jobs:
push_to_registry:
release:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'failure' }}
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
# Checkout latest or specific tag
- name: checkout
if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }}
uses: actions/checkout@v3
- name: checkout tag
if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }}
- name: Checkout code
uses: actions/checkout@v3
with:
ref: refs/tags/${{ github.event.inputs.version }}

# Assign environment variables used in subsequent steps
- name: Env variable assignment
run: echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
# TAG_NAME defaults to 'latest' if not a release or manual deployment
- name: Assign version

- name: Set up environment variables
run: |
echo "TAG_NAME=latest" >> $GITHUB_ENV
if [ "${{ github.event.release.tag_name }}" != "" ]; then
echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
fi;
if [ "${{ github.event.inputs.version }}" != "" ]; then
echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV
fi;
if [ ! -z "${{ secrets.APPSETTINGS_PATCH }}" ]; then
echo "HAS_APPSETTINGS_PATCH=true" >> $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_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
if find . -maxdepth 2 -type f -name "Configure.Db.Migrations.cs" | grep -q .; then
echo "HAS_MIGRATIONS=true" >> $GITHUB_ENV
else
echo "HAS_MIGRATIONS=false" >> $GITHUB_ENV
fi
if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then
echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV
else
echo "HAS_APPSETTINGS_PATCH=false" >> $GITHUB_ENV
fi;

echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV
fi

# This step is for the deployment of the templates only, safe to delete
- name: Modify deploy.yml
if: env.HAS_DEPLOY_ACTION == 'true'
run: |
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/- 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/MyApp/${{ env.repository_name }}/g" config/deploy.yml

- name: Login to GitHub Container Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}


- name: Setup dotnet
uses: actions/setup-dotnet@v3
username: ${{ env.KAMAL_REGISTRY_USERNAME }}
password: ${{ env.KAMAL_REGISTRY_PASSWORD }}

- name: Set up SSH key
uses: webfactory/ssh-agent@v0.9.0
with:
dotnet-version: '8.0'

- name: Install x tool
if: env.HAS_APPSETTINGS_PATCH == 'true'
run: dotnet tool install -g x

- name: Apply Production AppSettings
if: env.HAS_APPSETTINGS_PATCH == 'true'
working-directory: ./AiServer
run: |
cat <<EOF >> appsettings.json.patch
${{ secrets.APPSETTINGS_PATCH }}
EOF
x patch appsettings.json.patch


# Build and push new docker image, skip for manual redeploy other than 'latest'
- name: Build and push Docker image
run: |
dotnet publish --os linux --arch x64 -c Release -p:ContainerRepository=${{ env.image_repository_name }} -p:ContainerRegistry=ghcr.io -p:ContainerImageTags=${{ env.TAG_NAME }} -p:ContainerPort=80
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

deploy_via_ssh:
needs: push_to_registry
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'failure' }}
steps:
# Checkout latest or specific tag
- name: checkout
if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }}
uses: actions/checkout@v3
- name: checkout tag
if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }}
uses: actions/checkout@v3
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ref: refs/tags/${{ github.event.inputs.version }}
ruby-version: 3.3.0
bundler-cache: true

- name: repository name fix and env
- name: Install Kamal
run: gem install kamal -v 2.3.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: image=moby/buildkit:master

- name: Kamal bootstrap
run: kamal server bootstrap

- name: Check if first run and execute kamal app boot if necessary
run: |
echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
echo "TAG_NAME=latest" >> $GITHUB_ENV
if [ "${{ github.event.release.tag_name }}" != "" ]; then
echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
fi;
if [ "${{ github.event.inputs.version }}" != "" ]; then
echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV
fi;
FIRST_RUN_FILE=".${{ env.repository_name }}"
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 deploy -q -P --version latest || true
else
echo "Not first run, skipping kamal app boot"
fi

- name: Create .env file
- name: Ensure file permissions
run: |
echo "Generating .env file"

echo "# Autogenerated .env file" > .deploy/.env
echo "HOST_DOMAIN=${{ secrets.DEPLOY_HOST }}" >> .deploy/.env
echo "LETSENCRYPT_EMAIL=${{ secrets.LETSENCRYPT_EMAIL }}" >> .deploy/.env
echo "APP_NAME=${{ github.event.repository.name }}" >> .deploy/.env
echo "IMAGE_REPO=${{ env.image_repository_name }}" >> .deploy/.env
echo "RELEASE_VERSION=${{ env.TAG_NAME }}" >> .deploy/.env
echo "CIVIT_AI_API_KEY=${{ secrets.CIVIT_AI_API_KEY }}" >> .deploy/.env
echo "REPLICATE_API_KEY=${{ secrets.REPLICATE_API_KEY }}" >> .deploy/.env
echo "GOOGLE_API_KEY=${{ secrets.GOOGLE_API_KEY }}" >> .deploy/.env
echo "GROQ_API_KEY=${{ secrets.GROQ_API_KEY }}" >> .deploy/.env
echo "MISTRAL_API_KEY=${{ secrets.MISTRAL_API_KEY }}" >> .deploy/.env
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .deploy/.env
echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" >> .deploy/.env

# Copy only the docker-compose.yml to remote server home folder
- name: copy files to target server via scp
uses: appleboy/scp-action@v0.1.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USERNAME }}
port: 22
key: ${{ secrets.DEPLOY_KEY }}
strip_components: 2
source: "./.deploy/docker-compose.yml,./.deploy/.env"
target: "~/.deploy/${{ github.event.repository.name }}/"

- name: Setup App_Data volume directory
uses: appleboy/ssh-action@v0.1.5
env:
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USERNAME }}
key: ${{ secrets.DEPLOY_KEY }}
port: 22
envs: APPTOKEN,USERNAME
script: |
set -e
echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin
cd ~/.deploy/${{ github.event.repository.name }}
docker compose pull
export APP_ID=$(docker compose run --entrypoint "id -u" --rm app)
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/App_Data" --user root --rm app
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/artifacts" --user root --rm app
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/files" --user root --rm app
kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}"

- name: Run remote db migrations
uses: appleboy/ssh-action@v0.1.5
env:
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USERNAME }}
key: ${{ secrets.DEPLOY_KEY }}
port: 22
envs: APPTOKEN,USERNAME
script: |
set -e
echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin
cd ~/.deploy/${{ github.event.repository.name }}
docker compose pull
export APP_ID=$(docker compose run --entrypoint "id -u" --rm app)
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/App_Data" --user root --rm app
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/artifacts" --user root --rm app
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/files" --user root --rm app
docker compose up app-migration --exit-code-from app-migration
- name: Migration
if: env.HAS_MIGRATIONS == 'true'
run: |
kamal server exec --no-interactive 'echo "${{ env.KAMAL_REGISTRY_PASSWORD }}" | docker login ghcr.io -u ${{ env.KAMAL_REGISTRY_USERNAME }} --password-stdin'
kamal server exec --no-interactive "docker pull ghcr.io/${{ env.image_repository_name }}:latest || true"
kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate"

# Deploy Docker image with your application using `docker compose up` remotely
- name: remote docker-compose up via ssh
uses: appleboy/ssh-action@v0.1.5
env:
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USERNAME }}
key: ${{ secrets.DEPLOY_KEY }}
port: 22
envs: APPTOKEN,USERNAME
script: |
echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin
cd ~/.deploy/${{ github.event.repository.name }}
docker compose pull
docker compose up app -d
- name: Deploy with Kamal
run: |
kamal lock release -v
kamal deploy -P --version latest
3 changes: 3 additions & 0 deletions .kamal/hooks/docker-setup.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

echo "Docker set up on $KAMAL_HOSTS..."
Loading
Loading