Skip to content

Commit d2ab4e7

Browse files
committed
Migrate to kamal
1 parent 640024d commit d2ab4e7

File tree

11 files changed

+394
-129
lines changed

11 files changed

+394
-129
lines changed

.github/workflows/release.yml

Lines changed: 85 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,155 +1,111 @@
1-
name: Release
1+
name: Release and Deploy
22
permissions:
33
packages: write
44
contents: write
55
on:
6-
# Triggered on new GitHub Release
7-
release:
8-
types: [published]
9-
# Triggered on every successful Build action
106
workflow_run:
117
workflows: ["Build"]
12-
branches: [main,master]
138
types:
149
- completed
15-
# Manual trigger for rollback to specific release or redeploy latest
1610
workflow_dispatch:
17-
inputs:
18-
version:
19-
default: latest
20-
description: Tag you want to release.
21-
required: true
2211

12+
# Only update envs here if you need to change them for this workflow
13+
env:
14+
DOCKER_BUILDKIT: 1
15+
KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
16+
KAMAL_REGISTRY_USERNAME: ${{ github.actor }}
17+
18+
# Standard steps for building and deploying a .NET app via Kamal
2319
jobs:
24-
push_to_registry:
25-
runs-on: ubuntu-22.04
26-
if: ${{ github.event.workflow_run.conclusion != 'failure' }}
20+
build-and-deploy:
21+
runs-on: ubuntu-latest
2722
steps:
28-
# Checkout latest or specific tag
29-
- name: checkout
30-
if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }}
31-
uses: actions/checkout@v3
32-
- name: checkout tag
33-
if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }}
23+
- name: Checkout code
3424
uses: actions/checkout@v3
35-
with:
36-
ref: refs/tags/${{ github.event.inputs.version }}
37-
38-
# Assign environment variables used in subsequent steps
39-
- name: Env variable assignment
40-
run: echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
41-
# TAG_NAME defaults to 'latest' if not a release or manual deployment
42-
- name: Assign version
25+
26+
- name: Set up environment variables
4327
run: |
44-
echo "TAG_NAME=latest" >> $GITHUB_ENV
45-
if [ "${{ github.event.release.tag_name }}" != "" ]; then
46-
echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
47-
fi;
48-
if [ "${{ github.event.inputs.version }}" != "" ]; then
49-
echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV
50-
fi;
51-
28+
echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
29+
echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV
30+
if find . -maxdepth 2 -type f -name "Configure.Db.Migrations.cs" | grep -q .; then
31+
echo "HAS_MIGRATIONS=true" >> $GITHUB_ENV
32+
else
33+
echo "HAS_MIGRATIONS=false" >> $GITHUB_ENV
34+
fi
35+
if [ -n "${{ secrets.APPSETTINGS_PATCH }}" ]; then
36+
echo "HAS_APPSETTINGS_PATCH=true" >> $GITHUB_ENV
37+
else
38+
echo "HAS_APPSETTINGS_PATCH=false" >> $GITHUB_ENV
39+
fi
40+
5241
- name: Login to GitHub Container Registry
53-
uses: docker/login-action@v2
42+
uses: docker/login-action@v3
5443
with:
5544
registry: ghcr.io
56-
username: ${{ github.actor }}
57-
password: ${{ secrets.GITHUB_TOKEN }}
58-
59-
60-
- name: Setup dotnet
45+
username: ${{ env.KAMAL_REGISTRY_USERNAME }}
46+
password: ${{ env.KAMAL_REGISTRY_PASSWORD }}
47+
48+
- name: Setup .NET
6149
uses: actions/setup-dotnet@v3
6250
with:
63-
dotnet-version: '8.*'
64-
65-
# Build and push new docker image, skip for manual redeploy other than 'latest'
66-
- name: Build and push Docker image
67-
run: |
68-
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
51+
dotnet-version: '8.0'
6952

70-
deploy_via_ssh:
71-
needs: push_to_registry
72-
runs-on: ubuntu-22.04
73-
if: ${{ github.event.workflow_run.conclusion != 'failure' }}
74-
steps:
75-
# Checkout latest or specific tag
76-
- name: checkout
77-
if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }}
78-
uses: actions/checkout@v3
79-
- name: checkout tag
80-
if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }}
81-
uses: actions/checkout@v3
82-
with:
83-
ref: refs/tags/${{ github.event.inputs.version }}
53+
- name: Install x tool
54+
run: dotnet tool install -g x
8455

85-
- name: repository name fix and env
56+
- name: Apply Production AppSettings
57+
if: env.HAS_APPSETTINGS_PATCH == 'true'
58+
working-directory: ./BlazorGalleryWasm
8659
run: |
87-
echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
88-
echo "TAG_NAME=latest" >> $GITHUB_ENV
89-
if [ "${{ github.event.release.tag_name }}" != "" ]; then
90-
echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
91-
fi;
92-
if [ "${{ github.event.inputs.version }}" != "" ]; then
93-
echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV
94-
fi;
95-
96-
- name: Create .env file
60+
cat <<EOF >> appsettings.json.patch
61+
${{ secrets.APPSETTINGS_PATCH }}
62+
EOF
63+
x patch appsettings.json.patch
64+
65+
- name: Build and push Docker image
9766
run: |
98-
echo "Generating .env file"
99-
100-
echo "# Autogenerated .env file" > .deploy/.env
101-
echo "HOST_DOMAIN=${{ secrets.DEPLOY_HOST }}" >> .deploy/.env
102-
echo "LETSENCRYPT_EMAIL=${{ secrets.LETSENCRYPT_EMAIL }}" >> .deploy/.env
103-
echo "APP_NAME=${{ github.event.repository.name }}" >> .deploy/.env
104-
echo "IMAGE_REPO=${{ env.image_repository_name }}" >> .deploy/.env
105-
echo "RELEASE_VERSION=${{ env.TAG_NAME }}" >> .deploy/.env
106-
107-
# Copy only the docker-compose.yml to remote server home folder
108-
- name: copy files to target server via scp
109-
uses: appleboy/[email protected]
67+
dotnet publish --os linux --arch x64 -c Release -p:ContainerRepository=${{ env.image_repository_name }} -p:ContainerRegistry=ghcr.io -p:ContainerImageTags=latest -p:ContainerPort=80
68+
69+
- name: Set up SSH key
70+
uses: webfactory/[email protected]
11071
with:
111-
host: ${{ secrets.DEPLOY_HOST }}
112-
username: ${{ secrets.DEPLOY_USERNAME }}
113-
port: 22
114-
key: ${{ secrets.DEPLOY_KEY }}
115-
strip_components: 2
116-
source: "./.deploy/docker-compose.yml,./.deploy/.env"
117-
target: "~/.deploy/${{ github.event.repository.name }}/"
118-
119-
- name: Run remote db migrations
120-
uses: appleboy/[email protected]
121-
env:
122-
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
123-
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
72+
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
73+
74+
- name: Setup Ruby
75+
uses: ruby/setup-ruby@v1
12476
with:
125-
host: ${{ secrets.DEPLOY_HOST }}
126-
username: ${{ secrets.DEPLOY_USERNAME }}
127-
key: ${{ secrets.DEPLOY_KEY }}
128-
port: 22
129-
envs: APPTOKEN,USERNAME
130-
script: |
131-
set -e
132-
echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin
133-
cd ~/.deploy/${{ github.event.repository.name }}
134-
docker compose pull
135-
export APP_ID=$(docker compose run --entrypoint "id -u" --rm app)
136-
docker compose run --entrypoint "chown $APP_ID:$APP_ID /app/App_Data" --user root --rm app
137-
docker compose up app-migration --exit-code-from app-migration
138-
139-
# Deploy Docker image with your application using `docker compose up` remotely
140-
- name: remote docker-compose up via ssh
141-
uses: appleboy/[email protected]
142-
env:
143-
APPTOKEN: ${{ secrets.GITHUB_TOKEN }}
144-
USERNAME: ${{ secrets.DEPLOY_USERNAME }}
77+
ruby-version: 3.3.0
78+
bundler-cache: true
79+
80+
- name: Install Kamal
81+
run: gem install kamal -v 2.2.2
82+
83+
- name: Set up Docker Buildx
84+
uses: docker/setup-buildx-action@v3
14585
with:
146-
host: ${{ secrets.DEPLOY_HOST }}
147-
username: ${{ secrets.DEPLOY_USERNAME }}
148-
key: ${{ secrets.DEPLOY_KEY }}
149-
port: 22
150-
envs: APPTOKEN,USERNAME
151-
script: |
152-
echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin
153-
cd ~/.deploy/${{ github.event.repository.name }}
154-
docker compose pull
155-
docker compose up app -d
86+
driver-opts: image=moby/buildkit:master
87+
88+
- name: Kamal bootstrap
89+
run: kamal server bootstrap
90+
91+
- name: Check if first run and execute kamal app boot if necessary
92+
run: |
93+
FIRST_RUN_FILE=".${{ env.repository_name }}"
94+
if ! kamal server exec --no-interactive -q "test -f $FIRST_RUN_FILE"; then
95+
kamal server exec --no-interactive -q "touch $FIRST_RUN_FILE" || true
96+
kamal deploy -q -P --version latest > /dev/null 2>&1 || true
97+
else
98+
echo "Not first run, skipping kamal app boot"
99+
fi
100+
101+
- name: Ensure file permissions
102+
run: kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}"
103+
104+
- name: Migration
105+
if: env.HAS_MIGRATIONS == 'true'
106+
run: kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate"
107+
108+
- name: Deploy with Kamal
109+
run: |
110+
kamal lock release -v
111+
kamal deploy -P --version latest

.kamal/hooks/docker-setup.sample

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
3+
echo "Docker set up on $KAMAL_HOSTS..."

.kamal/hooks/post-deploy.sample

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/sh
2+
3+
# A sample post-deploy hook
4+
#
5+
# These environment variables are available:
6+
# KAMAL_RECORDED_AT
7+
# KAMAL_PERFORMER
8+
# KAMAL_VERSION
9+
# KAMAL_HOSTS
10+
# KAMAL_ROLE (if set)
11+
# KAMAL_DESTINATION (if set)
12+
# KAMAL_RUNTIME
13+
14+
echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
3+
echo "Rebooted kamal-proxy on $KAMAL_HOSTS"

.kamal/hooks/pre-build.sample

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/bin/sh
2+
3+
# A sample pre-build hook
4+
#
5+
# Checks:
6+
# 1. We have a clean checkout
7+
# 2. A remote is configured
8+
# 3. The branch has been pushed to the remote
9+
# 4. The version we are deploying matches the remote
10+
#
11+
# These environment variables are available:
12+
# KAMAL_RECORDED_AT
13+
# KAMAL_PERFORMER
14+
# KAMAL_VERSION
15+
# KAMAL_HOSTS
16+
# KAMAL_ROLE (if set)
17+
# KAMAL_DESTINATION (if set)
18+
19+
if [ -n "$(git status --porcelain)" ]; then
20+
echo "Git checkout is not clean, aborting..." >&2
21+
git status --porcelain >&2
22+
exit 1
23+
fi
24+
25+
first_remote=$(git remote)
26+
27+
if [ -z "$first_remote" ]; then
28+
echo "No git remote set, aborting..." >&2
29+
exit 1
30+
fi
31+
32+
current_branch=$(git branch --show-current)
33+
34+
if [ -z "$current_branch" ]; then
35+
echo "Not on a git branch, aborting..." >&2
36+
exit 1
37+
fi
38+
39+
remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
40+
41+
if [ -z "$remote_head" ]; then
42+
echo "Branch not pushed to remote, aborting..." >&2
43+
exit 1
44+
fi
45+
46+
if [ "$KAMAL_VERSION" != "$remote_head" ]; then
47+
echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
48+
exit 1
49+
fi
50+
51+
exit 0

.kamal/hooks/pre-connect.sample

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env ruby
2+
3+
# A sample pre-connect check
4+
#
5+
# Warms DNS before connecting to hosts in parallel
6+
#
7+
# These environment variables are available:
8+
# KAMAL_RECORDED_AT
9+
# KAMAL_PERFORMER
10+
# KAMAL_VERSION
11+
# KAMAL_HOSTS
12+
# KAMAL_ROLE (if set)
13+
# KAMAL_DESTINATION (if set)
14+
# KAMAL_RUNTIME
15+
16+
hosts = ENV["KAMAL_HOSTS"].split(",")
17+
results = nil
18+
max = 3
19+
20+
elapsed = Benchmark.realtime do
21+
results = hosts.map do |host|
22+
Thread.new do
23+
tries = 1
24+
25+
begin
26+
Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
27+
rescue SocketError
28+
if tries < max
29+
puts "Retrying DNS warmup: #{host}"
30+
tries += 1
31+
sleep rand
32+
retry
33+
else
34+
puts "DNS warmup failed: #{host}"
35+
host
36+
end
37+
end
38+
39+
tries
40+
end
41+
end.map(&:value)
42+
end
43+
44+
retries = results.sum - hosts.size
45+
nopes = results.count { |r| r == max }
46+
47+
puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]

0 commit comments

Comments
 (0)