Skip to content

Commit f80491e

Browse files
committed
Change to Kamal for deployments
1 parent 57aee7a commit f80491e

12 files changed

Lines changed: 546 additions & 0 deletions
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: Build Container
2+
permissions:
3+
packages: write
4+
contents: write
5+
on:
6+
workflow_run:
7+
workflows: ["Build"]
8+
types:
9+
- completed
10+
branches:
11+
- main
12+
- master
13+
workflow_dispatch:
14+
15+
env:
16+
DOCKER_BUILDKIT: 1
17+
KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
18+
KAMAL_REGISTRY_USERNAME: ${{ github.actor }}
19+
20+
jobs:
21+
build-container:
22+
runs-on: ubuntu-latest
23+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v3
27+
28+
- name: Set up environment variables
29+
run: |
30+
echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
31+
echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV
32+
echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
33+
echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV
34+
if [ -n "${{ secrets.APPSETTINGS_PATCH }}" ]; then
35+
echo "HAS_APPSETTINGS_PATCH=true" >> $GITHUB_ENV
36+
else
37+
echo "HAS_APPSETTINGS_PATCH=false" >> $GITHUB_ENV
38+
fi
39+
if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then
40+
echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV
41+
else
42+
echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV
43+
fi
44+
45+
# This step is for the deployment of the templates only, safe to delete
46+
- name: Modify csproj for template deploy
47+
if: env.HAS_DEPLOY_ACTION == 'true'
48+
run: |
49+
sed -i 's#<ContainerLabel Include="service" Value="ssg-examples" />#<ContainerLabel Include="service" Value="${{ env.repository_name_lower }}" />#g' SsgExamples/SsgExamples.csproj
50+
51+
- name: Check for Client directory and package.json
52+
id: check_client
53+
run: |
54+
if [ -d "SsgExamples.Client" ] && [ -f "SsgExamples.Client/package.json" ]; then
55+
echo "client_exists=true" >> $GITHUB_OUTPUT
56+
else
57+
echo "client_exists=false" >> $GITHUB_OUTPUT
58+
fi
59+
60+
- name: Setup Node.js
61+
if: steps.check_client.outputs.client_exists == 'true'
62+
uses: actions/setup-node@v3
63+
with:
64+
node-version: 22
65+
66+
- name: Install npm dependencies
67+
if: steps.check_client.outputs.client_exists == 'true'
68+
working-directory: ./SsgExamples.Client
69+
run: npm install
70+
71+
- name: Install x tool
72+
run: dotnet tool install -g x
73+
74+
- name: Apply Production AppSettings
75+
if: env.HAS_APPSETTINGS_PATCH == 'true'
76+
working-directory: ./SsgExamples
77+
run: |
78+
cat <<EOF >> appsettings.json.patch
79+
${{ secrets.APPSETTINGS_PATCH }}
80+
EOF
81+
x patch appsettings.json.patch
82+
83+
- name: Login to GitHub Container Registry
84+
uses: docker/login-action@v3
85+
with:
86+
registry: ghcr.io
87+
username: ${{ env.KAMAL_REGISTRY_USERNAME }}
88+
password: ${{ env.KAMAL_REGISTRY_PASSWORD }}
89+
90+
- name: Setup .NET
91+
uses: actions/setup-dotnet@v3
92+
with:
93+
dotnet-version: '8.0'
94+
95+
- name: Build and push Docker image
96+
run: |
97+
dotnet publish --os linux --arch x64 -c Release -p:ContainerRepository=${{ env.image_repository_name }} -p:ContainerRegistry=ghcr.io -p:ContainerImageTags=latest -p:ContainerPort=80

.github/workflows/release.yml

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
name: Release
2+
permissions:
3+
packages: write
4+
contents: write
5+
on:
6+
workflow_run:
7+
workflows: ["Build Container"]
8+
types:
9+
- completed
10+
branches:
11+
- main
12+
- master
13+
workflow_dispatch:
14+
15+
env:
16+
DOCKER_BUILDKIT: 1
17+
KAMAL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
18+
KAMAL_REGISTRY_USERNAME: ${{ github.actor }}
19+
20+
jobs:
21+
release:
22+
runs-on: ubuntu-latest
23+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v3
27+
28+
- name: Set up environment variables
29+
run: |
30+
echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
31+
echo "repository_name=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_ENV
32+
echo "repository_name_lower=$(echo ${{ github.repository }} | cut -d '/' -f 2 | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
33+
echo "org_name=$(echo ${{ github.repository }} | cut -d '/' -f 1)" >> $GITHUB_ENV
34+
if find . -maxdepth 2 -type f -name "Configure.Db.Migrations.cs" | grep -q .; then
35+
echo "HAS_MIGRATIONS=true" >> $GITHUB_ENV
36+
else
37+
echo "HAS_MIGRATIONS=false" >> $GITHUB_ENV
38+
fi
39+
if [ -n "${{ secrets.KAMAL_DEPLOY_IP }}" ]; then
40+
echo "HAS_DEPLOY_ACTION=true" >> $GITHUB_ENV
41+
else
42+
echo "HAS_DEPLOY_ACTION=false" >> $GITHUB_ENV
43+
fi
44+
45+
# This step is for the deployment of the templates only, safe to delete
46+
- name: Modify deploy.yml
47+
if: env.HAS_DEPLOY_ACTION == 'true'
48+
run: |
49+
sed -i "s/service: ssg-examples/service: ${{ env.repository_name_lower }}/g" config/deploy.yml
50+
sed -i "s#image: my-user/ssgexamples#image: ${{ env.image_repository_name }}#g" config/deploy.yml
51+
sed -i "s/- 192.168.0.1/- ${{ secrets.KAMAL_DEPLOY_IP }}/g" config/deploy.yml
52+
sed -i "s/host: ssg-examples.example.com/host: ${{ secrets.KAMAL_DEPLOY_HOST }}/g" config/deploy.yml
53+
sed -i "s/SsgExamples/${{ env.repository_name }}/g" config/deploy.yml
54+
55+
- name: Login to GitHub Container Registry
56+
uses: docker/login-action@v3
57+
with:
58+
registry: ghcr.io
59+
username: ${{ env.KAMAL_REGISTRY_USERNAME }}
60+
password: ${{ env.KAMAL_REGISTRY_PASSWORD }}
61+
62+
- name: Set up SSH key
63+
uses: webfactory/ssh-agent@v0.9.0
64+
with:
65+
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
66+
67+
- name: Setup Ruby
68+
uses: ruby/setup-ruby@v1
69+
with:
70+
ruby-version: 3.3.0
71+
bundler-cache: true
72+
73+
- name: Install Kamal
74+
run: gem install kamal -v 2.3.0
75+
76+
- name: Set up Docker Buildx
77+
uses: docker/setup-buildx-action@v3
78+
with:
79+
driver-opts: image=moby/buildkit:master
80+
81+
- name: Kamal bootstrap
82+
run: kamal server bootstrap
83+
84+
- name: Check if first run and execute kamal app boot if necessary
85+
run: |
86+
FIRST_RUN_FILE=".${{ env.repository_name }}"
87+
if ! kamal server exec --no-interactive -q "test -f $FIRST_RUN_FILE"; then
88+
kamal server exec --no-interactive -q "touch $FIRST_RUN_FILE" || true
89+
kamal deploy -q -P --version latest || true
90+
else
91+
echo "Not first run, skipping kamal app boot"
92+
fi
93+
94+
- name: Ensure file permissions
95+
run: |
96+
kamal server exec --no-interactive "mkdir -p /opt/docker/${{ env.repository_name }}/App_Data && chown -R 1654:1654 /opt/docker/${{ env.repository_name }}"
97+
98+
- name: Migration
99+
if: env.HAS_MIGRATIONS == 'true'
100+
run: |
101+
kamal server exec --no-interactive 'echo "${{ env.KAMAL_REGISTRY_PASSWORD }}" | docker login ghcr.io -u ${{ env.KAMAL_REGISTRY_USERNAME }} --password-stdin'
102+
kamal server exec --no-interactive "docker pull ghcr.io/${{ env.image_repository_name }}:latest || true"
103+
kamal app exec --no-reuse --no-interactive --version=latest "--AppTasks=migrate"
104+
105+
- name: Deploy with Kamal
106+
run: |
107+
kamal lock release -v
108+
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)