-
Notifications
You must be signed in to change notification settings - Fork 0
211 lines (197 loc) · 8.03 KB
/
Copy pathdeploy.yml
File metadata and controls
211 lines (197 loc) · 8.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# Reusable docs-site deploy: build the Docker image, push to GHCR, and deploy
# with Kamal to the oss-infrastructure server (behind a Cloudflare Tunnel +
# kamal-proxy). Every docs-kit site calls this so the deploy is defined ONCE.
#
# A site's own .github/workflows/deploy-docs.yml is a thin caller:
#
# jobs:
# deploy:
# uses: mhenrixon/docs-kit/.github/workflows/deploy.yml@main
# with:
# image: mhenrixon/my-docs
# secrets: inherit
#
# The image is built once (buildx, registry cache) and Kamal deploys it with
# --skip-push, so the image is never built twice.
name: Reusable docs deploy
on:
workflow_call:
inputs:
image:
description: >-
GHCR image repo, e.g. mhenrixon/daisyui (no registry, no tag). Use the
OWNER/REPO of the calling repo so the pushed package auto-links to the
repo — then GITHUB_TOKEN can push AND pull it, no PAT needed. (A name
that doesn't match the repo becomes an unlinked user-scoped package that
GITHUB_TOKEN can't pull.)
required: true
type: string
service:
description: >-
The Kamal service name (must equal `service:` in config/deploy.yml).
Stamped as the `service` image LABEL so Kamal's validate_image passes
on the --skip-push deploy — the image is built here (docker/build-push-action),
not by `kamal build`, so Kamal never injects the label itself.
required: true
type: string
ruby_version:
description: "Ruby for the Kamal CLI."
required: false
default: "3.4"
type: string
working_directory:
description: "The docs app dir holding config/deploy.yml (relative to repo root)."
required: false
default: "docs"
type: string
dockerfile:
description: "Dockerfile path relative to repo root."
required: false
default: "docs/Dockerfile"
type: string
build_context:
description: "Docker build context relative to repo root (repo root for a path-gem monorepo)."
required: false
default: "."
type: string
registry:
description: "Container registry host."
required: false
default: "ghcr.io"
type: string
# These are marked required: false on purpose. Callers pass them with
# `secrets: inherit`, and GitHub can't statically confirm an inherited secret
# satisfies a `required: true` declaration — that combination produces a
# startup_failure. They're still effectively required at runtime: the deploy
# step fails fast (below) if DEPLOY_HOST/DEPLOY_DOMAIN are empty, and the SSH
# agent step errors if SSH_PRIVATE_KEY is missing.
secrets:
SSH_PRIVATE_KEY:
description: "Deploy key for the Kamal SSH user on the deploy host."
required: false
DEPLOY_HOST:
description: "The deploy host (IP or DNS) — consumed by config/deploy.yml."
required: false
DEPLOY_DOMAIN:
description: "The public host kamal-proxy routes — consumed by config/deploy.yml."
required: false
concurrency:
group: deploy-docs-${{ inputs.image }}
cancel-in-progress: false
# Least privilege at the workflow level; each job widens only what it needs.
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
# Only the build job pushes to GHCR (docker/login-action + build-push-action).
permissions:
contents: read
packages: write
outputs:
image_tag: ${{ steps.tag.outputs.image_tag }}
steps:
# persist-credentials: false — the build COPYs the repo root (incl .git,
# kept for the gemspec's `git ls-files`) into the image, which is pushed to a
# public ghcr package; a GITHUB_TOKEN persisted in .git/config must not ride
# along into a published layer.
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Determine image tag
id: tag
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
if [ "$EVENT_NAME" = "release" ]; then
TAG="$RELEASE_TAG"
else
TAG="sha-$(git rev-parse --short HEAD)"
fi
echo "image_tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Building image tag: $TAG"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ inputs.registry }}
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: ${{ inputs.build_context }}
file: ${{ inputs.dockerfile }}
push: true
tags: |
${{ inputs.registry }}/${{ inputs.image }}:${{ steps.tag.outputs.image_tag }}
${{ inputs.registry }}/${{ inputs.image }}:latest
# Kamal's validate_image greps for this `service` label on the
# --skip-push deploy; since the image is built here and not by
# `kamal build`, we must stamp it ourselves.
labels: |
service=${{ inputs.service }}
cache-from: type=registry,ref=${{ inputs.registry }}/${{ inputs.image }}:buildcache
cache-to: type=registry,ref=${{ inputs.registry }}/${{ inputs.image }}:buildcache,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
environment: docs
# Pull-only (kamal deploy --skip-push); no image push, so packages: read.
permissions:
contents: read
packages: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ inputs.ruby_version }}
# Install the Kamal CLI directly so a consuming site doesn't need `kamal` in
# its docs Gemfile. `kamal deploy` (not `bundle exec kamal`) then works for
# every site regardless of its bundle.
- name: Install Kamal
run: gem install kamal
- name: Setup SSH agent
uses: webfactory/ssh-agent@v0.9.1
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Configure SSH for the deploy host
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# Disable host-key verification for the deploy host. Kamal uses net-ssh,
# which raises HostKeyMismatch on any known_hosts conflict (e.g. after
# the server is reprovisioned). net-ssh honors ~/.ssh/config, so this
# turns verification off for this single known IP. Acceptable here: a
# public read-only docs site reached over an authenticated deploy key.
cat >> ~/.ssh/config <<EOF
Host $DEPLOY_HOST
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
EOF
chmod 600 ~/.ssh/config
- name: Deploy with Kamal (image already pushed by the build job)
working-directory: ${{ inputs.working_directory }}
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_DOMAIN: ${{ secrets.DEPLOY_DOMAIN }}
KAMAL_REGISTRY_PASSWORD: ${{ github.token }}
IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
run: |
# DEPLOY_HOST/DEPLOY_DOMAIN are `docs`-environment secrets that only
# reach this job because the deploy job sets `environment: docs` AND the
# caller sets `secrets: inherit`. If either is dropped they resolve to ""
# with no error and Kamal would deploy against an empty host — fail fast.
if [ -z "$DEPLOY_HOST" ] || [ -z "$DEPLOY_DOMAIN" ]; then
echo "::error::DEPLOY_HOST/DEPLOY_DOMAIN empty — is the caller's 'secrets: inherit' set and does the deploy job keep 'environment: docs'?"
exit 1
fi
kamal deploy \
--skip-push \
--version="$IMAGE_TAG"