10 Commits

Author SHA1 Message Date
claude 9a33dc019a Promote staging->production: BuildKit CI, Compact JSON logging, consolidated compose + latest app code
Build and Push Docker Images / build (push) Successful in 3m23s
2026-06-18 19:14:59 +03:00
claude 1920885835 Merge main into production: PR #55 — Fix CV keyword extraction prompt
Build and Push Docker Images Staging / build (push) Successful in 52s
2026-06-09 16:41:03 +03:00
claude eb83d28ed5 Merge staging into production: PR #54 — Fix hardcoded user-facing strings
Build and Push Docker Images Staging / build (push) Successful in 10m36s
2026-06-08 22:34:35 +03:00
claude b6d9aea3bc Merge branch 'main' into production
Build and Push Docker Images Staging / build (push) Successful in 1m5s
2026-06-08 22:10:26 +03:00
claude 2b9132a3a9 Merge branch 'main' into production
Build and Push Docker Images Staging / build (push) Failing after 14s
2026-06-08 22:08:32 +03:00
claude e5bf56cc4d Merge branch 'main' into production
Build and Push Docker Images Staging / build (push) Successful in 42s
2026-06-08 21:48:24 +03:00
claude 8f58708cd9 Revert "Suppress environment prefix in email subjects on Production"
Build and Push Docker Images Staging / build (push) Successful in 1m35s
This reverts commit 06dd0140d6.
2026-06-08 21:45:45 +03:00
claude 06dd0140d6 Suppress environment prefix in email subjects on Production
[ENV_NAME] prefix is now only prepended in non-production environments
(Development, Staging, etc.). Production emails get a clean subject line.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 21:45:29 +03:00
claude 0aee7c4ed6 Changes
Build and Push Docker Images Staging / build (push) Successful in 45s
2026-06-08 21:31:35 +03:00
claude cd661fe613 Merge pull request 'Staging to Production' (#51) from main into production
Merge staging to production
2026-06-08 18:28:46 +00:00
6 changed files with 16 additions and 127 deletions
+2 -94
View File
@@ -11,7 +11,7 @@ on:
- production - production
env: env:
GIT_HOST: docker-git.easysoft.ro GIT_HOST: git.easysoft.ro
REGISTRY_HOST: registry.easysoft.ro REGISTRY_HOST: registry.easysoft.ro
DOCKER_BUILDKIT: "1" DOCKER_BUILDKIT: "1"
API_IMAGE: apps/myai-api API_IMAGE: apps/myai-api
@@ -23,7 +23,6 @@ env:
CV_SEARCH_JOB_IMAGE: apps/myai-cv-search-job CV_SEARCH_JOB_IMAGE: apps/myai-cv-search-job
PAGE_FETCHER_API_IMAGE: apps/myai-page-fetcher-api PAGE_FETCHER_API_IMAGE: apps/myai-page-fetcher-api
IMAGE_TAG: ${{ github.ref_name }} # branch name == image tag (staging | production) IMAGE_TAG: ${{ github.ref_name }} # branch name == image tag (staging | production)
WEB_PORT: "5140" # host port the web container is published on
jobs: jobs:
build: build:
@@ -61,8 +60,7 @@ jobs:
- name: Build Web image - name: Build Web image
run: | run: |
docker build --build-arg GIT_SHA="${{ github.sha }}" \ docker build -f web/Dockerfile -t "${REGISTRY_HOST}/${WEB_IMAGE}:${IMAGE_TAG}" .
-f web/Dockerfile -t "${REGISTRY_HOST}/${WEB_IMAGE}:${IMAGE_TAG}" .
- name: Build CV cleanup job image - name: Build CV cleanup job image
run: | run: |
@@ -108,97 +106,7 @@ jobs:
run: | run: |
docker push "${REGISTRY_HOST}/${PAGE_FETCHER_API_IMAGE}:${IMAGE_TAG}" docker push "${REGISTRY_HOST}/${PAGE_FETCHER_API_IMAGE}:${IMAGE_TAG}"
# Watchtower's poll is the fallback, not the mechanism: 30s on staging but 300s on
# production, so without this a green run can sit five minutes ahead of the deploy it
# claims to have made. Copied from easyDent, including the soft failure -- an unset
# secret or an unreachable API must degrade to the poll, never fail the build.
- name: Trigger Watchtower redeploy
env:
URL_STAGING: ${{ secrets.WATCHTOWER_URL_STAGING }}
URL_PRODUCTION: ${{ secrets.WATCHTOWER_URL_PRODUCTION }}
TOKEN: ${{ secrets.WATCHTOWER_TOKEN }}
run: |
if [ "${IMAGE_TAG}" = "production" ]; then URL="${URL_PRODUCTION}"; else URL="${URL_STAGING}"; fi
if [ -n "${URL}" ] && [ -n "${TOKEN}" ]; then
echo "Triggering Watchtower at ${URL}"
curl -sf -m 30 -H "Authorization: Bearer ${TOKEN}" "${URL}" && echo " -> redeploy triggered" \
|| echo " -> trigger failed; Watchtower will still pick it up on the next poll"
else
echo "Watchtower push-trigger not configured (WATCHTOWER_* secrets unset); relying on the poll interval."
fi
- name: Reclaim disk space (keep recent build cache) - name: Reclaim disk space (keep recent build cache)
if: always() if: always()
run: | run: |
docker image prune -f # dangling only (keep base images) docker image prune -f # dangling only (keep base images)
# Building and pushing an image proves nothing about what the host is running.
# Watchtower pulls asynchronously, and for a month it was pulling a tag nobody
# intended -- with every run green, because no step ever asked the deployed site
# what it was serving. This job asks.
#
# It polls the deploy host directly on the LAN rather than the public hostname:
# the runner sits inside the network, only easysoft.ro has a staging equivalent in
# public DNS, and going direct also takes Caddy and any CDN out of the answer.
smoke:
runs-on: host
needs: build
steps:
- name: Wait for the deploy host to serve this commit
run: |
case "${{ github.ref_name }}" in
staging) HOST=192.168.1.111 ;;
production) HOST=192.168.1.101 ;;
*) echo "::error::No deploy host mapped for '${{ github.ref_name }}'."; exit 1 ;;
esac
URL="http://${HOST}:${WEB_PORT}/version.json"
echo "Polling ${URL} for ${{ github.sha }}"
# 10 minutes: Watchtower's poke is fire-and-forget with a 30s fallback poll,
# and the container still has to start.
# ⚠️ Steps run under `bash -e -o pipefail`, so a polling loop has to be written
# defensively: the FIRST miss is the normal case, not an error.
# - `curl -sf | sed` fails the whole pipeline under pipefail while the old
# container is still up (404/connection refused), so `|| GOT=""` is required
# - `[ test ] && { ... }` returns non-zero when the test fails, which under -e
# aborts the step. Use `if`.
# Getting both wrong made the first run of this job fail in 20 seconds.
DEADLINE=$(( $(date +%s) + 600 ))
while :; do
GOT=$(curl -sf -m 15 "${URL}" 2>/dev/null | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') || GOT=""
if [ "${GOT}" = "${{ github.sha }}" ]; then
echo "Serving ${GOT}."
break
fi
if [ "$(date +%s)" -ge "${DEADLINE}" ]; then
echo "::error::Timed out after 10m. ${HOST} is serving '${GOT:-nothing}', wanted ${{ github.sha }}."
echo "Either Watchtower never pulled the new image, the container failed to"
echo "start, or the stack's IMAGE_TAG does not match this branch."
exit 1
fi
echo " still serving '${GOT:-nothing}' ..."
sleep 15
done
- name: Check the site actually answers
run: |
case "${{ github.ref_name }}" in
staging) HOST=192.168.1.111 ;;
production) HOST=192.168.1.101 ;;
esac
# `-L` follows redirects and we assert on the FINAL code, because a 302 from `/`
# is a healthy answer for a site running in UnderConstruction mode -- it means the
# app is up and routing. Asserting a bare 200 failed jecreativ.ro's first
# production deploy for doing exactly what it was configured to do.
#
# `|| CODE=000` for the same reason as above: curl exiting non-zero on a
# connection failure must produce a reportable code, not kill the step before
# it can say what went wrong. (`-s` without `-f` already tolerates 4xx/5xx.)
CODE=$(curl -sL -o /dev/null -w '%{http_code}' -m 20 "http://${HOST}:${WEB_PORT}/") || CODE=000
if [ "${CODE}" != "200" ]; then
echo "::error::Home page returned ${CODE}."
exit 1
fi
echo "Home page 200."
+2 -4
View File
@@ -49,10 +49,8 @@ Ai__Ollama__ChatModel=llama2
Ai__Ollama__EmbeddingModel=embedding-model Ai__Ollama__EmbeddingModel=embedding-model
Ai__Ollama__TimeoutSeconds=30 Ai__Ollama__TimeoutSeconds=30
# Database (shared) - maps to Database:Host etc. used by apps. # Database (shared) - maps to Database:Host etc. used by apps
# Deployed (staging/prod) uses the LAN DNS name (resolves to the MSSQL VM 10.0.0.240); Database__Host=sqlserver
# for local dev leave it unset to use the docker-compose 'sqlserver' service default.
Database__Host=mssql.easysoft.ro
Database__Port=1433 Database__Port=1433
Database__Name=MyAiDb Database__Name=MyAiDb
Database__User=sa Database__User=sa
+8 -16
View File
@@ -1,14 +1,6 @@
# ⚠️ The IMAGE_TAG fallback is a DELIBERATELY INVALID tag, not `staging`.
# On 2026-07-26 these stacks were recreated by hand and lost their environment
# variables. The old `${IMAGE_TAG:-staging}` then quietly resolved to `staging`, so the
# production host pulled staging images -- with no mail credentials and no recipient
# addresses -- and served them for a month. Nothing failed, because falling back to a
# real tag is indistinguishable from being configured. Now an unset IMAGE_TAG yields
# `IMAGE_TAG-NOT-SET`, the pull fails with "manifest not found", the running container
# is left untouched and the deploy goes red. Loud beats plausible.
services: services:
rag-api: rag-api:
image: registry.easysoft.ro/apps/myai-rag-api:${IMAGE_TAG:-IMAGE_TAG-NOT-SET} image: registry.easysoft.ro/apps/myai-rag-api:${IMAGE_TAG:-staging}
container_name: myai-rag-api container_name: myai-rag-api
environment: environment:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging} - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
@@ -58,7 +50,7 @@ services:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
cv-matcher-api: cv-matcher-api:
image: registry.easysoft.ro/apps/myai-cv-matcher-api:${IMAGE_TAG:-IMAGE_TAG-NOT-SET} image: registry.easysoft.ro/apps/myai-cv-matcher-api:${IMAGE_TAG:-staging}
container_name: myai-cv-matcher-api container_name: myai-cv-matcher-api
depends_on: depends_on:
- rag-api - rag-api
@@ -110,7 +102,7 @@ services:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
email-api: email-api:
image: registry.easysoft.ro/apps/myai-email-api:${IMAGE_TAG:-IMAGE_TAG-NOT-SET} image: registry.easysoft.ro/apps/myai-email-api:${IMAGE_TAG:-staging}
container_name: myai-email-api container_name: myai-email-api
environment: environment:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging} - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
@@ -151,7 +143,7 @@ services:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
api: api:
image: registry.easysoft.ro/apps/myai-api:${IMAGE_TAG:-IMAGE_TAG-NOT-SET} image: registry.easysoft.ro/apps/myai-api:${IMAGE_TAG:-staging}
container_name: myai-api container_name: myai-api
depends_on: depends_on:
- cv-matcher-api - cv-matcher-api
@@ -225,7 +217,7 @@ services:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
cv-cleanup-job: cv-cleanup-job:
image: registry.easysoft.ro/apps/myai-cv-cleanup-job:${IMAGE_TAG:-IMAGE_TAG-NOT-SET} image: registry.easysoft.ro/apps/myai-cv-cleanup-job:${IMAGE_TAG:-staging}
container_name: myai-cv-cleanup-job container_name: myai-cv-cleanup-job
depends_on: depends_on:
- api - api
@@ -255,7 +247,7 @@ services:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
cv-search-job: cv-search-job:
image: registry.easysoft.ro/apps/myai-cv-search-job:${IMAGE_TAG:-IMAGE_TAG-NOT-SET} image: registry.easysoft.ro/apps/myai-cv-search-job:${IMAGE_TAG:-staging}
container_name: myai-cv-search-job container_name: myai-cv-search-job
depends_on: depends_on:
- cv-matcher-api - cv-matcher-api
@@ -308,7 +300,7 @@ services:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
page-fetcher-api: page-fetcher-api:
image: registry.easysoft.ro/apps/myai-page-fetcher-api:${IMAGE_TAG:-IMAGE_TAG-NOT-SET} image: registry.easysoft.ro/apps/myai-page-fetcher-api:${IMAGE_TAG:-staging}
container_name: myai-page-fetcher-api container_name: myai-page-fetcher-api
environment: environment:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging} - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging}
@@ -340,7 +332,7 @@ services:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
web: web:
image: registry.easysoft.ro/apps/myai-web:${IMAGE_TAG:-IMAGE_TAG-NOT-SET} image: registry.easysoft.ro/apps/myai-web:${IMAGE_TAG:-staging}
container_name: myai-web container_name: myai-web
depends_on: depends_on:
- api - api
-9
View File
@@ -16,13 +16,4 @@ EXPOSE 8080
ENV ASPNETCORE_URLS=http://0.0.0.0:8080 ENV ASPNETCORE_URLS=http://0.0.0.0:8080
COPY --from=build /app/publish . COPY --from=build /app/publish .
# Stamp the commit into the image so a deploy can be verified from the outside.
#
# Without this a smoke test can only ask "does the site return 200?" -- which it did
# throughout the month production was quietly serving staging images. A status check
# cannot tell one build from another; /version.json can, so the smoke job refuses to
# pass until the host is actually serving THIS commit.
ARG GIT_SHA=unknown
RUN mkdir -p wwwroot && printf '{"version":"%s"}' "$GIT_SHA" > wwwroot/version.json
ENTRYPOINT ["dotnet", "web.dll"] ENTRYPOINT ["dotnet", "web.dll"]
+2 -2
View File
@@ -139,13 +139,13 @@
<div> <div>
<span data-i18n="contact.phone">Phone</span> <span data-i18n="contact.phone">Phone</span>
<strong> <strong>
<a href="tel:+40774885011">+40 774-885-011</a> <a href="tel:+40722523764">+40 722-523-764</a>
</strong> </strong>
</div> </div>
<div> <div>
<span>WhatsApp</span> <span>WhatsApp</span>
<strong> <strong>
<a href="https://wa.me/40774885011" target="_blank" rel="noreferrer">+40 774-885-011</a> <a href="https://wa.me/40722523764" target="_blank" rel="noreferrer">+40 722-523-764</a>
</strong> </strong>
</div> </div>
+2 -2
View File
@@ -126,13 +126,13 @@
<div> <div>
<span data-i18n="contact.phone">Phone</span> <span data-i18n="contact.phone">Phone</span>
<strong> <strong>
<a href="tel:+40774885011">+40 774-885-011</a> <a href="tel:+40722523764">+40 722-523-764</a>
</strong> </strong>
</div> </div>
<div> <div>
<span>WhatsApp</span> <span>WhatsApp</span>
<strong> <strong>
<a href="https://wa.me/40774885011" target="_blank" rel="noreferrer">+40 774-885-011</a> <a href="https://wa.me/40722523764" target="_blank" rel="noreferrer">+40 722-523-764</a>
</strong> </strong>
</div> </div>
</div> </div>