@hed-hog/cli 0.0.55 → 0.0.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,9 +6,16 @@ on:
6
6
  workflow_dispatch:
7
7
  env:
8
8
  REGISTRY: <%= config.containerRegistry %>
9
- CLUSTER_NAME: <%= config.clusterName %>
10
9
  NAMESPACE: <%= config.namespace %>
11
- API_PUBLIC_URL: <%= config.domain ? `https://api.${config.domain}` : '' %>
10
+ K8S_CLUSTER_ID: <%= config.k8sClusterId || config.clusterName %>
11
+ # API environment
12
+ API_FRONTEND_URL: <%= config.frontendUrl || (config.domain ? `https://${config.domain}` : '') %>
13
+ API_JWT_EXPIRES_IN: <%= config.jwtExpiresIn || '7d' %>
14
+ # Admin environment (runtime-replaced at container startup)
15
+ ADMIN_API_BASE_URL: <%= config.domain ? `https://api.${config.domain}` : '' %>
16
+ ADMIN_API_URL: <%= config.domain ? `https://api.${config.domain}` : '' %>
17
+ ADMIN_INTERNAL_API_URL: http://<%= config.appName %>-api:<%= config.appPorts?.api || 3000 %>
18
+ FRONTEND_URLS: <%= config.frontendUrls || (config.domain ? `https://${config.domain}` : '') %>
12
19
  jobs:
13
20
  apply-cluster-config:
14
21
  name: Apply Kubernetes and Helm configs
@@ -20,10 +27,8 @@ jobs:
20
27
  uses: digitalocean/action-doctl@v2
21
28
  with:
22
29
  token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
23
- - name: Set up Helm
24
- uses: azure/setup-helm@v4
25
30
  - name: Save DigitalOcean kubeconfig
26
- run: doctl kubernetes cluster kubeconfig save ${{ env.CLUSTER_NAME }}
31
+ run: doctl kubernetes cluster kubeconfig save ${{ env.K8S_CLUSTER_ID }}
27
32
  - name: Ensure namespace exists
28
33
  run: |
29
34
  kubectl create namespace ${{ env.NAMESPACE }} --dry-run=client -o yaml | kubectl apply -f -
@@ -32,12 +37,7 @@ jobs:
32
37
  <% if (config.setupSSL) { %> kubectl apply -f k8s/cert-manager-issuer.yaml
33
38
  <% } %><% (config.apps || []).forEach((app) => { %> kubectl apply -f k8s/<%= app %> -n ${{ env.NAMESPACE }}
34
39
  <% }); %><% if (config.setupIngress) { %> kubectl apply -f k8s/ingress.yaml -n ${{ env.NAMESPACE }}
35
- <% } %><% if ((config.infraServices || []).length > 0) { %> - name: Apply Helm chart(s)
36
- run: |
37
- <% config.infraServices.forEach((service) => { %> helm upgrade --install <%= config.appName %>-<%= service.name %> ./helm/services/<%= service.name %> \
38
- --namespace ${{ env.NAMESPACE }} \
39
- --create-namespace
40
- <% }); %><% } %>
40
+ <% } %><% if ((config.infraServices || []).length > 0) { %>
41
41
 
42
42
  <% if (config.apps.includes('api')) { %> deploy-api:
43
43
  name: Deploy API
@@ -50,29 +50,135 @@ jobs:
50
50
  uses: digitalocean/action-doctl@v2
51
51
  with:
52
52
  token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
53
+ - name: Validate DigitalOcean token
54
+ run: |
55
+ doctl auth validate
56
+ doctl account get
57
+ - name: Validate Container Registry access
58
+ run: |
59
+ doctl registry get
60
+ doctl registry repository list >/dev/null 2>&1 && doctl registry repository list || echo "doctl registry repository list not available in this version"
53
61
  - name: Log in to Container Registry
54
- run: doctl registry login
55
- - name: Build and push Docker image
62
+ run: doctl registry login --expiry-seconds 1200
63
+ - name: Check Docker auth config
64
+ run: |
65
+ if [ -f "$HOME/.docker/config.json" ]; then
66
+ grep -q 'registry.digitalocean.com' "$HOME/.docker/config.json" && echo "Docker auth entry for registry.digitalocean.com found"
67
+ else
68
+ echo "Docker config not found at $HOME/.docker/config.json"
69
+ exit 1
70
+ fi
71
+ - name: Registry auth probe push
72
+ run: |
73
+ docker pull hello-world
74
+ docker tag hello-world ${{ env.REGISTRY }}/ci-auth-probe:${{ github.run_id }}
75
+ docker push ${{ env.REGISTRY }}/ci-auth-probe:${{ github.run_id }}
76
+ - name: Build Docker image
56
77
  run: |
57
78
  docker build -t ${{ env.REGISTRY }}/<%= config.appName %>-api:${{ github.sha }} \
58
79
  -f apps/api/Dockerfile .
80
+ - name: Push Docker image (sha)
81
+ run: |
59
82
  docker push ${{ env.REGISTRY }}/<%= config.appName %>-api:${{ github.sha }}
83
+ - name: Push Docker image (latest)
84
+ run: |
60
85
  docker tag ${{ env.REGISTRY }}/<%= config.appName %>-api:${{ github.sha }} \
61
86
  ${{ env.REGISTRY }}/<%= config.appName %>-api:latest
62
87
  docker push ${{ env.REGISTRY }}/<%= config.appName %>-api:latest
88
+ - name: Build and push migrator image
89
+ run: |
90
+ docker build --target migrator \
91
+ -t ${{ env.REGISTRY }}/<%= config.appName %>-api-migrate:${{ github.sha }} \
92
+ -f apps/api/Dockerfile .
93
+ docker push ${{ env.REGISTRY }}/<%= config.appName %>-api-migrate:${{ github.sha }}
63
94
  - name: Save DigitalOcean kubeconfig
64
- run: doctl kubernetes cluster kubeconfig save ${{ env.CLUSTER_NAME }}
95
+ run: doctl kubernetes cluster kubeconfig save ${{ env.K8S_CLUSTER_ID }}
96
+ - name: Validate required GitHub secrets
97
+ env:
98
+ DATABASE_URL: ${{ secrets.DATABASE_URL }}
99
+ run: |
100
+ if [ -z "$DATABASE_URL" ]; then
101
+ echo "::error::GitHub secret DATABASE_URL is not set. Configure it in Settings > Secrets and variables > Actions."
102
+ echo "::error::Expected format: postgresql://postgres:<password>@postgresql-<%= config.appName %>:5432/<%= config.appName %>"
103
+ exit 1
104
+ fi
105
+ - name: Ensure API config exists
106
+ run: |
107
+ kubectl create configmap <%= config.appName %>-api-config \
108
+ -n ${{ env.NAMESPACE }} \
109
+ --from-literal=FRONTEND_URL='${{ env.API_FRONTEND_URL }}' \
110
+ --from-literal=JWT_EXPIRES_IN='${{ env.API_JWT_EXPIRES_IN }}' \
111
+ --dry-run=client -o yaml | kubectl apply -f -
112
+ - name: Ensure API secrets exist
113
+ run: |
114
+ kubectl create secret generic <%= config.appName %>-api-secrets \
115
+ -n ${{ env.NAMESPACE }} \
116
+ --from-literal=DATABASE_URL='${{ secrets.DATABASE_URL }}' \
117
+ --from-literal=JWT_SECRET='${{ secrets.JWT_SECRET }}' \
118
+ --from-literal=ENCRYPTION_SECRET='${{ secrets.ENCRYPTION_SECRET }}' \
119
+ --from-literal=PEPPER='${{ secrets.PEPPER }}' \
120
+ --dry-run=client -o yaml | kubectl apply -f -
121
+ - name: Run database migrations
122
+ run: |
123
+ kubectl delete job <%= config.appName %>-api-migrate -n ${{ env.NAMESPACE }} --ignore-not-found
124
+ cat <<EOF | kubectl apply -f -
125
+ apiVersion: batch/v1
126
+ kind: Job
127
+ metadata:
128
+ name: <%= config.appName %>-api-migrate
129
+ namespace: ${{ env.NAMESPACE }}
130
+ spec:
131
+ backoffLimit: 3
132
+ ttlSecondsAfterFinished: 300
133
+ template:
134
+ spec:
135
+ restartPolicy: Never
136
+ containers:
137
+ - name: migrate
138
+ image: ${{ env.REGISTRY }}/<%= config.appName %>-api-migrate:${{ github.sha }}
139
+ env:
140
+ - name: DATABASE_URL
141
+ valueFrom:
142
+ secretKeyRef:
143
+ name: <%= config.appName %>-api-secrets
144
+ key: DATABASE_URL
145
+ EOF
146
+ kubectl wait --for=condition=complete job/<%= config.appName %>-api-migrate \
147
+ -n ${{ env.NAMESPACE }} --timeout=120s || (
148
+ echo "--- Migration failed. Logs:"
149
+ kubectl logs job/<%= config.appName %>-api-migrate -n ${{ env.NAMESPACE }} --tail=50
150
+ exit 1
151
+ )
65
152
  - name: Deploy to Kubernetes
66
153
  run: |
67
154
  kubectl set image deployment/<%= config.appName %>-api \
68
155
  <%= config.appName %>-api=${{ env.REGISTRY }}/<%= config.appName %>-api:${{ github.sha }} \
69
156
  -n ${{ env.NAMESPACE }}
70
- kubectl rollout status deployment/<%= config.appName %>-api -n ${{ env.NAMESPACE }}
157
+ kubectl rollout status deployment/<%= config.appName %>-api -n ${{ env.NAMESPACE }} --timeout=5m || (
158
+ echo "--- Rollout timed out. Pod status:"
159
+ kubectl get pods -l app=<%= config.appName %>-api -n ${{ env.NAMESPACE }}
160
+ echo "--- Recent events:"
161
+ kubectl describe deployment/<%= config.appName %>-api -n ${{ env.NAMESPACE }} | tail -20
162
+ POD_NAME=$(kubectl get pods -l app=<%= config.appName %>-api -n ${{ env.NAMESPACE }} --sort-by=.metadata.creationTimestamp -o name | tail -n 1 | cut -d/ -f2)
163
+ if [ -n "$POD_NAME" ]; then
164
+ echo "--- Describe newest pod: $POD_NAME"
165
+ kubectl describe pod "$POD_NAME" -n ${{ env.NAMESPACE }}
166
+ echo "--- Current logs from $POD_NAME:"
167
+ kubectl logs "$POD_NAME" -n ${{ env.NAMESPACE }} --tail=120 || true
168
+ echo "--- Previous logs from $POD_NAME:"
169
+ kubectl logs "$POD_NAME" -n ${{ env.NAMESPACE }} --previous --tail=120 || true
170
+ fi
171
+ kubectl logs -l app=<%= config.appName %>-api -n ${{ env.NAMESPACE }} --tail=50 --previous 2>/dev/null || true
172
+ exit 1
173
+ )
71
174
 
72
175
  <% } %><% if (config.apps.includes('admin')) { %> deploy-admin:
73
176
  name: Deploy ADMIN
74
177
  runs-on: ubuntu-latest
75
- needs: apply-cluster-config
178
+ needs:
179
+ - apply-cluster-config
180
+ <% if (config.apps.includes('api')) { %> - deploy-api
181
+ <% } %>
76
182
  steps:
77
183
  - name: Checkout code
78
184
  uses: actions/checkout@v4
@@ -80,26 +186,74 @@ jobs:
80
186
  uses: digitalocean/action-doctl@v2
81
187
  with:
82
188
  token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
189
+ - name: Validate DigitalOcean token
190
+ run: |
191
+ doctl auth validate
192
+ doctl account get
193
+ - name: Validate Container Registry access
194
+ run: |
195
+ doctl registry get
196
+ doctl registry repository list >/dev/null 2>&1 && doctl registry repository list || echo "doctl registry repository list not available in this version"
83
197
  - name: Log in to Container Registry
84
- run: doctl registry login
85
- - name: Build and push Docker image
198
+ run: doctl registry login --expiry-seconds 1200
199
+ - name: Check Docker auth config
200
+ run: |
201
+ if [ -f "$HOME/.docker/config.json" ]; then
202
+ grep -q 'registry.digitalocean.com' "$HOME/.docker/config.json" && echo "Docker auth entry for registry.digitalocean.com found"
203
+ else
204
+ echo "Docker config not found at $HOME/.docker/config.json"
205
+ exit 1
206
+ fi
207
+ - name: Registry auth probe push
208
+ run: |
209
+ docker pull hello-world
210
+ docker tag hello-world ${{ env.REGISTRY }}/ci-auth-probe-admin:${{ github.run_id }}
211
+ docker push ${{ env.REGISTRY }}/ci-auth-probe-admin:${{ github.run_id }}
212
+ - name: Build Docker image
86
213
  run: |
87
214
  docker build -t ${{ env.REGISTRY }}/<%= config.appName %>-admin:${{ github.sha }} \
88
- --build-arg NEXT_PUBLIC_API_BASE_URL=${{ env.API_PUBLIC_URL }} \
89
- --build-arg NEXT_PUBLIC_API_URL=${{ env.API_PUBLIC_URL }} \
90
215
  -f apps/admin/Dockerfile .
216
+ - name: Push Docker image (sha)
217
+ run: |
91
218
  docker push ${{ env.REGISTRY }}/<%= config.appName %>-admin:${{ github.sha }}
219
+ - name: Push Docker image (latest)
220
+ run: |
92
221
  docker tag ${{ env.REGISTRY }}/<%= config.appName %>-admin:${{ github.sha }} \
93
222
  ${{ env.REGISTRY }}/<%= config.appName %>-admin:latest
94
223
  docker push ${{ env.REGISTRY }}/<%= config.appName %>-admin:latest
95
224
  - name: Save DigitalOcean kubeconfig
96
- run: doctl kubernetes cluster kubeconfig save ${{ env.CLUSTER_NAME }}
225
+ run: doctl kubernetes cluster kubeconfig save ${{ env.K8S_CLUSTER_ID }}
226
+ - name: Ensure admin config exists
227
+ run: |
228
+ kubectl create configmap <%= config.appName %>-admin-config \
229
+ -n ${{ env.NAMESPACE }} \
230
+ --from-literal=NEXT_PUBLIC_API_BASE_URL='${{ env.ADMIN_API_BASE_URL }}' \
231
+ --from-literal=NEXT_PUBLIC_API_URL='${{ env.ADMIN_API_URL }}' \
232
+ --from-literal=INTERNAL_API_URL='${{ env.ADMIN_INTERNAL_API_URL }}' \
233
+ --from-literal=FRONTEND_URLS='${{ env.FRONTEND_URLS }}' \
234
+ --dry-run=client -o yaml | kubectl apply -f -
97
235
  - name: Deploy to Kubernetes
98
236
  run: |
99
237
  kubectl set image deployment/<%= config.appName %>-admin \
100
238
  <%= config.appName %>-admin=${{ env.REGISTRY }}/<%= config.appName %>-admin:${{ github.sha }} \
101
239
  -n ${{ env.NAMESPACE }}
102
- kubectl rollout status deployment/<%= config.appName %>-admin -n ${{ env.NAMESPACE }}
240
+ kubectl rollout status deployment/<%= config.appName %>-admin -n ${{ env.NAMESPACE }} --timeout=5m || (
241
+ echo "--- Rollout timed out. Pod status:"
242
+ kubectl get pods -l app=<%= config.appName %>-admin -n ${{ env.NAMESPACE }}
243
+ echo "--- Recent events:"
244
+ kubectl describe deployment/<%= config.appName %>-admin -n ${{ env.NAMESPACE }} | tail -20
245
+ POD_NAME=$(kubectl get pods -l app=<%= config.appName %>-admin -n ${{ env.NAMESPACE }} --sort-by=.metadata.creationTimestamp -o name | tail -n 1 | cut -d/ -f2)
246
+ if [ -n "$POD_NAME" ]; then
247
+ echo "--- Describe newest pod: $POD_NAME"
248
+ kubectl describe pod "$POD_NAME" -n ${{ env.NAMESPACE }}
249
+ echo "--- Current logs from $POD_NAME:"
250
+ kubectl logs "$POD_NAME" -n ${{ env.NAMESPACE }} --tail=120 || true
251
+ echo "--- Previous logs from $POD_NAME:"
252
+ kubectl logs "$POD_NAME" -n ${{ env.NAMESPACE }} --previous --tail=120 || true
253
+ fi
254
+ kubectl logs -l app=<%= config.appName %>-admin -n ${{ env.NAMESPACE }} --tail=50 --previous 2>/dev/null || true
255
+ exit 1
256
+ )
103
257
  <% } %><% (config.apps || []).filter((app) => app !== 'api' && app !== 'admin').forEach((app) => { %>
104
258
  deploy-<%= app %>:
105
259
  name: Deploy <%= app.toUpperCase() %>
@@ -112,22 +266,54 @@ jobs:
112
266
  uses: digitalocean/action-doctl@v2
113
267
  with:
114
268
  token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
269
+ - name: Validate DigitalOcean token
270
+ run: |
271
+ doctl auth validate
272
+ doctl account get
273
+ - name: Validate Container Registry access
274
+ run: |
275
+ doctl registry get
276
+ doctl registry repository list >/dev/null 2>&1 && doctl registry repository list || echo "doctl registry repository list not available in this version"
115
277
  - name: Log in to Container Registry
116
- run: doctl registry login
117
- - name: Build and push Docker image
278
+ run: doctl registry login --expiry-seconds 1200
279
+ - name: Check Docker auth config
280
+ run: |
281
+ if [ -f "$HOME/.docker/config.json" ]; then
282
+ grep -q 'registry.digitalocean.com' "$HOME/.docker/config.json" && echo "Docker auth entry for registry.digitalocean.com found"
283
+ else
284
+ echo "Docker config not found at $HOME/.docker/config.json"
285
+ exit 1
286
+ fi
287
+ - name: Registry auth probe push
288
+ run: |
289
+ docker pull hello-world
290
+ docker tag hello-world ${{ env.REGISTRY }}/ci-auth-probe-<%= app %>:${{ github.run_id }}
291
+ docker push ${{ env.REGISTRY }}/ci-auth-probe-<%= app %>:${{ github.run_id }}
292
+ - name: Build Docker image
118
293
  run: |
119
294
  docker build -t ${{ env.REGISTRY }}/<%= config.appName %>-<%= app %>:${{ github.sha }} \
120
295
  -f apps/<%= app %>/Dockerfile .
296
+ - name: Push Docker image (sha)
297
+ run: |
121
298
  docker push ${{ env.REGISTRY }}/<%= config.appName %>-<%= app %>:${{ github.sha }}
299
+ - name: Push Docker image (latest)
300
+ run: |
122
301
  docker tag ${{ env.REGISTRY }}/<%= config.appName %>-<%= app %>:${{ github.sha }} \
123
302
  ${{ env.REGISTRY }}/<%= config.appName %>-<%= app %>:latest
124
303
  docker push ${{ env.REGISTRY }}/<%= config.appName %>-<%= app %>:latest
125
304
  - name: Save DigitalOcean kubeconfig
126
- run: doctl kubernetes cluster kubeconfig save ${{ env.CLUSTER_NAME }}
305
+ run: doctl kubernetes cluster kubeconfig save ${{ env.K8S_CLUSTER_ID }}
127
306
  - name: Deploy to Kubernetes
128
307
  run: |
129
308
  kubectl set image deployment/<%= config.appName %>-<%= app %> \
130
309
  <%= config.appName %>-<%= app %>=${{ env.REGISTRY }}/<%= config.appName %>-<%= app %>:${{ github.sha }} \
131
310
  -n ${{ env.NAMESPACE }}
132
- kubectl rollout status deployment/<%= config.appName %>-<%= app %> -n ${{ env.NAMESPACE }}
311
+ kubectl rollout status deployment/<%= config.appName %>-<%= app %> -n ${{ env.NAMESPACE }} --timeout=5m || (
312
+ echo "--- Rollout timed out. Pod status:"
313
+ kubectl get pods -l app=<%= config.appName %>-<%= app %> -n ${{ env.NAMESPACE }}
314
+ echo "--- Recent events:"
315
+ kubectl describe deployment/<%= config.appName %>-<%= app %> -n ${{ env.NAMESPACE }} | tail -20
316
+ kubectl logs -l app=<%= config.appName %>-<%= app %> -n ${{ env.NAMESPACE }} --tail=50 --previous 2>/dev/null || true
317
+ exit 1
318
+ )
133
319
  <% }); %>